| author | |
| committer | |
| log | 73b387c0343ab799e3abdfc49b0cd564043c57c0 |
| tree | 9eb4f5e2440b320d0012ffd903dece5a6e32b85f |
| parent | 464da86b45523af24588c5558b41fff36ed753b2 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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.54 files changed, 70 insertions(+), 14 deletions(-)
crates/canvas/src/editor/format.rs+25| ... | ... | @@ -356,6 +356,10 @@ pub struct FormatState { |
| 356 | 356 | pub tags: Vec<(NoteTag, u16)>, |
| 357 | 357 | /// The stored name of the paragraph style every selected paragraph shares. |
| 358 | 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 | } |
| 360 | 364 | |
| 361 | 365 | impl Toggle { |
| ... | ... | @@ -703,6 +707,11 @@ impl CanvasEditor { |
| 703 | 707 | .iter() |
| 704 | 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 | 715 | Ok(FormatState { |
| 707 | 716 | toggles: Toggle::ALL |
| 708 | 717 | .into_iter() |
| ... | ... | @@ -754,6 +763,20 @@ impl CanvasEditor { |
| 754 | 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 | } |
| 759 | 782 | |
| ... | ... | @@ -2665,6 +2688,8 @@ mod tests { |
| 2665 | 2688 | (NoteTag::defaults()[2].clone(), 2) |
| 2666 | 2689 | ], |
| 2667 | 2690 | style: state.style.clone(), |
| 2691 | style_font: state.style_font.clone(), | |
| 2692 | style_size: state.style_size, | |
| 2668 | 2693 | } |
| 2669 | 2694 | ); |
| 2670 | 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 | 2331 | current: *name == Some(font.as_str()), |
| 2332 | 2332 | heading: name.is_none(), |
| 2333 | 2333 | separated: *separated, |
| 2334 | shortcut: if name.is_some() && *name == state.style_font.as_deref() { | |
| 2335 | "Default" | |
| 2336 | } else { | |
| 2337 | "" | |
| 2338 | }, | |
| 2334 | 2339 | ..Default::default() |
| 2335 | 2340 | }) |
| 2336 | 2341 | .collect(); |
| ... | ... | @@ -2344,26 +2349,45 @@ impl State { |
| 2344 | 2349 | let combo = ui.id("size"); |
| 2345 | 2350 | let menu = toolbar_popup("size"); |
| 2346 | 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. | |
| 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 | }; | |
| 2357 | 2361 | let labels: Vec<_> = sizes.iter().map(|size| format!("{size}")).collect(); |
| 2358 | let items: Vec<_> = labels | |
| 2362 | let mut items: Vec<_> = labels | |
| 2359 | 2363 | .iter() |
| 2360 | .map(|label| ui::popup::Item { | |
| 2364 | .zip(&sizes) | |
| 2365 | .map(|(label, points)| ui::popup::Item { | |
| 2361 | 2366 | text: label, |
| 2362 | 2367 | checked: Some(*label == size), |
| 2363 | 2368 | current: *label == size, |
| 2369 | shortcut: if state.style_size == Some(*points) { | |
| 2370 | "Default" | |
| 2371 | } else { | |
| 2372 | "" | |
| 2373 | }, | |
| 2374 | fallback: number.is_some(), | |
| 2364 | 2375 | ..Default::default() |
| 2365 | 2376 | }) |
| 2366 | 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 | 2391 | let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); |
| 2368 | 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 | 579 | return Some((mode, index)); |
| 580 | 580 | } |
| 581 | 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 | 587 | let popup = state(ui, id); |
| 584 | 588 | popup.query = query; |
crates/ui/src/tests.rs+5-2| ... | ... | @@ -1545,15 +1545,18 @@ fn typing_filters_to_the_best_matches_first() { |
| 1545 | 1545 | } |
| 1546 | 1546 | |
| 1547 | 1547 | #[test] |
| 1548 | fn nothing_matching_leaves_nothing_to_choose() { | |
| 1548 | fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() { | |
| 1549 | 1549 | let mut ui = open_menu(Some("Filter")); |
| 1550 | 1550 | ui.event(typed("z")); |
| 1551 | 1551 | menu_frame(&mut ui, BELOW, Some("Filter")); |
| 1552 | 1552 | ui.event(key(NamedKey::ArrowDown)); |
| 1553 | ui.event(key(NamedKey::Enter)); | |
| 1554 | 1553 | let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter")); |
| 1555 | 1554 | assert_eq!(chosen, None); |
| 1556 | 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 | } |
| 1558 | 1561 | |
| 1559 | 1562 | #[test] |