feat: configurable terminal cell width mode
新增全局「字体间距」偏好:紧凑(上游兼容默认)或宽松。 终端把字体的非整数平均 advance 转成整像素列宽时,上游一律向下取整。同样的 字体与字号下,向下取整会少一像素。宽松改用最接近整数取整补上 这一像素,紧凑保持现有取整,因此现有列数与换行行为不变。 该偏好只控制终端内容网格的列宽:单元格高度在两种模式下逐位相同,字体家族、 字号、字重、字形比例与回退顺序均不受影响。`refresh_ui_font` 计算的 Nebula 原生界面字体单元格固定按向下取整,不随该偏好变化。 切换时重推当前字体,让既有的字体更新路径重算单元格,网格、viewport、pane、 渲染器与 PTY 一致更新,无需重启。
This commit is contained in:
1 parent
f52b9a428c
commit
894c6e87c6
4 files changed
+259
-15
No files matched your search
+118
-12
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user