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 {...@@ -356,6 +356,10 @@ pub struct FormatState {
356 pub tags: Vec<(NoteTag, u16)>,356 pub tags: Vec<(NoteTag, u16)>,
357 /// The stored name of the paragraph style every selected paragraph shares.357 /// The stored name of the paragraph style every selected paragraph shares.
358 pub style: Option<String>,358 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>,
359}363}
360364
361impl Toggle {365impl Toggle {
...@@ -703,6 +707,11 @@ impl CanvasEditor {...@@ -703,6 +707,11 @@ impl CanvasEditor {
703 .iter()707 .iter()
704 .all(|node| self.list(node).as_ref() == Some(&kind))708 .all(|node| self.list(node).as_ref() == Some(&kind))
705 };709 };
710 let styled = |node: &&PageParagraph| {
711 (node.style.as_ref())
712 .and_then(|style| self.definitions.get(style))
713 .map(|definition| &definition.format)
714 };
706 Ok(FormatState {715 Ok(FormatState {
707 toggles: Toggle::ALL716 toggles: Toggle::ALL
708 .into_iter()717 .into_iter()
...@@ -754,6 +763,20 @@ impl CanvasEditor {...@@ -754,6 +763,20 @@ impl CanvasEditor {
754 _ => None,763 _ => None,
755 }764 }
756 })),765 })),
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 })),
757 })780 })
758 }781 }
759782
...@@ -2665,6 +2688,8 @@ mod tests {...@@ -2665,6 +2688,8 @@ mod tests {
2665 (NoteTag::defaults()[2].clone(), 2)2688 (NoteTag::defaults()[2].clone(), 2)
2666 ],2689 ],
2667 style: state.style.clone(),2690 style: state.style.clone(),
2691 style_font: state.style_font.clone(),
2692 style_size: state.style_size,
2668 }2693 }
2669 );2694 );
2670 // Format Painter, a gallery tag and an inserted table write as well.2695 // 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 {...@@ -2331,6 +2331,11 @@ impl State {
2331 current: *name == Some(font.as_str()),2331 current: *name == Some(font.as_str()),
2332 heading: name.is_none(),2332 heading: name.is_none(),
2333 separated: *separated,2333 separated: *separated,
2334 shortcut: if name.is_some() && *name == state.style_font.as_deref() {
2335 "Default"
2336 } else {
2337 ""
2338 },
2334 ..Default::default()2339 ..Default::default()
2335 })2340 })
2336 .collect();2341 .collect();
...@@ -2344,26 +2349,45 @@ impl State {...@@ -2344,26 +2349,45 @@ impl State {
2344 let combo = ui.id("size");2349 let combo = ui.id("size");
2345 let menu = toolbar_popup("size");2350 let menu = toolbar_popup("size");
2346 ui::shell::combo(ui, "size", "Font Size", &size, 44.0, menu, fonts_apply);2351 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.2352 // A number typed offers that size alone, cut to the half point below, or else the
2348 let mut sizes = SIZES.to_vec();2353 // range OneNote 2010 takes, as the lab's OneNote 2010 does.
2349 if let Some(typed) = ui::popup::query(ui, toolbar_popup("size"))2354 let typed = ui::popup::query(ui, toolbar_popup("size")).map(str::trim);
2350 .and_then(|query| query.trim().parse::<f32>().ok())2355 let number = typed.and_then(|typed| typed.parse::<f32>().ok());
2351 .map(|typed| (typed * 2.0).round() / 2.0)2356 let sizes = match number.map(|typed| (typed * 2.0).floor() / 2.0) {
2352 .filter(|typed| onestore::FONT_SIZES.contains(typed) && !sizes.contains(typed))2357 Some(typed) if onestore::FONT_SIZES.contains(&typed) => vec![typed],
2353 {2358 Some(_) => Vec::new(),
2354 let at = sizes.partition_point(|size| *size < typed);2359 None => SIZES.to_vec(),
2355 sizes.insert(at, typed);2360 };
2356 }
2357 let labels: Vec<_> = sizes.iter().map(|size| format!("{size}")).collect();2361 let labels: Vec<_> = sizes.iter().map(|size| format!("{size}")).collect();
2358 let items: Vec<_> = labels2362 let mut items: Vec<_> = labels
2359 .iter()2363 .iter()
2360 .map(|label| ui::popup::Item {2364 .zip(&sizes)
2365 .map(|(label, points)| ui::popup::Item {
2361 text: label,2366 text: label,
2362 checked: Some(*label == size),2367 checked: Some(*label == size),
2363 current: *label == size,2368 current: *label == size,
2369 shortcut: if state.style_size == Some(*points) {
2370 "Default"
2371 } else {
2372 ""
2373 },
2374 fallback: number.is_some(),
2364 ..Default::default()2375 ..Default::default()
2365 })2376 })
2366 .collect();2377 .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 }
2367 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());2391 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
2368 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))2392 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))
2369 {2393 {
crates/ui/src/popup.rs+4
...@@ -579,6 +579,10 @@ fn choose(...@@ -579,6 +579,10 @@ fn choose(
579 return Some((mode, index));579 return Some((mode, index));
580 }580 }
581 state(ui, id).submenu = Some((matches.key(row), Some(ui.now)));581 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;
582 }586 }
583 let popup = state(ui, id);587 let popup = state(ui, id);
584 popup.query = query;588 popup.query = query;
crates/ui/src/tests.rs+5-2
...@@ -1545,15 +1545,18 @@ fn typing_filters_to_the_best_matches_first() {...@@ -1545,15 +1545,18 @@ fn typing_filters_to_the_best_matches_first() {
1545}1545}
15461546
1547#[test]1547#[test]
1548fn nothing_matching_leaves_nothing_to_choose() {1548fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
1549 let mut ui = open_menu(Some("Filter"));1549 let mut ui = open_menu(Some("Filter"));
1550 ui.event(typed("z"));1550 ui.event(typed("z"));
1551 menu_frame(&mut ui, BELOW, Some("Filter"));1551 menu_frame(&mut ui, BELOW, Some("Filter"));
1552 ui.event(key(NamedKey::ArrowDown));1552 ui.event(key(NamedKey::ArrowDown));
1553 ui.event(key(NamedKey::Enter));
1554 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));1553 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));
1555 assert_eq!(chosen, None);1554 assert_eq!(chosen, None);
1556 assert!(ui.popup_open(menu_id()));1555 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()));
1557}1560}
15581561
1559#[test]1562#[test]