authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 22:00:19-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 01:50:33-07:00
log73b387c0343ab799e3abdfc49b0cd564043c57c0
tree9eb4f5e2440b320d0012ffd903dece5a6e32b85f
parent464da86b45523af24588c5558b41fff36ed753b2
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: the font size box takes any size typed and tags the default size and font

Clover: show which size is the default and allow typing any number. A number typed in Font Size offers that size alone, cut to the half point below, or else says the range OneNote 2010 takes (6 to 130, as its own box refuses others in the lab); Enter on a menu offering nothing for what was typed leaves the size as it was. The size and font the paragraph's style gives, or else the Default font's, read Default in the lists. Assisted-by: claude-opus-5.5

4 files changed, 70 insertions(+), 14 deletions(-)

crates/canvas/src/editor/format.rs+25
......@@ -356,6 +356,10 @@ pub struct FormatState {
356356 pub tags: Vec<(NoteTag, u16)>,
357357 /// The stored name of the paragraph style every selected paragraph shares.
358358 pub style: Option<String>,
359 /// The font and size the selected paragraphs' style gives, or else the Default font,
360 /// where they share one: what their text is in without formatting of its own.
361 pub style_font: Option<String>,
362 pub style_size: Option<f32>,
359363}
360364
361365impl Toggle {
......@@ -703,6 +707,11 @@ impl CanvasEditor {
703707 .iter()
704708 .all(|node| self.list(node).as_ref() == Some(&kind))
705709 };
710 let styled = |node: &&PageParagraph| {
711 (node.style.as_ref())
712 .and_then(|style| self.definitions.get(style))
713 .map(|definition| &definition.format)
714 };
706715 Ok(FormatState {
707716 toggles: Toggle::ALL
708717 .into_iter()
......@@ -754,6 +763,20 @@ impl CanvasEditor {
754763 _ => None,
755764 }
756765 })),
766 style_font: common(paragraphs.iter().map(|node| {
767 Some(
768 styled(node)
769 .and_then(|format| format.font.clone())
770 .unwrap_or_else(|| self.default_font.face.clone()),
771 )
772 })),
773 style_size: common(paragraphs.iter().map(|node| {
774 Some(
775 styled(node)
776 .and_then(|format| format.font_size)
777 .unwrap_or(self.default_font.size),
778 )
779 })),
757780 })
758781 }
759782
......@@ -2665,6 +2688,8 @@ mod tests {
26652688 (NoteTag::defaults()[2].clone(), 2)
26662689 ],
26672690 style: state.style.clone(),
2691 style_font: state.style_font.clone(),
2692 style_size: state.style_size,
26682693 }
26692694 );
26702695 // Format Painter, a gallery tag and an inserted table write as well.
crates/snowbound/src/main.rs+36-12
......@@ -2331,6 +2331,11 @@ impl State {
23312331 current: *name == Some(font.as_str()),
23322332 heading: name.is_none(),
23332333 separated: *separated,
2334 shortcut: if name.is_some() && *name == state.style_font.as_deref() {
2335 "Default"
2336 } else {
2337 ""
2338 },
23342339 ..Default::default()
23352340 })
23362341 .collect();
......@@ -2344,26 +2349,45 @@ impl State {
23442349 let combo = ui.id("size");
23452350 let menu = toolbar_popup("size");
23462351 ui::shell::combo(ui, "size", "Font Size", &size, 44.0, menu, fonts_apply);
2347 // A size typed in the field joins the list, in half points as stored.
2348 let mut sizes = SIZES.to_vec();
2349 if let Some(typed) = ui::popup::query(ui, toolbar_popup("size"))
2350 .and_then(|query| query.trim().parse::<f32>().ok())
2351 .map(|typed| (typed * 2.0).round() / 2.0)
2352 .filter(|typed| onestore::FONT_SIZES.contains(typed) && !sizes.contains(typed))
2353 {
2354 let at = sizes.partition_point(|size| *size < typed);
2355 sizes.insert(at, typed);
2356 }
2352 // A number typed offers that size alone, cut to the half point below, or else the
2353 // range OneNote 2010 takes, as the lab's OneNote 2010 does.
2354 let typed = ui::popup::query(ui, toolbar_popup("size")).map(str::trim);
2355 let number = typed.and_then(|typed| typed.parse::<f32>().ok());
2356 let sizes = match number.map(|typed| (typed * 2.0).floor() / 2.0) {
2357 Some(typed) if onestore::FONT_SIZES.contains(&typed) => vec![typed],
2358 Some(_) => Vec::new(),
2359 None => SIZES.to_vec(),
2360 };
23572361 let labels: Vec<_> = sizes.iter().map(|size| format!("{size}")).collect();
2358 let items: Vec<_> = labels
2362 let mut items: Vec<_> = labels
23592363 .iter()
2360 .map(|label| ui::popup::Item {
2364 .zip(&sizes)
2365 .map(|(label, points)| ui::popup::Item {
23612366 text: label,
23622367 checked: Some(*label == size),
23632368 current: *label == size,
2369 shortcut: if state.style_size == Some(*points) {
2370 "Default"
2371 } else {
2372 ""
2373 },
2374 fallback: number.is_some(),
23642375 ..Default::default()
23652376 })
23662377 .collect();
2378 let range = format!(
2379 "Type a size from {} to {}",
2380 onestore::FONT_SIZES.start(),
2381 onestore::FONT_SIZES.end()
2382 );
2383 if items.is_empty() || typed.is_some_and(|typed| !typed.is_empty()) && number.is_none() {
2384 items.push(ui::popup::Item {
2385 text: &range,
2386 disabled: true,
2387 fallback: true,
2388 ..Default::default()
2389 });
2390 }
23672391 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
23682392 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))
23692393 {
crates/ui/src/popup.rs+4
......@@ -579,6 +579,10 @@ fn choose(
579579 return Some((mode, index));
580580 }
581581 state(ui, id).submenu = Some((matches.key(row), Some(ui.now)));
582 } else if role == Role::Menu && !typed.is_empty() && keys.contains(&NamedKey::Enter) {
583 // Enter on a menu offering nothing for what was typed leaves things as they were.
584 ui.close_popup(id);
585 return None;
582586 }
583587 let popup = state(ui, id);
584588 popup.query = query;
crates/ui/src/tests.rs+5-2
......@@ -1545,15 +1545,18 @@ fn typing_filters_to_the_best_matches_first() {
15451545}
15461546
15471547#[test]
1548fn nothing_matching_leaves_nothing_to_choose() {
1548fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
15491549 let mut ui = open_menu(Some("Filter"));
15501550 ui.event(typed("z"));
15511551 menu_frame(&mut ui, BELOW, Some("Filter"));
15521552 ui.event(key(NamedKey::ArrowDown));
1553 ui.event(key(NamedKey::Enter));
15541553 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));
15551554 assert_eq!(chosen, None);
15561555 assert!(ui.popup_open(menu_id()));
1556 ui.event(key(NamedKey::Enter));
1557 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));
1558 assert_eq!(chosen, None);
1559 assert!(!ui.popup_open(menu_id()));
15571560}
15581561
15591562#[test]