From b53cd580f6cd57534c51f4456de76fdc22153083 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Fri, 4 Sep 2026 15:38:59 +0100 Subject: [PATCH] feat(ui): shared dropdown popup width calculation with wide-character support - Add dropdown_popup_width helper in common.rs replacing magic formula heuristics - Implement is_wide range table to count East Asian Wide/Fullwidth (CJK) glyphs double for localized strings - Calibrate unit scale factor to 0.68 * font_size preserving existing width behavior at font size 11 - Apply shared calculation across viewport controls and ribbon dropdowns - Update unit tests to verify width calculation on standard and CJK labels --- src/app/view/controls.rs | 32 +++++++++++++++++++++++++------- src/ui/ribbon/mod.rs | 16 ++++++++++++++-- src/ui/style/common.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index a98b3e4a4..2f333e099 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -131,12 +131,13 @@ pub(super) fn viewport_controls<'a>( ); let preview_mode = render_mode_preview.unwrap_or(render_mode); - let max_label_len = render_modes - .iter() - .map(|c| c.to_string().chars().count()) - .max() - .unwrap_or(0); - let dropdown_w = (max_label_len as f32 * 7.5 + 46.0).max(180.0); + let mode_names: Vec = render_modes.iter().map(|c| c.to_string()).collect(); + let dropdown_w = crate::ui::style::common::dropdown_popup_width( + mode_names.iter().map(|s| s.as_str()), + 11.0, + 46.0, + 180.0, + ); let mut choices = column![].spacing(2).width(Length::Fill); for choice in render_modes { @@ -445,13 +446,30 @@ mod tests { .map(|c| c.label.chars().count()) .max() .unwrap_or(0); - let dropdown_w = (max_label_len as f32 * 7.5 + 46.0).max(180.0); + let dropdown_w = crate::ui::style::common::dropdown_popup_width( + crate::modules::view::visual_style::VISUAL_STYLES + .iter() + .map(|c| c.label), + 11.0, + 46.0, + 180.0, + ); // Longest default label is "Gouraud Shaded + Edges" (22 chars) assert_eq!(max_label_len, 22); // Computed width should easily fit the 22-char label + checkmark + padding assert!(dropdown_w >= 210.0); + // CJK labels count wide characters double + let cjk_labels = ["二维线框", "着色+边"]; + let cjk_w = crate::ui::style::common::dropdown_popup_width( + cjk_labels.iter().copied(), + 11.0, + 46.0, + 180.0, + ); + assert_eq!(cjk_w, 180.0); + for style in crate::modules::view::visual_style::VISUAL_STYLES { assert!(!style.label.contains('\n'), "Label should be a single line"); } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 39d7444d3..d4a61b697 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -853,6 +853,7 @@ impl Ribbon { let label_el = text(t!(*label)) .size(11) + .wrapping(iced::advanced::text::Wrapping::None) .style(move |theme: &Theme| iced::widget::text::Style { color: (!is_current).then_some( theme @@ -880,11 +881,22 @@ impl Ribbon { }) .collect(); + let labels: Vec = items + .iter() + .map(|(_, label, _)| t!(*label).to_string()) + .collect(); + let panel_w = crate::ui::style::common::dropdown_popup_width( + labels.iter().map(|s| s.as_str()), + 11.0, + 68.0, + 190.0, + ); + let panel = container(column(rows)) .style(popup_panel_style) - .width(Length::Fixed(190.0)); + .width(Length::Fixed(panel_w)); - let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.0); + let (align_right, h_pad, top) = self.dd_anchor(open_id, panel_w, win.0); Some(dropdown_backdrop(position_ribbon_dropdown( panel.into(), align_right, diff --git a/src/ui/style/common.rs b/src/ui/style/common.rs index 8a8bca239..d5d57da63 100644 --- a/src/ui/style/common.rs +++ b/src/ui/style/common.rs @@ -73,3 +73,33 @@ pub(crate) fn accessible_accent(accent: Color, bg: Color, fallback: Color) -> Co pub(crate) fn canvas_is_light(bg: [f32; 4]) -> bool { wcag_luminance(Color::from_rgb(bg[0], bg[1], bg[2])) > 0.5 } + +/// True if a character is East Asian Wide or Fullwidth (CJK ideographs, kana, hangul, fullwidth forms). +pub(crate) fn is_wide(c: char) -> bool { + let u = c as u32; + matches!( + u, + 0x1100..=0x115F // Hangul Jamo + | 0x2E80..=0xA4CF // CJK Radicals, Kangxi, Hiragana, Katakana, Bopomofo, Hangul Compatibility, CJK Unified Ideographs, Yi + | 0xAC00..=0xD7A3 // Hangul Syllables + | 0xF900..=0xFAFF // CJK Compatibility Ideographs + | 0xFE30..=0xFE4F // CJK Compatibility Forms + | 0xFF01..=0xFF60 // Fullwidth ASCII variants + | 0xFFE0..=0xFFE6 // Fullwidth symbol variants + | 0x20000..=0x2FA1F // CJK Unified Ideographs Extension B-F, Compatibility Supplement + ) +} + +/// Estimated popup width for a vertical list of labels. Wide (CJK) glyphs +/// count double so localized labels via t!() don't truncate. +pub fn dropdown_popup_width<'a>( + labels: impl Iterator, + font_size: f32, + chrome: f32, + min: f32, +) -> f32 { + let max_units = labels + .map(|l| l.chars().map(|c| if is_wide(c) { 2.0 } else { 1.0 }).sum::()) + .fold(0.0f32, f32::max); + (max_units * font_size * 0.68 + chrome).max(min) +}