From 4a1acb6c3f2e27fa26e6f0be5aef1fed60dcf184 Mon Sep 17 00:00:00 2001 From: Dmitrii Khlapov Date: Mon, 12 Oct 2026 12:13:17 +0200 Subject: [PATCH] Ribbon: large-button labels end above the group titles (#653) (#699) * #659: one drop-down marker (the dropdown icon) on every ribbon button Co-Authored-By: Claude Opus 5.5 * #658: menu buttons grey out while their command is disabled Co-Authored-By: Claude Opus 5.5 * #653: large-button labels end above the group title row Co-Authored-By: Claude Opus 5.5 --------- 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 | 22 ++++++++++------ crates/ui-egui/src/widgets.rs | 40 +++++++++++++++++++++++------- docs/ui-parity.md | 3 ++- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/ui-egui/src/equation_tab.rs b/crates/ui-egui/src/equation_tab.rs index 0367e90..4a01475 100644 --- a/crates/ui-egui/src/equation_tab.rs +++ b/crates/ui-egui/src/equation_tab.rs @@ -11,7 +11,7 @@ use wordcraft_engine::math_gallery::{BUILT_INS, STRUCTURES, SYMBOL_SETS, Templat use crate::WordApp; use crate::theme::{Tokens, regular, semibold}; -use crate::widgets::{CONTENT_H, group, small}; +use crate::widgets::{BIG_ICON_Y, BIG_LABEL_Y, CONTENT_H, LABEL_PITCH, group, small}; /// What each structure button shows. fn button_picture(id: &str) -> &'static str { @@ -56,14 +56,14 @@ fn math_menu_button(ui: &mut Ui, app: &mut WordApp, picture: &str, label: &str, if resp.hovered() { ui.painter().rect_filled(r, 4.0, t.hover); } - let pic = Rect::from_center_size(pos2(r.center().x, r.min.y + 20.0), vec2(40.0, 34.0)); + let pic = Rect::from_center_size(pos2(r.center().x, r.min.y + BIG_ICON_Y), vec2(40.0, 32.0)); ui.painter().rect_filled(pic, 3.0, egui::Color32::WHITE); paint_math(app, ui, pic, &format!("btn:{picture}"), picture, None, false, 12.0); let lines: Vec<&str> = label.split('\n').collect(); - let mut y = r.min.y + 43.0; + let mut y = r.min.y + BIG_LABEL_Y; for (i, l) in lines.iter().enumerate() { crate::widgets::label_line(ui, r.center().x, y, l, regular(11.5), t.text, i + 1 == lines.len()); - y += 13.0; + y += LABEL_PITCH; } let resp = resp.on_hover_text(label.replace('\n', " ")); egui::Popup::menu(&resp).show(|ui| { @@ -152,8 +152,8 @@ pub fn insert_button(ui: &mut Ui, app: &mut WordApp) { let label = tl!("Equation"); let w = (ui.ctx().fonts_mut(|f| f.layout_no_wrap(label.to_string(), regular(11.5), t.text).size().x) + 22.0).max(48.0); let (r, _) = ui.allocate_exact_size(vec2(w, CONTENT_H), Sense::hover()); - let top = Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + 40.0)); - let bottom = Rect::from_min_max(pos2(r.min.x, r.min.y + 40.0), r.max); + let top = Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + BIG_LABEL_Y - 2.0)); + let bottom = Rect::from_min_max(pos2(r.min.x, r.min.y + BIG_LABEL_Y - 2.0), r.max); let rt = ui.interact(top, ui.id().with("eq_insert_top"), Sense::click()); let rb = ui.interact(bottom, ui.id().with("eq_insert_bottom"), Sense::click()); for (rr, resp) in [(top, &rt), (bottom, &rb)] { @@ -161,8 +161,14 @@ pub fn insert_button(ui: &mut Ui, app: &mut WordApp) { ui.painter().rect_filled(rr, 4.0, t.hover); } } - 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); - crate::widgets::label_line(ui, r.center().x, r.min.y + 43.0, label, regular(11.5), t.text, true); + crate::icons::paint( + ui.painter(), + Rect::from_center_size(pos2(r.center().x, r.min.y + BIG_ICON_Y), vec2(32.0, 32.0)), + "equation", + t.icon, + t.accent, + ); + crate::widgets::label_line(ui, r.center().x, r.min.y + BIG_LABEL_Y, 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/widgets.rs b/crates/ui-egui/src/widgets.rs index 1046465..3c3ff0a 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -8,7 +8,17 @@ use crate::{WordApp, icons}; /// Height of the ribbon content area (without group labels). pub const CONTENT_H: f32 = 66.0; -pub const LABEL_H: f32 = 16.0; +/// Height of the group title row under the content. +pub const LABEL_H: f32 = 18.0; +/// Large buttons: centre of the 32 px icon, top of the first label line, and the pitch and +/// height of a label line (`regular(11.5)`; a test checks the font's real line height). +pub const BIG_ICON_Y: f32 = 18.0; +pub const BIG_LABEL_Y: f32 = 36.0; +pub const LABEL_PITCH: f32 = 13.0; +pub const LABEL_LINE_H: f32 = 14.0; +// #653: a two-line label ends inside the content, above the group title row. +const _: () = assert!(BIG_LABEL_Y + LABEL_PITCH + LABEL_LINE_H <= CONTENT_H); +const _: () = assert!(BIG_ICON_Y + 16.0 <= BIG_LABEL_Y); /// Shortcut text for a command (`⌘B` on macOS, `Ctrl+B` elsewhere): its first key in effect, /// custom keys included (Customize Keyboard). @@ -154,14 +164,14 @@ fn big_button(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, keytip_badge(ui, r, id, &label.replace('\n', " ")); let on = enabled(app, id); bg(ui, r, &resp, checked, &t); - let ic = Rect::from_center_size(pos2(r.center().x, r.min.y + 20.0), vec2(32.0, 32.0)); + let ic = Rect::from_center_size(pos2(r.center().x, r.min.y + BIG_ICON_Y), vec2(32.0, 32.0)); let (c, a) = if on { (t.icon, t.accent) } else { (t.text_disabled, t.text_disabled) }; icons::paint(ui.painter(), ic, icon, c, a); - let mut y = r.min.y + 43.0; + let mut y = r.min.y + BIG_LABEL_Y; let lines: Vec<&str> = label.split('\n').collect(); for (i, l) in lines.iter().enumerate() { 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; + y += LABEL_PITCH; } let resp = tooltip(app, resp, &label.replace('\n', " "), id); command_menu(&resp, app, id); @@ -263,12 +273,12 @@ pub fn menu_button( let (r, resp) = ui.allocate_exact_size(vec2(w, CONTENT_H), Sense::click()); keytip_badge(ui, r, "menu", tip); bg(ui, r, &resp, false, &t); - icons::paint(ui.painter(), Rect::from_center_size(pos2(r.center().x, r.min.y + 20.0), vec2(32.0, 32.0)), icon, c, a); + icons::paint(ui.painter(), Rect::from_center_size(pos2(r.center().x, r.min.y + BIG_ICON_Y), vec2(32.0, 32.0)), icon, c, a); let lines: Vec<&str> = label.unwrap_or("").split('\n').collect(); - let mut y = r.min.y + 43.0; + let mut y = r.min.y + BIG_LABEL_Y; for (i, l) in lines.iter().enumerate() { label_line(ui, r.center().x, y, l, regular(11.5), text, i + 1 == lines.len()); - y += 13.0; + y += LABEL_PITCH; } resp } else { @@ -313,9 +323,10 @@ pub fn group_titled(ui: &mut Ui, id_title: &str, title: &str, launcher: Option<& }); let r = inner.response.rect; let r = Rect::from_min_max(start, pos2(r.max.x, start.y + CONTENT_H + LABEL_H)); - ui.painter().text(pos2(r.center().x, r.max.y - 8.0), Align2::CENTER_CENTER, title, regular(11.0), t.group_label); + let title_y = r.max.y - LABEL_H / 2.0; + ui.painter().text(pos2(r.center().x, title_y), Align2::CENTER_CENTER, title, regular(11.0), t.group_label); if let Some(cmd) = launcher { - let lr = Rect::from_center_size(pos2(r.max.x - 6.0, r.max.y - 8.0), vec2(11.0, 11.0)); + let lr = Rect::from_center_size(pos2(r.max.x - 6.0, title_y), vec2(11.0, 11.0)); let resp = ui.interact(lr, ui.id().with(("launch", id_title)), Sense::click()); if resp.hovered() { ui.painter().rect_filled(lr.expand(2.0), 2.0, t.hover); @@ -822,6 +833,17 @@ fn icon_text_button_with(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId mod tests { use super::*; + /// #653: a large button's label line is no taller than `LABEL_LINE_H` in the interface font, so + /// two lines (checked at compile time) end above the group title row. + #[test] + fn big_button_label_lines_fit_their_slot() { + let ctx = egui::Context::default(); + ctx.set_fonts(crate::theme::font_definitions(false, false)); + ctx.run_ui(egui::RawInput::default(), |_| {}).drop_without_applying_deltas(); + let h = ctx.fonts_mut(|f| f.layout_no_wrap("Page Number Mail Merge gjpqy".into(), crate::theme::regular(11.5), Color32::WHITE).size().y); + assert!(h <= LABEL_LINE_H, "label line is {h} px tall"); + } + #[test] fn drawn_buttons_fade_in_on_hover_and_darken_while_pressed() { // #305's drawn replacements for ✕, 💬, ✎ and ➕ must react like egui's buttons. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index b7b39a2..eb48300 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 (menu buttons grey out, #658; previously 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 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (ribbon labels clear the group titles, #653; previously trivial: menu buttons grey out, #658; previously 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 | Two-line labels on large ribbon buttons end above the group title row (icon and label moved up, title row 18 px; shared constants with a compile-time check) (#653) | | 2026-10-11 | trivial | Menu buttons (Header, Page Number, Breaks, Wrap Text, table Select/Delete/AutoFit…) grey out and don't open while the command their menu is about is disabled, like the other ribbon buttons (#658) | | 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) |