From 4bcc5c87d175b449931f56c4bccea5b32665418e Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 13 Oct 2026 20:09:54 +1100 Subject: [PATCH] Ribbon: Font Size box matches the Font name box, list drops under it The size box is one frame with the Font name box's fill, border, height and corners, with the text field and the dropdown arrow inside it (focus shows the selection stroke). The size list drops directly below the box and is as wide as it, instead of opening 220 pt wide under the arrow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ut3hr3a13szpQdQ7gbYbfc --- crates/ui-egui/src/ribbon.rs | 46 +++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 99b3b01e2..d2d5fe101 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -393,13 +393,14 @@ pub(crate) fn editable_combo( items: &[(&str, String)], forced: bool, ) -> ComboResult { - combo_popup(ui, anchor, salt, current_value, items, forced, true) + combo_popup(ui, anchor, salt, current_value, items, forced, true, None) } /// The dropdown of [`editable_combo`] without the filter field: the whole list, picked by click or -/// arrows and Enter. -fn list_combo(ui: &mut Ui, anchor: &egui::Response, salt: &'static str, current_value: &str, items: &[(&str, String)]) -> ComboResult { - combo_popup(ui, anchor, salt, current_value, items, false, false) +/// arrows and Enter. It drops directly below `under` and is as wide as it, like Excel's Font Size +/// list under its box. +fn list_combo(ui: &mut Ui, anchor: &egui::Response, under: Rect, salt: &'static str, current_value: &str, items: &[(&str, String)]) -> ComboResult { + combo_popup(ui, anchor, salt, current_value, items, false, false, Some(under)) } fn combo_popup( @@ -410,6 +411,7 @@ fn combo_popup( items: &[(&str, String)], forced: bool, filter: bool, + under: Option, ) -> ComboResult { let popup_id = anchor.id.with("popup"); let state_id = anchor.id.with(salt).with("__combo_state"); @@ -424,7 +426,12 @@ fn combo_popup( } st.was_open = open_now; let mut result = ComboResult::None; - egui::Popup::menu(anchor).open_memory(popup_command(forced, anchor.clicked())).show(|ui| { + let mut popup = egui::Popup::menu(anchor).open_memory(popup_command(forced, anchor.clicked())); + if let Some(r) = under { + popup = popup.anchor(r).align(egui::RectAlign::BOTTOM_START).width(r.width()); + } + let row_w = under.map_or(220.0, |r| r.width() - 12.0); + popup.show(|ui| { // Arrows and Enter navigate the list, not the filter field's cursor. Capture them and // strip them from the input so the TextEdit never sees them. let (mut down, mut up, mut enter) = (false, false, false); @@ -474,7 +481,7 @@ fn combo_popup( egui::ScrollArea::vertical().max_height(240.0).show(ui, |ui| { for i in &filtered { let (value, disp) = &items[*i]; - let row = ui.add(egui::Button::selectable(*i as i32 == st.sel, egui::RichText::new(disp)).min_size(vec2(220.0, 20.0))); + let row = ui.add(egui::Button::selectable(*i as i32 == st.sel, egui::RichText::new(disp)).min_size(vec2(row_w, 20.0))); if row.clicked() { result = ComboResult::Picked(value.to_string()); } @@ -1114,8 +1121,21 @@ fn size_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let field = egui::Id::new("gridcraft.font_size_box"); let draft = field.with("draft"); let mut text = ui.data_mut(|d| d.get_temp::(draft)).unwrap_or_else(|| current.clone()); - let gap = std::mem::replace(&mut ui.spacing_mut().item_spacing.x, 0.0); - let out = egui::TextEdit::singleline(&mut text).id(field).font(theme::ui_font(12.5)).desired_width(30.0).show(ui); + // One frame like the Font name box beside it (same fill, border, height and corners), with the + // text field and the dropdown arrow inside it, as in Excel. + let (frame, _) = ui.allocate_exact_size(vec2(52.0, 24.0), egui::Sense::hover()); + // Painted once the field's focus and hover are known, but beneath the field and the arrow. + let bg = ui.painter().add(egui::Shape::Noop); + let text_rect = Rect::from_min_max(frame.min, egui::pos2(frame.right() - 16.0, frame.bottom())); + let arrow_rect = Rect::from_min_max(egui::pos2(frame.right() - 16.0, frame.top() + 1.0), egui::pos2(frame.right() - 1.0, frame.bottom() - 1.0)); + let mut field_ui = + ui.new_child(egui::UiBuilder::new().max_rect(text_rect.shrink2(vec2(6.0, 0.0))).layout(egui::Layout::left_to_right(egui::Align::Center))); + let out = egui::TextEdit::singleline(&mut text) + .id(field) + .font(theme::ui_font(12.5)) + .frame(egui::Frame::NONE) + .desired_width(text_rect.width() - 12.0) + .show(&mut field_ui); let resp = out.response; control_keytip(app, ui, &resp, "home.fontSize"); if invoke(app, "home.fontSize") { @@ -1140,13 +1160,17 @@ fn size_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { } else { ui.data_mut(|d| d.remove::(draft)); } - let arrow = icon_button(ui, Icon::Chevron, t.text_dim, "Font Size", vec2(14.0, 24.0)); - ui.spacing_mut().item_spacing.x = gap; + let mut arrow_ui = ui.new_child(egui::UiBuilder::new().max_rect(arrow_rect)); + let arrow = icon_button(&mut arrow_ui, Icon::Chevron, t.text_dim, "Font Size", arrow_rect.size()); + let visuals = ui.style().visuals.clone(); + let looks = if resp.has_focus() || ui.rect_contains_pointer(frame) { &visuals.widgets.hovered } else { &visuals.widgets.inactive }; + let stroke = if resp.has_focus() { visuals.selection.stroke } else { looks.bg_stroke }; + ui.painter().set(bg, egui::epaint::RectShape::new(frame, looks.corner_radius, looks.weak_bg_fill, stroke, egui::StrokeKind::Inside)); arrow.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Font Size")); let sizes = [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0]; let labels: Vec = sizes.iter().map(|s| format!("{}", *s as i32)).collect(); let items: Vec<(&str, String)> = labels.iter().map(|s| (s.as_str(), s.clone())).collect(); - if let ComboResult::Picked(value) = list_combo(ui, &arrow, "size", ¤t, &items) + if let ComboResult::Picked(value) = list_combo(ui, &arrow, frame, "size", ¤t, &items) && let Ok(v) = value.parse::() { act(app, "home.fontSize", json!({"size": v}));