feat: configurable terminal cell width mode

新增全局「字体间距」偏好:紧凑(上游兼容默认)或宽松。

终端把字体的非整数平均 advance 转成整像素列宽时,上游一律向下取整。同样的
字体与字号下,向下取整会少一像素。宽松改用最接近整数取整补上
这一像素,紧凑保持现有取整,因此现有列数与换行行为不变。

该偏好只控制终端内容网格的列宽:单元格高度在两种模式下逐位相同,字体家族、
字号、字重、字形比例与回退顺序均不受影响。`refresh_ui_font` 计算的 Nebula
原生界面字体单元格固定按向下取整,不随该偏好变化。

切换时重推当前字体,让既有的字体更新路径重算单元格,网格、viewport、pane、
渲染器与 PTY 一致更新,无需重启。
This commit is contained in:
Sakyvo committed 2026-08-03 17:23:32 +08:00
1 parent f52b9a428c
commit 894c6e87c6
4 files changed
+259 -15

No files matched your search

+118 -12
View File
@@ -132,8 +132,8 @@ pub(crate) use ui::theme::write_nebula_prompt_theme;
pub(crate) fn caret_blink_on() -> bool {
ui::caret::is_on()
}
pub(crate) use settings::SettingsOpacityTarget;
pub use settings::{NebulaSettingsSection, SettingsDropdown, SettingsHit, settings_hit};
pub(crate) use settings::{CellWidthMode, SettingsOpacityTarget};
/// 按显示列宽贪心断行(确认框正文等 UI 段落用):CJK 逐字可断,行首空
/// 格吞掉;零宽字符跟随前一个字。不做拉丁连词回退——正文以中文为主,
@@ -1450,6 +1450,9 @@ pub struct Display {
/// 捕获态实时回显:当前按住的修饰键前缀("Ctrl+Shift+")。松开清空。
pub nebula_keymap_capture_preview: String,
pub nebula_tab_reveal_motion: settings::TabRevealMotion,
/// 单元格宽度模式。只作用于终端内容网格;Nebula 原生界面的字体单元格
/// 始终按上游的向下取整计算,不随该偏好变化。
pub nebula_cell_width_mode: settings::CellWidthMode,
pub nebula_font_family: String,
nebula_font_families: Vec<String>,
nebula_font_notice: Option<String>,
@@ -1707,7 +1710,8 @@ impl Display {
crate::boot_trace("glyph cache (font faces loaded)");
let metrics = glyph_cache.font_metrics();
let (cell_width, cell_height) = compute_cell_size(config, &metrics);
let (cell_width, cell_height) =
compute_cell_size(config, &metrics, settings_init.cell_width_mode);
// Resize the window to the user-configured size, or a Windows
// Terminal-like default when unset. A 116-column by 30-row canvas is
@@ -1725,7 +1729,9 @@ impl Display {
let base_font_size = config.font.size().scale(scale_factor);
let (base_cell_width, base_cell_height) = glyph_cache
.metrics_at(base_font_size)
.map(|base_metrics| compute_cell_size(config, &base_metrics))
.map(|base_metrics| {
compute_cell_size(config, &base_metrics, settings_init.cell_width_mode)
})
.unwrap_or((cell_width, cell_height));
let size = window_size(
config,
@@ -1968,6 +1974,7 @@ impl Display {
nebula_keymap_capture: None,
nebula_keymap_capture_preview: String::new(),
nebula_tab_reveal_motion: settings_init.tab_reveal,
nebula_cell_width_mode: settings_init.cell_width_mode,
nebula_font_family: settings_init.font_family,
nebula_font_families,
nebula_font_notice: None,
@@ -2992,6 +2999,7 @@ impl Display {
panel_resize: self.nebula_panel_resize,
cjk_bold_regular: self.nebula_cjk_bold_regular,
tab_reveal: self.nebula_tab_reveal_motion,
cell_width_mode: self.nebula_cell_width_mode,
preview_bg: self.preview_terminal_bg(),
preview_fg: {
let bg = self.preview_terminal_bg();
@@ -3234,6 +3242,22 @@ impl Display {
self.pending_update.dirty = true;
}
/// 切换单元格宽度模式。列宽取整方式变了就必须重算单元格——重推当前
/// 字体让字体更新路径走一遍,网格、viewport、pane 与 PTY 随之一致更新,
/// 无需重启。字号、字体家族与行高都不变。
pub fn set_cell_width_mode_option(&mut self, index: usize, base: &Font) {
if let Some(mode) = settings::CELL_WIDTH_MODE_OPTIONS.get(index)
&& *mode != self.nebula_cell_width_mode
{
self.nebula_cell_width_mode = *mode;
self.persist_nebula_settings();
let font = self.effective_font(base).with_size(self.font_size);
self.pending_update.set_font(font);
}
self.nebula_settings_dropdown = None;
self.pending_update.dirty = true;
}
/// Returns true when the default cursor style changed (the caller then
/// pushes the new default into every live terminal).
pub fn set_cursor_shape_option(&mut self, index: usize) -> bool {
@@ -4518,7 +4542,10 @@ impl Display {
fn refresh_ui_font(&mut self, config: &UiConfig) {
let ui_size = FontSize::from_px(self.nebula_ui_font.px);
let metrics = self.glyph_cache.set_ui_font_size(ui_size);
self.nebula_ui_font.cell = compute_cell_size(config, &metrics);
// 原生界面的字体单元格不受单元格宽度模式影响——该偏好只控制终端
// 内容网格的列宽。这里固定用上游的向下取整。
self.nebula_ui_font.cell =
compute_cell_size(config, &metrics, settings::CellWidthMode::Compact);
let ratio = self.ui_text_scale();
// Unconditional breadcrumb (tiny, a handful of lines per session):
// diagnosing "the sidebar zooms with the terminal" reports needs this
@@ -4812,6 +4839,7 @@ impl Display {
copy_on_select: self.nebula_copy_on_select,
cjk_bold_regular: self.nebula_cjk_bold_regular,
tab_reveal: self.nebula_tab_reveal_motion,
cell_width_mode: self.nebula_cell_width_mode,
theme: self.nebula_theme_preference,
follow_system_theme: self.nebula_follow_system_theme,
pinned_hosts: self.nebula_pinned_hosts.clone(),
@@ -4909,6 +4937,7 @@ impl Display {
self.reset_glyph_cache();
}
self.nebula_tab_reveal_motion = settings.tab_reveal;
self.nebula_cell_width_mode = settings.cell_width_mode;
self.nebula_window_opacity = settings.opacity;
self.nebula_background = if settings.follow_system_theme {
Some(active_theme.palette().term_bg)
@@ -5129,11 +5158,12 @@ impl Display {
glyph_cache: &mut GlyphCache,
config: &UiConfig,
font: &Font,
cell_width_mode: settings::CellWidthMode,
) -> (f32, f32) {
let _ = glyph_cache.update_font_size(font);
// Compute new cell sizes.
compute_cell_size(config, &glyph_cache.font_metrics())
compute_cell_size(config, &glyph_cache.font_metrics(), cell_width_mode)
}
/// Reset glyph cache.
@@ -5173,7 +5203,9 @@ impl Display {
// Update font size and cell dimensions.
if let Some(font) = pending_update.font() {
let cell_dimensions = Self::update_font_size(&mut self.glyph_cache, config, font);
let cell_width_mode = self.nebula_cell_width_mode;
let cell_dimensions =
Self::update_font_size(&mut self.glyph_cache, config, font, cell_width_mode);
cell_width = cell_dimensions.0;
cell_height = cell_dimensions.1;
@@ -8132,13 +8164,21 @@ impl FrameTimer {
///
/// This will return a tuple of the cell width and height.
#[inline]
fn compute_cell_size(config: &UiConfig, metrics: &crossfont::Metrics) -> (f32, f32) {
fn compute_cell_size(
config: &UiConfig,
metrics: &crossfont::Metrics,
cell_width_mode: settings::CellWidthMode,
) -> (f32, f32) {
let offset_x = f64::from(config.font.offset.x);
let offset_y = f64::from(config.font.offset.y);
(
(metrics.average_advance + offset_x).floor().max(1.) as f32,
(metrics.line_height + offset_y).floor().max(1.) as f32,
)
// 宽度取整方式由单元格宽度模式决定;高度始终向下取整,两种模式必须
// 得到逐位相同的高度——该偏好只控制列宽。
let raw_width = metrics.average_advance + offset_x;
let width = match cell_width_mode {
settings::CellWidthMode::Compact => raw_width.floor(),
settings::CellWidthMode::Relaxed => raw_width.round(),
};
(width.max(1.) as f32, (metrics.line_height + offset_y).floor().max(1.) as f32)
}
/// Calculate the size of the window given padding, terminal dimensions and cell size.
@@ -8177,11 +8217,77 @@ mod nebula_ux_tests {
use super::{
AI_LOGO_GROK_DARK_PNG, AI_LOGO_GROK_LIGHT_PNG, AiLogo, NebulaConfirm, SizeInfo, ai_logo,
alt_screen_vertical_padding_bands, extract_program, nebula_command_hint,
alt_screen_vertical_padding_bands, compute_cell_size, extract_program, nebula_command_hint,
percent_decode_lossy, prepare_ai_logo_texture, program_icon, remove_ssh_host_from_lists,
replays_untrusted_terminal_output, restore_ssh_host_to_lists, strip_file_scheme,
system_theme_snapshot,
};
use crate::config::UiConfig;
use crate::display::settings::CellWidthMode;
/// 受控字体度量:只有 advance 与 line_height 参与单元格尺寸计算,
/// 其余字段取任意合法值。
fn metrics(average_advance: f64, line_height: f64) -> crossfont::Metrics {
crossfont::Metrics {
average_advance,
line_height,
descent: -4.0,
underline_position: -2.0,
underline_thickness: 1.0,
strikeout_position: 5.0,
strikeout_thickness: 1.0,
}
}
#[test]
fn relaxed_cell_width_rounds_up_the_fraction_compact_floors_it() {
let config = UiConfig::default();
// Maple Mono NF CN 这类字体的平均 advance 常落在 .5 以上,紧凑向下
// 取整因此比 Windows Terminal 少一像素——宽松就是为补这一像素而设。
let m = metrics(9.6, 20.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Compact).0, 9.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Relaxed).0, 10.0);
}
#[test]
fn a_fraction_below_half_stays_on_the_same_column_width_in_both_modes() {
let config = UiConfig::default();
let m = metrics(9.4, 20.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Compact).0, 9.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Relaxed).0, 9.0);
}
#[test]
fn the_exact_half_boundary_rounds_away_from_zero_in_relaxed_mode() {
let config = UiConfig::default();
let m = metrics(9.5, 20.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Compact).0, 9.0);
assert_eq!(compute_cell_size(&config, &m, CellWidthMode::Relaxed).0, 10.0);
}
#[test]
fn both_modes_compute_the_same_cell_height() {
let config = UiConfig::default();
// 该偏好只控制列宽;高度必须逐位相同,否则行距会随模式漂移。
for (advance, line_height) in [(9.6, 20.7), (7.5, 16.5), (12.2, 25.9)] {
let m = metrics(advance, line_height);
let compact = compute_cell_size(&config, &m, CellWidthMode::Compact);
let relaxed = compute_cell_size(&config, &m, CellWidthMode::Relaxed);
assert_eq!(compact.1, relaxed.1, "line_height {line_height} 的高度在两模式间漂移");
}
}
#[test]
fn both_modes_share_the_same_minimum_cell_width() {
let config = UiConfig::default();
// 退化度量(字体加载异常)不能产出 0 宽单元格——那会让网格除零。
let m = metrics(0.3, 0.4);
let compact = compute_cell_size(&config, &m, CellWidthMode::Compact);
let relaxed = compute_cell_size(&config, &m, CellWidthMode::Relaxed);
assert_eq!(compact.0, 1.0);
assert_eq!(relaxed.0, 1.0);
assert_eq!(compact.1, relaxed.1, "退化度量下高度也不得随模式漂移");
}
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_owned()).collect()
+124 -3
View File
@@ -94,6 +94,7 @@ pub enum SettingsDropdown {
Accept,
CursorShape,
TabReveal,
CellWidthMode,
/// 背景色:色板网格 + 16 进制输入的专用浮层(不是通用行列表)。
BackgroundColor,
}
@@ -143,6 +144,9 @@ pub(super) const ACCEPT_OPTIONS: [AcceptKey; 3] =
pub(super) const TAB_REVEAL_OPTIONS: [TabRevealMotion; 2] =
[TabRevealMotion::Slide, TabRevealMotion::Instant];
pub(super) const CELL_WIDTH_MODE_OPTIONS: [CellWidthMode; 2] =
[CellWidthMode::Compact, CellWidthMode::Relaxed];
/// Order mirrors the appearance page the user referenced.
pub(super) const CURSOR_SHAPE_OPTIONS: [CursorShape; 4] =
[CursorShape::Beam, CursorShape::Underline, CursorShape::Block, CursorShape::HollowBlock];
@@ -178,6 +182,41 @@ fn tab_reveal_label(motion: TabRevealMotion, language: UiLanguage) -> &'static s
}
}
/// 单元格宽度模式:终端把字体的非整数设计宽度转换为整像素列宽的方式。
/// 「紧凑」保持上游的向下取整并作为兼容默认;「宽松」采用最接近整数取整,
/// 补足与 Windows Terminal 相差的那一像素。它只影响列宽,不改变单元格高度、
/// 字形比例或原生界面排版。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum CellWidthMode {
#[default]
Compact,
Relaxed,
}
impl CellWidthMode {
fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"compact" => Some(Self::Compact),
"relaxed" => Some(Self::Relaxed),
_ => None,
}
}
fn settings_value(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Relaxed => "relaxed",
}
}
}
fn cell_width_mode_label(mode: CellWidthMode, language: UiLanguage) -> &'static str {
match mode {
CellWidthMode::Compact => language.pick("紧凑", "Compact"),
CellWidthMode::Relaxed => language.pick("宽松", "Relaxed"),
}
}
pub(super) fn cursor_shape_label(shape: CursorShape, language: UiLanguage) -> &'static str {
match shape {
CursorShape::Beam => language.pick("条形(│)", "Bar (│)"),
@@ -240,6 +279,8 @@ pub enum SettingsHit {
CjkBoldToggle,
TabRevealDropdown,
TabRevealOption(usize),
CellWidthModeDropdown,
CellWidthModeOption(usize),
/// Language combobox trigger (options resolve to [`SettingsHit::Language`]).
LanguageDropdown,
/// Expanded dropdown option rows for the cycle-style settings.
@@ -309,6 +350,7 @@ pub(super) struct NebulaRuntimeSettings {
/// 默认开:小字号下雅黑 Bold fallback 与 Regular 混排发闷(任务 #4)。
pub(super) cjk_bold_regular: bool,
pub(super) tab_reveal: TabRevealMotion,
pub(super) cell_width_mode: CellWidthMode,
pub(super) fetch: bool,
pub(super) powerline: bool,
/// Window close keeps the PTYs alive in the resident process (detach /
@@ -381,6 +423,7 @@ pub(super) fn nebula_settings_load(config: &UiConfig) -> NebulaRuntimeSettings {
copy_on_select: true,
cjk_bold_regular: true,
tab_reveal: TabRevealMotion::Slide,
cell_width_mode: CellWidthMode::Compact,
// Off by default: the welcome screen pipes a whole script through the
// fresh shell and repaints on resize — real startup-latency cost on
// the critical path (user ruling: startup speed outranks the art).
@@ -468,6 +511,9 @@ pub(super) fn nebula_settings_load(config: &UiConfig) -> NebulaRuntimeSettings {
Some(("tab_reveal", v)) => {
settings.tab_reveal = TabRevealMotion::parse(v).unwrap_or_default();
},
Some(("cell_width_mode", v)) => {
settings.cell_width_mode = CellWidthMode::parse(v).unwrap_or_default();
},
Some(("startup_directory", v)) => {
let path = std::path::PathBuf::from(v.trim());
if path.is_dir() {
@@ -681,7 +727,7 @@ pub(super) fn nebula_settings_write(settings: &NebulaRuntimeSettings) {
path,
format!(
"language={}\ntheme={theme}\nfollow_system_theme={}\nghost={}\naccept={accept}\nshell={shell}\nstartup_directory={startup_directory}\nfont_family={}\nfont_size={font_size}\ncursor_shape={}\ncursor_blink={}\ncopy_on_select={}\ncjk_bold_regular={}
tab_reveal={}\nfetch={}\npowerline={}\nkeep_session={}\nrestore_session={}\nblur={}\nopacity={:.2}\nbackground={background}\nbackground_image={background_image}\nbackground_image_opacity={:.2}\nbackground_image_fit={}\nbackground_image_alignment={}\nbackground_image_cover_chrome={}\npanel_resize={}\nsidebar_w={:.0}\ndrawer_w={:.0}\nhosts_band={:.0}\npinned_hosts={pinned_hosts}\nsaved_hosts={saved_hosts}\nhidden_hosts={hidden_hosts}\nquick_terminal_hotkey={quick_terminal_hotkey}\n{keybinds}",
tab_reveal={}\ncell_width_mode={}\nfetch={}\npowerline={}\nkeep_session={}\nrestore_session={}\nblur={}\nopacity={:.2}\nbackground={background}\nbackground_image={background_image}\nbackground_image_opacity={:.2}\nbackground_image_fit={}\nbackground_image_alignment={}\nbackground_image_cover_chrome={}\npanel_resize={}\nsidebar_w={:.0}\ndrawer_w={:.0}\nhosts_band={:.0}\npinned_hosts={pinned_hosts}\nsaved_hosts={saved_hosts}\nhidden_hosts={hidden_hosts}\nquick_terminal_hotkey={quick_terminal_hotkey}\n{keybinds}",
settings.language.as_str(),
settings.follow_system_theme as u8,
settings.ghost as u8,
@@ -691,6 +737,7 @@ tab_reveal={}\nfetch={}\npowerline={}\nkeep_session={}\nrestore_session={}\nblur
settings.copy_on_select as u8,
settings.cjk_bold_regular as u8,
settings.tab_reveal.settings_value(),
settings.cell_width_mode.settings_value(),
settings.fetch as u8,
settings.powerline as u8,
settings.keep_session as u8,
@@ -729,6 +776,7 @@ struct SettingsGeometry {
font: (f32, f32, f32, f32),
/// "字号" spinner row; the value box + steppers derive via `widgets`.
font_size_row: (f32, f32, f32, f32),
cell_width_mode: (f32, f32, f32, f32),
fetch: (f32, f32, f32, f32),
powerline: (f32, f32, f32, f32),
ghost: (f32, f32, f32, f32),
@@ -923,7 +971,8 @@ fn settings_geometry(
let startup_directory_y0 = shell_y0 + ROW_H;
let font_y0 = startup_directory_y0 + ROW_H;
let font_size_y0 = font_y0 + ROW_H;
let fetch_y0 = font_size_y0 + ROW_H;
let cell_width_mode_y0 = font_size_y0 + ROW_H;
let fetch_y0 = cell_width_mode_y0 + ROW_H;
let ghost_y0 = fetch_y0 + 2.0 * ROW_H + GROUP_ADVANCE;
let open_y0 = ghost_y0 + 2.0 * ROW_H + GROUP_ADVANCE;
let hidden_y0 = open_y0 + ROW_H + GROUP_ADVANCE;
@@ -1010,6 +1059,7 @@ fn settings_geometry(
),
font: (row_x, at(font_y0), row_w, row_h),
font_size_row: (row_x, at(font_size_y0), row_w, row_h),
cell_width_mode: (row_x, at(cell_width_mode_y0), row_w, row_h),
fetch: (row_x, at(fetch_y0), row_w, row_h),
powerline: (row_x, at(fetch_y0 + ROW_H), row_w, row_h),
ghost: (row_x, at(ghost_y0), row_w, row_h),
@@ -1135,6 +1185,9 @@ fn dropdown_anchor(
(Section::Profiles, SettingsDropdown::Accept) => {
Some((anchor(geometry.accept), ACCEPT_OPTIONS.len()))
},
(Section::Profiles, SettingsDropdown::CellWidthMode) => {
Some((anchor(geometry.cell_width_mode), CELL_WIDTH_MODE_OPTIONS.len()))
},
(Section::Interaction, SettingsDropdown::TabReveal) => {
Some((anchor(geometry.tab_reveal), TAB_REVEAL_OPTIONS.len()))
},
@@ -1226,6 +1279,7 @@ pub fn settings_hit(
SettingsDropdown::Language => SettingsHit::Language(LANGUAGE_OPTIONS[index]),
SettingsDropdown::Accept => SettingsHit::AcceptOption(index),
SettingsDropdown::TabReveal => SettingsHit::TabRevealOption(index),
SettingsDropdown::CellWidthMode => SettingsHit::CellWidthModeOption(index),
SettingsDropdown::CursorShape => SettingsHit::CursorShapeOption(index),
// 背景色浮层在上方特判处理,走不到通用行列表。
SettingsDropdown::BackgroundColor => SettingsHit::Panel,
@@ -1321,6 +1375,9 @@ pub fn settings_hit(
return SettingsHit::FontSizeDown;
}
}
if contains_rect(geometry.cell_width_mode, x, y) {
return SettingsHit::CellWidthModeDropdown;
}
if contains_rect(geometry.fetch, x, y) {
return SettingsHit::FetchToggle;
}
@@ -1451,6 +1508,7 @@ pub(super) struct SettingsView {
pub(super) panel_resize: bool,
pub(super) cjk_bold_regular: bool,
pub(super) tab_reveal: TabRevealMotion,
pub(super) cell_width_mode: CellWidthMode,
/// Live-preview colors: the ACTUAL terminal background/foreground the
/// grid would use right now (custom background wins over the theme).
pub(super) preview_bg: Rgb,
@@ -1617,6 +1675,9 @@ fn dropdown_selected_index(view: &SettingsView, dropdown: SettingsDropdown) -> O
SettingsDropdown::TabReveal => {
TAB_REVEAL_OPTIONS.iter().position(|motion| *motion == view.tab_reveal)
},
SettingsDropdown::CellWidthMode => {
CELL_WIDTH_MODE_OPTIONS.iter().position(|mode| *mode == view.cell_width_mode)
},
SettingsDropdown::CursorShape => {
CURSOR_SHAPE_OPTIONS.iter().position(|shape| *shape == view.cursor_shape)
},
@@ -1637,6 +1698,7 @@ fn dropdown_hover_index(hover: SettingsHit, dropdown: SettingsDropdown) -> Optio
},
(SettingsDropdown::Accept, SettingsHit::AcceptOption(index)) => Some(index),
(SettingsDropdown::TabReveal, SettingsHit::TabRevealOption(index)) => Some(index),
(SettingsDropdown::CellWidthMode, SettingsHit::CellWidthModeOption(index)) => Some(index),
(SettingsDropdown::CursorShape, SettingsHit::CursorShapeOption(index)) => Some(index),
_ => None,
}
@@ -2172,6 +2234,18 @@ pub(super) fn push_quads(
view.hover == SettingsHit::FontSizeUp,
view.hover == SettingsHit::FontSizeDown,
);
row_hover(
quads,
geometry.cell_width_mode,
view.hover == SettingsHit::CellWidthModeDropdown,
);
combobox(
quads,
&mut staged,
geometry.cell_width_mode,
view.hover == SettingsHit::CellWidthModeDropdown,
view.dropdown == Some(SettingsDropdown::CellWidthMode),
);
for quad in staged.drain(..) {
clip(quads, quad);
}
@@ -2604,6 +2678,9 @@ pub(super) fn draw_popup_text(
SettingsDropdown::TabReveal => {
tab_reveal_label(TAB_REVEAL_OPTIONS[index], language).to_owned()
},
SettingsDropdown::CellWidthMode => {
cell_width_mode_label(CELL_WIDTH_MODE_OPTIONS[index], language).to_owned()
},
SettingsDropdown::CursorShape => {
cursor_shape_label(CURSOR_SHAPE_OPTIONS[index], language).to_owned()
},
@@ -3329,6 +3406,26 @@ pub(super) fn draw_text(
gc,
);
}
if visible(geometry.cell_width_mode.1, geometry.cell_width_mode.3) {
row_label(
r,
gc,
size,
scale,
&sk,
geometry.cell_width_mode,
language.pick("字体间距", "Font spacing"),
"",
sk.ink,
);
combobox_value(
r,
gc,
geometry.cell_width_mode,
cell_width_mode_label(view.cell_width_mode, language),
sk.accent,
);
}
// Boolean rows: the switch (drawn in `push_quads`) carries the
// state; no "On/Off" string next to it.
if visible(geometry.fetch.1, geometry.fetch.3) {
@@ -3829,7 +3926,8 @@ pub(super) fn draw_text(
#[cfg(test)]
mod tests {
use super::{
SHOW_WEBDAV_SYNC_SETTINGS, TabRevealMotion, advanced_content_end, opacity_from_pointer,
CELL_WIDTH_MODE_OPTIONS, CellWidthMode, SHOW_WEBDAV_SYNC_SETTINGS, TabRevealMotion,
UiLanguage, advanced_content_end, cell_width_mode_label, opacity_from_pointer,
};
#[test]
@@ -3860,4 +3958,27 @@ mod tests {
assert_eq!(TabRevealMotion::parse(value.settings_value()), Some(value));
}
}
#[test]
fn cell_width_mode_defaults_compatibly_and_round_trips() {
assert_eq!(CellWidthMode::default(), CellWidthMode::Compact);
assert_eq!(CellWidthMode::parse("compact"), Some(CellWidthMode::Compact));
assert_eq!(CellWidthMode::parse("RELAXED"), Some(CellWidthMode::Relaxed));
assert_eq!(CellWidthMode::parse("unknown").unwrap_or_default(), CellWidthMode::Compact);
for value in [CellWidthMode::Compact, CellWidthMode::Relaxed] {
assert_eq!(CellWidthMode::parse(value.settings_value()), Some(value));
}
}
#[test]
fn cell_width_mode_dropdown_offers_both_choices_with_the_compatible_one_first() {
// 列表顺序即下拉顺序,也是持久化索引的来源;兼容默认必须排第一。
assert_eq!(CELL_WIDTH_MODE_OPTIONS.len(), 2);
assert_eq!(CELL_WIDTH_MODE_OPTIONS[0], CellWidthMode::default());
assert_eq!(CELL_WIDTH_MODE_OPTIONS[1], CellWidthMode::Relaxed);
for option in CELL_WIDTH_MODE_OPTIONS {
assert!(!cell_width_mode_label(option, UiLanguage::ZhCn).is_empty());
assert!(!cell_width_mode_label(option, UiLanguage::EnUs).is_empty());
}
}
}
+15
View File
@@ -862,6 +862,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
self.ctx.mark_dirty();
return;
},
crate::display::SettingsHit::CellWidthModeDropdown => {
self.ctx.display().toggle_settings_dropdown(
crate::display::SettingsDropdown::CellWidthMode,
);
self.ctx.mark_dirty();
return;
},
crate::display::SettingsHit::CellWidthModeOption(index) => {
let base_font = self.ctx.config().font.clone();
self.ctx.display().set_cell_width_mode_option(index, &base_font);
self.ctx.mark_dirty();
return;
},
crate::display::SettingsHit::FontSizeUp => {
let ui_scale = self.ctx.display().window.scale_factor as f32;
self.ctx.change_font_size(ui_scale);
@@ -1292,6 +1305,8 @@ fn settings_dropdown_keeps_open(hit: crate::display::SettingsHit) -> bool {
| Hit::AcceptOption(_)
| Hit::TabRevealDropdown
| Hit::TabRevealOption(_)
| Hit::CellWidthModeDropdown
| Hit::CellWidthModeOption(_)
| Hit::CursorShapeDropdown
| Hit::CursorShapeOption(_)
| Hit::BackgroundColor
+2
View File
@@ -331,6 +331,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
| crate::display::SettingsHit::KeymapRow(_)
| crate::display::SettingsHit::TabRevealDropdown
| crate::display::SettingsHit::TabRevealOption(_)
| crate::display::SettingsHit::CellWidthModeDropdown
| crate::display::SettingsHit::CellWidthModeOption(_)
| crate::display::SettingsHit::FontSizeUp
| crate::display::SettingsHit::FontSizeDown
| crate::display::SettingsHit::BackgroundImageCoverChrome