From c1e1c06297f8bf033900becbd01e237a3cd2a1c0 Mon Sep 17 00:00:00 2001 From: Dmitrii Khlapov Date: Mon, 12 Oct 2026 12:05:30 +0200 Subject: [PATCH] #659: one drop-down marker (the dropdown icon) on every ribbon button (#697) Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/ui-egui/src/equation_tab.rs | 5 ++- crates/ui-egui/src/ribbon.rs | 8 ++--- crates/ui-egui/src/widgets.rs | 52 ++++++++++++++++++++++++------ docs/ui-parity.md | 3 +- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/crates/ui-egui/src/equation_tab.rs b/crates/ui-egui/src/equation_tab.rs index 0720afe..840a50e 100644 --- a/crates/ui-egui/src/equation_tab.rs +++ b/crates/ui-egui/src/equation_tab.rs @@ -62,8 +62,7 @@ fn math_menu_button(ui: &mut Ui, app: &mut WordApp, picture: &str, label: &str, let lines: Vec<&str> = label.split('\n').collect(); let mut y = r.min.y + 43.0; for (i, l) in lines.iter().enumerate() { - let txt = if i + 1 == lines.len() { format!("{l} ▾") } else { (*l).to_string() }; - ui.painter().text(pos2(r.center().x, y), Align2::CENTER_TOP, txt, regular(11.5), t.text); + crate::widgets::label_line(ui, r.center().x, y, l, regular(11.5), t.text, i + 1 == lines.len()); y += 13.0; } let resp = resp.on_hover_text(label.replace('\n', " ")); @@ -163,7 +162,7 @@ pub fn insert_button(ui: &mut Ui, app: &mut WordApp) { } } crate::icons::paint(ui.painter(), Rect::from_center_size(pos2(r.center().x, r.min.y + 20.0), vec2(32.0, 32.0)), "equation", t.icon, t.accent); - ui.painter().text(pos2(r.center().x, r.min.y + 43.0), Align2::CENTER_TOP, format!("{label} ▾"), regular(11.5), t.text); + crate::widgets::label_line(ui, r.center().x, r.min.y + 43.0, label, regular(11.5), t.text, true); if rt.on_hover_text(tl!("Insert a new equation (Alt+=)")).clicked() { let _ = app.run("insert.equation", json!({})); app.canvas.want_focus = true; diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 43bfc45..1b9ca64 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -172,12 +172,8 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { } ui.add_space(6.0); let track = app.session.doc.settings.track_changes; - let mode = crate::widgets::icon_text_button( - ui, - "pencil", - &format!("{} ▾", if track { tl!("Reviewing") } else { tl!("Editing") }), - regular(12.0), - ); + let mode = + crate::widgets::icon_text_menu_button(ui, "pencil", if track { tl!("Reviewing") } else { tl!("Editing") }, regular(12.0)); egui::Popup::menu(&mode).show(|ui| { if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() { let _ = app.run("review.trackChanges", json!({"value": false})); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 58954d4..8881adc 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -125,6 +125,24 @@ pub fn big_toggle(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: & big_button(ui, app, icon, label, id, Value::Object(Default::default()), false, checked) } +/// Size of the drop-down marker drawn after a label or beside a split button (#659: one marker, +/// the `dropdown` icon, everywhere; no text glyph). +pub const DROPDOWN: f32 = 10.0; + +/// One centred label line (top at `y`); `marker` draws the drop-down icon after the text, in the +/// text's colour, and the pair is centred together. +pub fn label_line(ui: &Ui, cx: f32, y: f32, text: &str, font: egui::FontId, color: Color32, marker: bool) { + let galley = ui.painter().layout_no_wrap(text.to_string(), font, color); + let extra = if marker { DROPDOWN + 3.0 } else { 0.0 }; + let size = galley.size(); + let x = cx - (size.x + extra) / 2.0; + ui.painter().galley(pos2(x, y), galley, color); + if marker { + let c = pos2(x + size.x + 3.0 + DROPDOWN / 2.0, y + size.y / 2.0); + icons::paint(ui.painter(), Rect::from_center_size(c, vec2(DROPDOWN, DROPDOWN)), "dropdown", color, color); + } +} + #[allow(clippy::too_many_arguments)] fn big_button(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, params: Value, menu: bool, checked: bool) -> Response { let label = tl!(label); @@ -142,8 +160,7 @@ fn big_button(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, let mut y = r.min.y + 43.0; let lines: Vec<&str> = label.split('\n').collect(); for (i, l) in lines.iter().enumerate() { - let txt = if menu && i + 1 == lines.len() { format!("{l} ▾") } else { (*l).to_string() }; - ui.painter().text(pos2(r.center().x, y), Align2::CENTER_TOP, txt, regular(11.5), if on { t.text } else { t.text_disabled }); + label_line(ui, r.center().x, y, l, regular(11.5), if on { t.text } else { t.text_disabled }, menu && i + 1 == lines.len()); y += 13.0; } let resp = tooltip(app, resp, &label.replace('\n', " "), id); @@ -202,7 +219,7 @@ pub fn split( if let Some(sw) = swatch { ui.painter().rect_filled(Rect::from_min_max(pos2(r.min.x + 4.0, r.max.y - 5.0), pos2(r.max.x - 4.0, r.max.y - 2.0)), 0.0, sw); } - icons::paint(ui.painter(), Rect::from_center_size(ar.center(), vec2(10.0, 10.0)), "dropdown", t.icon, t.accent); + icons::paint(ui.painter(), Rect::from_center_size(ar.center(), vec2(DROPDOWN, DROPDOWN)), "dropdown", t.icon, t.accent); let resp = tooltip(app, resp, tip, id); command_menu(&resp, app, id); if resp.clicked() { @@ -214,7 +231,7 @@ pub fn split( }); } -/// A button that only opens a menu (icon + label + ▾). +/// A button that only opens a menu (icon + label + drop-down marker). pub fn menu_button( ui: &mut Ui, app: &mut WordApp, @@ -240,8 +257,7 @@ pub fn menu_button( let lines: Vec<&str> = label.unwrap_or("").split('\n').collect(); let mut y = r.min.y + 43.0; for (i, l) in lines.iter().enumerate() { - let txt = if i + 1 == lines.len() { format!("{l} ▾") } else { (*l).to_string() }; - ui.painter().text(pos2(r.center().x, y), Align2::CENTER_TOP, txt, regular(11.5), t.text); + label_line(ui, r.center().x, y, l, regular(11.5), t.text, i + 1 == lines.len()); y += 13.0; } resp @@ -254,7 +270,7 @@ pub fn menu_button( if let Some(l) = label { ui.painter().text(pos2(r.min.x + 24.0, r.center().y), Align2::LEFT_CENTER, l, regular(11.5), t.text); } - icons::paint(ui.painter(), Rect::from_center_size(pos2(r.max.x - 6.0, r.center().y), vec2(9.0, 9.0)), "dropdown", t.icon, t.accent); + icons::paint(ui.painter(), Rect::from_center_size(pos2(r.max.x - 6.0, r.center().y), vec2(DROPDOWN, DROPDOWN)), "dropdown", t.icon, t.accent); resp }; let resp = resp.on_hover_text(tip); @@ -575,7 +591,7 @@ fn combo_impl( } let (ar, aresp) = ui.allocate_exact_size(vec2(16.0, resp.rect.height()), Sense::click()); ui.painter().rect(ar, 0.0, if aresp.hovered() { t.hover } else { t.input }, Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside); - icons::paint(ui.painter(), Rect::from_center_size(ar.center(), vec2(10.0, 10.0)), "dropdown", t.icon, t.accent); + icons::paint(ui.painter(), Rect::from_center_size(ar.center(), vec2(DROPDOWN, DROPDOWN)), "dropdown", t.icon, t.accent); // The list drops from the box's left edge, as in Word, not from the arrow (#535). egui::Popup::menu(&aresp).anchor(resp.rect.union(ar)).show(|ui| { if let Some(v) = combo_list(ui, width, current, items, preview) { @@ -753,11 +769,21 @@ pub fn icon_button(ui: &mut Ui, icon: &str, tip: &str) -> Response { /// fonts and would draw as empty boxes, so icons are painted (`icons.rs`) rather than typed. /// It looks and reacts like `ui.button` (see [`drawn_state`]). pub fn icon_text_button(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId) -> Response { + icon_text_button_with(ui, icon, text, font, false) +} + +/// [`icon_text_button`] that opens a menu: the drop-down marker follows the text. +pub fn icon_text_menu_button(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId) -> Response { + icon_text_button_with(ui, icon, text, font, true) +} + +fn icon_text_button_with(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId, marker: bool) -> Response { let t = Tokens::get(ui.ctx()); let galley = ui.painter().layout_no_wrap(text.to_string(), font, Color32::PLACEHOLDER); let pad = ui.spacing().button_padding; let icon_w = 16.0; - let size = vec2(pad.x + icon_w + 4.0 + galley.size().x + pad.x, galley.size().y.max(icon_w) + 2.0 * pad.y); + let marker_w = if marker { DROPDOWN + 4.0 } else { 0.0 }; + let size = vec2(pad.x + icon_w + 4.0 + galley.size().x + marker_w + pad.x, galley.size().y.max(icon_w) + 2.0 * pad.y); let (r, resp) = ui.allocate_exact_size(size, Sense::click()); let st = drawn_state(ui, &resp, true); let r = r.expand(st.expansion); @@ -769,7 +795,13 @@ pub fn icon_text_button(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId) t.icon, t.accent, ); - ui.painter().galley(pos2(r.min.x + pad.x + icon_w + 4.0, r.center().y - galley.size().y / 2.0), galley, st.text); + let text_x = r.min.x + pad.x + icon_w + 4.0; + let text_w = galley.size().x; + ui.painter().galley(pos2(text_x, r.center().y - galley.size().y / 2.0), galley, st.text); + if marker { + let c = pos2(text_x + text_w + 4.0 + DROPDOWN / 2.0, r.center().y); + icons::paint(ui.painter(), Rect::from_center_size(c, vec2(DROPDOWN, DROPDOWN)), "dropdown", st.text, st.text); + } resp } diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 49b6ffd..5166937 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (gallery previews dimmed in the dark theme, #663; previously trivial: hard-coded interface colours moved to the theme, #656; previously minor: status bar language follows the caret and sets the proofing language; Review › Language is a menu; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (one drop-down marker, #659; previously minor: status bar language follows the caret and sets the proofing language; Review › Language is a menu; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch. Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our @@ -69,6 +69,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | 2026-10-11 | trivial | Text field and combo outlines reach 3:1 against their fill and surroundings in both themes (light #8A8A8A, dark #7A7A7A); a test pins the ratios (#657) | | 2026-10-11 | trivial | Font dialog: Asian text font and Latin text font boxes, each with its own list and type-to-find, shown for a Chinese/Japanese interface or East Asian text in the selection; OK sets only the fonts changed; `format.font {"eastAsia"}` (#505) | | 2026-10-11 | trivial | References › Table of Authorities: Insert Table of Authorities is labelled and joined by Update Table (`references.updateAuthorities`), as in the Index group (#661) | +| 2026-10-11 | trivial | One drop-down marker everywhere: large ribbon buttons, Equation and the Editing/Reviewing button draw the `dropdown` icon after their label, like split and menu buttons, instead of a text glyph (#659) | | 2026-10-11 | trivial | Dark theme: Styles, style-set and table-style previews sit on a dimmer page tone; with View › Switch Modes on, the Styles gallery previews the dark page (#663) | | 2026-10-11 | trivial | Dialog primary buttons, the Discord pill, the table eraser line and the Navigation search highlight take their colours from the theme in both themes (`widgets::primary_button`); a test bans the old fixed values (#656) | | 2026-10-11 | trivial | Dark theme: Share, the avatar initials, the AutoSave knob and keytips use dark `on_accent` text on the light accent (6.2:1, 8.4:1 on hover; white was 2.9:1) (#655) |