From 71aa24c22b7a4cff849602cb561f7bfb68a2d375 Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 22:14:10 -0800 Subject: [PATCH] feat(ui-egui): font boxes on the floating toolbar The mini toolbar over the page has the same font name and font size boxes as the ribbon. The ribbon stores the current values and lists in egui memory each frame, and the page menu reads them. A test was written first. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/floating.rs | 66 ++++++++++++++++++++++++++++++++-- crates/ui-egui/src/pagemenu.rs | 3 +- crates/ui-egui/src/ribbon.rs | 8 +++++ crates/ui-egui/tests/ui.rs | 8 +++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/floating.rs b/crates/ui-egui/src/floating.rs index 80942dd2..ddc3e273 100644 --- a/crates/ui-egui/src/floating.rs +++ b/crates/ui-egui/src/floating.rs @@ -106,9 +106,25 @@ pub struct BarButton { pub act: Act, } +/// The font name and font size boxes shown on a mini toolbar, as on the ribbon. The ribbon keeps +/// these up to date in egui memory under [`FONT_BOXES_KEY`], and the page menu reads them. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FontBoxes { + pub family: String, + pub size: String, + pub families: Vec, + pub families_note: Option, + pub sizes: Vec, +} + +/// The egui memory key under which the ribbon stores the [`FontBoxes`] for this frame. +pub const FONT_BOXES_KEY: &str = "notecraft-font-boxes"; + /// A mini toolbar: rows of buttons. Plain data, so any place can build its own. #[derive(Clone, Debug, Default, PartialEq)] pub struct Bar { + /// The font boxes, drawn in a row above the buttons, when there are any. + pub fonts: Option, pub rows: Vec>, } @@ -124,6 +140,7 @@ pub fn text_bar() -> Bar { let m = Glyph::Mark; let i = Glyph::Icon; Bar { + fonts: None, rows: vec![ vec![ button(m(Mark::Bold), "Bold", "Bold (Ctrl+B)", Act::Mark("bold")), @@ -145,6 +162,11 @@ pub fn text_bar() -> Bar { } } +/// The mini toolbar with the font boxes on top, as on the ribbon. +pub fn text_bar_with_fonts(fonts: FontBoxes) -> Bar { + Bar { fonts: Some(fonts), ..text_bar() } +} + const BTN: f32 = 26.0; /// What drawing a bar did. @@ -159,7 +181,7 @@ pub struct BarOut { /// above, and pushes the act of a pressed button into `acts`. `id` must differ between bars that /// can be open at once. pub fn show_bar(ctx: &Context, id: Id, anchor: Rect, bar: &Bar, t: &Tokens, acts: &mut Vec) -> BarOut { - let rows = bar.rows.len() as f32; + let rows = bar.rows.len() as f32 + if bar.fonts.is_some() { 1.0 } else { 0.0 }; let room_above = anchor.min.y - ctx.content_rect().min.y; let want = rows * (BTN + 2.0) + 14.0; let (pivot, at) = if room_above >= want + 4.0 { @@ -171,6 +193,46 @@ pub fn show_bar(ctx: &Context, id: Id, anchor: Rect, bar: &Bar, t: &Tokens, acts let area = Area::new(id).order(Order::Foreground).pivot(pivot).fixed_pos(at).constrain(true).show(ctx, |ui| { Frame::popup(ui.style()).inner_margin(egui::Margin::same(4)).show(ui, |ui| { ui.spacing_mut().item_spacing = vec2(2.0, 2.0); + if let Some(fonts) = &bar.fonts { + ui.horizontal(|ui| { + let families = crate::combo::show( + ui, + t.text, + crate::combo::Spec { + id: id.with("font-family"), + box_label: crate::tl_id!("ui.font_family_box"), + arrow_label: crate::tl_id!("ui.font_family"), + shown: &fonts.family, + width: 96.0, + items: &fonts.families, + empty_note: fonts.families_note.as_deref(), + accept: &|typed| notecraft_formats::markdown::valid_font_family(typed), + }, + ); + if let Some(name) = families.chosen { + acts.push(Act::FontFamily(name)); + picked = true; + } + let size = crate::combo::show( + ui, + t.text, + crate::combo::Spec { + id: id.with("font-size"), + box_label: crate::tl_id!("ui.font_size_box"), + arrow_label: crate::tl_id!("ui.font_size"), + shown: &fonts.size, + width: 38.0, + items: &fonts.sizes, + empty_note: None, + accept: &|typed| typed.parse::().is_ok_and(|size| crate::ribbon::FONT_SIZES.contains(&size)), + }, + ); + if let Some(chosen) = size.chosen.and_then(|typed| typed.parse::().ok()) { + acts.push(Act::FontSize(chosen)); + picked = true; + } + }); + } for row in &bar.rows { ui.horizontal(|ui| { for b in row { @@ -224,7 +286,7 @@ mod tests { #[test] fn a_bar_can_be_built_from_data_and_drawn_anywhere() { let t = Theme::DEFAULT.tokens(); - let bar = Bar { rows: vec![vec![button(Glyph::Mark(Mark::Cut), "Cut", "Cut", Act::Clipboard("cut"))]] }; + let bar = Bar { fonts: None, rows: vec![vec![button(Glyph::Mark(Mark::Cut), "Cut", "Cut", Act::Clipboard("cut"))]] }; let ctx = Context::default(); let mut acts = Vec::new(); let mut out = None; diff --git a/crates/ui-egui/src/pagemenu.rs b/crates/ui-egui/src/pagemenu.rs index 5506517b..bc961c8c 100644 --- a/crates/ui-egui/src/pagemenu.rs +++ b/crates/ui-egui/src/pagemenu.rs @@ -56,7 +56,8 @@ pub fn show(ui: &mut Ui, body: egui::Rect, acts: &mut Vec, t: &Tokens) { }) .response; // The mini toolbar floats over the menu, as it does in OneNote. - let bar = floating::show_bar(ui.ctx(), id().with("bar"), area.rect, &floating::text_bar(), t, acts); + let fonts = ui.ctx().data(|d| d.get_temp::(egui::Id::new(floating::FONT_BOXES_KEY))).unwrap_or_default(); + let bar = floating::show_bar(ui.ctx(), id().with("bar"), area.rect, &floating::text_bar_with_fonts(fonts), t, acts); picked |= bar.picked; let away = |p: egui::Pos2| !area.rect.contains(p) && !bar.rect.contains(p); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 5aae0e6e..b29934fb 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -910,6 +910,14 @@ fn home_tab(ui: &mut Ui, view: &View<'_>, t: &Tokens, acts: &mut Vec) { accept: &|typed| typed.parse::().is_ok_and(|size| FONT_SIZES.contains(&size)), }, ); + let boxes = crate::floating::FontBoxes { + family: view.font_family.unwrap_or("").to_owned(), + size: shown.to_owned(), + families: families.clone(), + families_note: families_note.clone(), + sizes: sizes.clone(), + }; + ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new(crate::floating::FONT_BOXES_KEY), boxes)); if let Some(size) = size_out.chosen.and_then(|typed| typed.parse::().ok()) { acts.push(Act::FontSize(size)); } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 9f815e54..a24be1c1 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -5129,6 +5129,14 @@ fn right_click_on_selected_words_keeps_them_selected_and_shows_the_mini_toolbar( } } +/// The floating toolbar has the same font name and font size boxes as the ribbon (owner, 2026-11-12). +#[test] +fn the_floating_toolbar_has_the_font_boxes() { + let (_t, h) = right_click_on_selected_words("ui-floating-fonts"); + assert!(h.query_all_by_label("Font size").count() >= 2, "the size box is on the ribbon and on the floating toolbar"); + assert!(h.query_all_by_label("Font name").count() >= 2, "the font name box is on the ribbon and on the floating toolbar"); +} + /// A button on the toolbar acts on the words that were selected when the menu was opened. #[test] fn bold_on_the_mini_toolbar_wraps_the_selected_words() {