| 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,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 | } |
| 360 | 364 | ||
| 361 | impl Toggle { | 365 | impl 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::ALL | 716 | 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 | } |
| 759 | 782 | ||
| ... | @@ -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<_> = labels | 2362 | 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 | } |
| 1546 | 1546 | ||
| 1547 | #[test] | 1547 | #[test] |
| 1548 | fn nothing_matching_leaves_nothing_to_choose() { | 1548 | fn 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 | } |
| 1558 | 1561 | ||
| 1559 | #[test] | 1562 | #[test] |