diff --git a/crates/canvas/src/editor/format.rs b/crates/canvas/src/editor/format.rs index 2b3d917895cadb7914a8aab82dc8c834084decb7..a3754e6f87c8e9efa45d11c422078b2cd89000be 100644 --- a/crates/canvas/src/editor/format.rs +++ b/crates/canvas/src/editor/format.rs @@ -356,6 +356,10 @@ pub struct FormatState { pub tags: Vec<(NoteTag, u16)>, /// The stored name of the paragraph style every selected paragraph shares. pub style: Option, + /// The font and size the selected paragraphs' style gives, or else the Default font, + /// where they share one: what their text is in without formatting of its own. + pub style_font: Option, + pub style_size: Option, } impl Toggle { @@ -703,6 +707,11 @@ impl CanvasEditor { .iter() .all(|node| self.list(node).as_ref() == Some(&kind)) }; + let styled = |node: &&PageParagraph| { + (node.style.as_ref()) + .and_then(|style| self.definitions.get(style)) + .map(|definition| &definition.format) + }; Ok(FormatState { toggles: Toggle::ALL .into_iter() @@ -754,6 +763,20 @@ impl CanvasEditor { _ => None, } })), + style_font: common(paragraphs.iter().map(|node| { + Some( + styled(node) + .and_then(|format| format.font.clone()) + .unwrap_or_else(|| self.default_font.face.clone()), + ) + })), + style_size: common(paragraphs.iter().map(|node| { + Some( + styled(node) + .and_then(|format| format.font_size) + .unwrap_or(self.default_font.size), + ) + })), }) } @@ -2665,6 +2688,8 @@ mod tests { (NoteTag::defaults()[2].clone(), 2) ], style: state.style.clone(), + style_font: state.style_font.clone(), + style_size: state.style_size, } ); // Format Painter, a gallery tag and an inserted table write as well. diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 7d8de1ba75ebea9f3bbdfaf9c56c0ee4716ad693..e625cc549a14b556fe701d5ee97368bbef18e445 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -2331,6 +2331,11 @@ impl State { current: *name == Some(font.as_str()), heading: name.is_none(), separated: *separated, + shortcut: if name.is_some() && *name == state.style_font.as_deref() { + "Default" + } else { + "" + }, ..Default::default() }) .collect(); @@ -2344,26 +2349,45 @@ impl State { let combo = ui.id("size"); let menu = toolbar_popup("size"); ui::shell::combo(ui, "size", "Font Size", &size, 44.0, menu, fonts_apply); - // A size typed in the field joins the list, in half points as stored. - let mut sizes = SIZES.to_vec(); - if let Some(typed) = ui::popup::query(ui, toolbar_popup("size")) - .and_then(|query| query.trim().parse::().ok()) - .map(|typed| (typed * 2.0).round() / 2.0) - .filter(|typed| onestore::FONT_SIZES.contains(typed) && !sizes.contains(typed)) - { - let at = sizes.partition_point(|size| *size < typed); - sizes.insert(at, typed); - } + // A number typed offers that size alone, cut to the half point below, or else the + // range OneNote 2010 takes, as the lab's OneNote 2010 does. + let typed = ui::popup::query(ui, toolbar_popup("size")).map(str::trim); + let number = typed.and_then(|typed| typed.parse::().ok()); + let sizes = match number.map(|typed| (typed * 2.0).floor() / 2.0) { + Some(typed) if onestore::FONT_SIZES.contains(&typed) => vec![typed], + Some(_) => Vec::new(), + None => SIZES.to_vec(), + }; let labels: Vec<_> = sizes.iter().map(|size| format!("{size}")).collect(); - let items: Vec<_> = labels + let mut items: Vec<_> = labels .iter() - .map(|label| ui::popup::Item { + .zip(&sizes) + .map(|(label, points)| ui::popup::Item { text: label, checked: Some(*label == size), current: *label == size, + shortcut: if state.style_size == Some(*points) { + "Default" + } else { + "" + }, + fallback: number.is_some(), ..Default::default() }) .collect(); + let range = format!( + "Type a size from {} to {}", + onestore::FONT_SIZES.start(), + onestore::FONT_SIZES.end() + ); + if items.is_empty() || typed.is_some_and(|typed| !typed.is_empty()) && number.is_none() { + items.push(ui::popup::Item { + text: &range, + disabled: true, + fallback: true, + ..Default::default() + }); + } let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size)) { diff --git a/crates/ui/src/popup.rs b/crates/ui/src/popup.rs index 5761bb746725c2847967ff3cccd20dfd9b38038b..b34f0e8ace9a93e4de8e325a1220bb3819d9dc0a 100644 --- a/crates/ui/src/popup.rs +++ b/crates/ui/src/popup.rs @@ -579,6 +579,10 @@ fn choose( return Some((mode, index)); } state(ui, id).submenu = Some((matches.key(row), Some(ui.now))); + } else if role == Role::Menu && !typed.is_empty() && keys.contains(&NamedKey::Enter) { + // Enter on a menu offering nothing for what was typed leaves things as they were. + ui.close_popup(id); + return None; } let popup = state(ui, id); popup.query = query; diff --git a/crates/ui/src/tests.rs b/crates/ui/src/tests.rs index c6fe77fc019438f20938cf220ed57ad705efc26b..e5420a71ddd52cfe711b1d50a5fcf9ed5a58491f 100644 --- a/crates/ui/src/tests.rs +++ b/crates/ui/src/tests.rs @@ -1545,15 +1545,18 @@ fn typing_filters_to_the_best_matches_first() { } #[test] -fn nothing_matching_leaves_nothing_to_choose() { +fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() { let mut ui = open_menu(Some("Filter")); ui.event(typed("z")); menu_frame(&mut ui, BELOW, Some("Filter")); ui.event(key(NamedKey::ArrowDown)); - ui.event(key(NamedKey::Enter)); let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter")); assert_eq!(chosen, None); assert!(ui.popup_open(menu_id())); + ui.event(key(NamedKey::Enter)); + let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter")); + assert_eq!(chosen, None); + assert!(!ui.popup_open(menu_id())); } #[test]