| author | |
| committer | |
| log | 11bf37c8a7d470d535338524dbe2c81bbcff29bb |
| tree | 627101d55e619a14342bf7f7ea74cd1a2d4190c0 |
| parent | 71e4e308b60bf1a52ad839646150a0f9e324ecee |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Select All widens from the paragraph to its outline as OneNote does, driven
from the same editor step as the desktop.
Assisted-by: claude-opus-5.56 files changed, 82 insertions(+), 10 deletions(-)
apps/ios/Snowbound/CanvasView.swift+2-1| ... | @@ -606,7 +606,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -606,7 +606,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 606 | } | 606 | } |
| 607 | 607 | ||
| 608 | override func selectAll(_ sender: Any?) { | 608 | override func selectAll(_ sender: Any?) { |
| 609 | edit(external: true) { choose(Range(0, endOfDocument.offset)) } | 609 | guard let handle else { return } |
| 610 | edit(external: true) { sb_select_more(handle) } | ||
| 610 | } | 611 | } |
| 611 | 612 | ||
| 612 | override func toggleBoldface(_ sender: Any?) { apply(0) } | 613 | override func toggleBoldface(_ sender: Any?) { apply(0) } |
apps/ios/Snowbound/Script.swift+3-1| ... | @@ -9,7 +9,8 @@ extension CanvasView { | ... | @@ -9,7 +9,8 @@ extension CanvasView { |
| 9 | /// `tap:120,300|type:hi|mark:かな|unmark|return|delete|select:2,9|scroll:0,600|zoom:1.5|shot:a`. | 9 | /// `tap:120,300|type:hi|mark:かな|unmark|return|delete|select:2,9|scroll:0,600|zoom:1.5|shot:a`. |
| 10 | /// `tap` and `doubletap` take points from the view's corner, `select` text offsets, | 10 | /// `tap` and `doubletap` take points from the view's corner, `select` text offsets, |
| 11 | /// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word` | 11 | /// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word` |
| 12 | /// selects a match, `title` edits the title, `picture` inserts a drawn picture; `done` | 12 | /// selects a match, `title` edits the title, `picture` inserts a drawn picture, `selectall` |
| 13 | /// is the Select All command; `done` | ||
| 13 | /// ends editing, `tree` saves the view hierarchy to | 14 | /// ends editing, `tree` saves the view hierarchy to |
| 14 | /// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no | 15 | /// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no |
| 15 | /// screenshot command. | 16 | /// screenshot command. |
| ... | @@ -54,6 +55,7 @@ extension CanvasView { | ... | @@ -54,6 +55,7 @@ extension CanvasView { |
| 54 | let values = argument.split(separator: ",").compactMap { Double($0) } | 55 | let values = argument.split(separator: ",").compactMap { Double($0) } |
| 55 | zoom(at: CGPoint(x: values[0] + contentOffset.x, y: values[1] + contentOffset.y)) | 56 | zoom(at: CGPoint(x: values[0] + contentOffset.x, y: values[1] + contentOffset.y)) |
| 56 | case "done": _ = resignFirstResponder() | 57 | case "done": _ = resignFirstResponder() |
| 58 | case "selectall": selectAll(nil) | ||
| 57 | case "format": apply(UInt8(argument) ?? 0) | 59 | case "format": apply(UInt8(argument) ?? 0) |
| 58 | case "find": _ = find(argument) | 60 | case "find": _ = find(argument) |
| 59 | case "title": _ = focusTitle() | 61 | case "title": _ = focusTitle() |
crates/mobile/include/snowbound.h+1| ... | @@ -72,6 +72,7 @@ uint32_t sb_text_length(const View *view); | ... | @@ -72,6 +72,7 @@ uint32_t sb_text_length(const View *view); |
| 72 | char *sb_text(const View *view, uint32_t start, uint32_t end); | 72 | char *sb_text(const View *view, uint32_t start, uint32_t end); |
| 73 | void sb_selection(const View *view, uint32_t range[2]); | 73 | void sb_selection(const View *view, uint32_t range[2]); |
| 74 | bool sb_select(View *view, uint32_t start, uint32_t end); | 74 | bool sb_select(View *view, uint32_t start, uint32_t end); |
| 75 | bool sb_select_more(View *view); | ||
| 75 | bool sb_marked(const View *view, uint32_t range[2]); | 76 | bool sb_marked(const View *view, uint32_t range[2]); |
| 76 | bool sb_set_marked(View *view, const char *text, uint32_t selected_start, uint32_t selected_end); | 77 | bool sb_set_marked(View *view, const char *text, uint32_t selected_start, uint32_t selected_end); |
| 77 | void sb_unmark(View *view); | 78 | void sb_unmark(View *view); |
crates/mobile/src/lib.rs+19-3| ... | @@ -13,10 +13,10 @@ use library::Shared; | ... | @@ -13,10 +13,10 @@ use library::Shared; |
| 13 | pub use library::{Library, Section, Share}; | 13 | pub use library::{Library, Section, Share}; |
| 14 | 14 | ||
| 15 | use canvas::{ | 15 | use canvas::{ |
| 16 | date::DateField, | ||
| 16 | document::TextPosition, | 17 | document::TextPosition, |
| 17 | editor::{Formatting, NoteTag, Selection, TextOutline, Toggle}, | 18 | editor::{Formatting, NoteTag, Selection, TextOutline, Toggle}, |
| 18 | gpu::{Paper, Viewport, page::PageScene}, | 19 | gpu::{Paper, Viewport, page::PageScene}, |
| 19 | date::DateField, | ||
| 20 | interaction::{Hit, ObjectFocus, PageView, Request, Response, TextColors}, | 20 | interaction::{Hit, ObjectFocus, PageView, Request, Response, TextColors}, |
| 21 | layout::TextEngine, | 21 | layout::TextEngine, |
| 22 | }; | 22 | }; |
| ... | @@ -416,7 +416,8 @@ impl Canvas { | ... | @@ -416,7 +416,8 @@ impl Canvas { |
| 416 | /// Selects the first match of `query` on the page, as the search that found it matches. | 416 | /// Selects the first match of `query` on the page, as the search that found it matches. |
| 417 | fn find(&mut self, query: &str) -> Result<bool> { | 417 | fn find(&mut self, query: &str) -> Result<bool> { |
| 418 | let query = canvas::search::Query::new(query); | 418 | let query = canvas::search::Query::new(query); |
| 419 | let Some(&(id, selection)) = canvas::search::page_matches(&self.page.editor, &query).first() | 419 | let Some(&(id, selection)) = |
| 420 | canvas::search::page_matches(&self.page.editor, &query).first() | ||
| 420 | else { | 421 | else { |
| 421 | return Ok(false); | 422 | return Ok(false); |
| 422 | }; | 423 | }; |
| ... | @@ -943,7 +944,9 @@ pub unsafe extern "C" fn sb_view_change_date( | ... | @@ -943,7 +944,9 @@ pub unsafe extern "C" fn sb_view_change_date( |
| 943 | date: *const c_char, | 944 | date: *const c_char, |
| 944 | time: *const c_char, | 945 | time: *const c_char, |
| 945 | ) -> bool { | 946 | ) -> bool { |
| 946 | let result = view.canvas.change_date(seconds, [string(date), string(time)]); | 947 | let result = view |
| 948 | .canvas | ||
| 949 | .change_date(seconds, [string(date), string(time)]); | ||
| 947 | view.stored(result) | 950 | view.stored(result) |
| 948 | } | 951 | } |
| 949 | 952 | ||
| ... | @@ -977,6 +980,19 @@ pub extern "C" fn sb_select(view: &mut View, start: u32, end: u32) -> bool { | ... | @@ -977,6 +980,19 @@ pub extern "C" fn sb_select(view: &mut View, start: u32, end: u32) -> bool { |
| 977 | report(view.canvas.select([start, end])).is_some() | 980 | report(view.canvas.select([start, end])).is_some() |
| 978 | } | 981 | } |
| 979 | 982 | ||
| 983 | /// Widens the selection as OneNote's Ctrl+A does, one enclosing unit per call. | ||
| 984 | #[unsafe(no_mangle)] | ||
| 985 | pub extern "C" fn sb_select_more(view: &mut View) -> bool { | ||
| 986 | report( | ||
| 987 | view.canvas | ||
| 988 | .page | ||
| 989 | .editor | ||
| 990 | .widen_selection() | ||
| 991 | .map_err(Into::into), | ||
| 992 | ) | ||
| 993 | .is_some() | ||
| 994 | } | ||
| 995 | |||
| 980 | #[unsafe(no_mangle)] | 996 | #[unsafe(no_mangle)] |
| 981 | pub extern "C" fn sb_marked(view: &View, range: &mut [u32; 2]) -> bool { | 997 | pub extern "C" fn sb_marked(view: &View, range: &mut [u32; 2]) -> bool { |
| 982 | report(view.canvas.marked()) | 998 | report(view.canvas.marked()) |
crates/mobile/src/library.rs+12-1| ... | @@ -226,6 +226,15 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) { | ... | @@ -226,6 +226,15 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) { |
| 226 | readable, | 226 | readable, |
| 227 | }); | 227 | }); |
| 228 | } | 228 | } |
| 229 | for entry in folder.unavailable.iter().filter(|entry| !entry.group) { | ||
| 230 | tabs.push(Tab { | ||
| 231 | name: stem(&entry.path), | ||
| 232 | path: entry.path.clone(), | ||
| 233 | group: folder.path.clone(), | ||
| 234 | color: rgb(SECTION_COLOR), | ||
| 235 | readable: false, | ||
| 236 | }); | ||
| 237 | } | ||
| 229 | for group in &folder.groups { | 238 | for group in &folder.groups { |
| 230 | if !group.path.ends_with("OneNote_RecycleBin") { | 239 | if !group.path.ends_with("OneNote_RecycleBin") { |
| 231 | self::tabs(group, tabs); | 240 | self::tabs(group, tabs); |
| ... | @@ -891,7 +900,9 @@ pub unsafe extern "C" fn sb_library_new_section( | ... | @@ -891,7 +900,9 @@ pub unsafe extern "C" fn sb_library_new_section( |
| 891 | let (folder, name) = (string(folder), string(name)); | 900 | let (folder, name) = (string(folder), string(name)); |
| 892 | let page = dated(&string(author), &string(date), &string(time)); | 901 | let page = dated(&string(author), &string(date), &string(time)); |
| 893 | report(page.and_then(|page| { | 902 | report(page.and_then(|page| { |
| 894 | library.with_notebook(true, |notebook| Ok(notebook.create_section(&folder, &name, &page)?)) | 903 | library.with_notebook(true, |notebook| { |
| 904 | Ok(notebook.create_section(&folder, &name, &page)?) | ||
| 905 | }) | ||
| 895 | })) | 906 | })) |
| 896 | .map_or(std::ptr::null_mut(), owned) | 907 | .map_or(std::ptr::null_mut(), owned) |
| 897 | } | 908 | } |
crates/mobile/src/tests.rs+45-4| ... | @@ -197,6 +197,33 @@ fn the_text_model_skips_collapsed_paragraphs_and_round_trips_selection() { | ... | @@ -197,6 +197,33 @@ fn the_text_model_skips_collapsed_paragraphs_and_round_trips_selection() { |
| 197 | } | 197 | } |
| 198 | } | 198 | } |
| 199 | 199 | ||
| 200 | #[test] | ||
| 201 | fn select_more_widens_from_the_caret_paragraph_to_the_outline() { | ||
| 202 | let (_directory, section) = features(); | ||
| 203 | let mut canvas = canvas(&section, "Paragraph controls"); | ||
| 204 | focus(&mut canvas, "Collapsed parent"); | ||
| 205 | let shown = canvas.active().shown_text(); | ||
| 206 | let units = |text: &str| text.encode_utf16().count() as u32; | ||
| 207 | let start = units(&shown[..shown.find("Collapsed parent").unwrap()]); | ||
| 208 | let line = units( | ||
| 209 | shown | ||
| 210 | .split('\n') | ||
| 211 | .find(|line| line.contains("Collapsed parent")) | ||
| 212 | .unwrap(), | ||
| 213 | ); | ||
| 214 | canvas.select([start + 2; 2]).unwrap(); | ||
| 215 | canvas.page.editor.widen_selection().unwrap(); | ||
| 216 | let [lo, hi] = canvas.selection().unwrap(); | ||
| 217 | assert!( | ||
| 218 | lo == start && [start + line, start + line + 1].contains(&hi), | ||
| 219 | "{lo}..{hi}" | ||
| 220 | ); | ||
| 221 | for _ in 0..8 { | ||
| 222 | canvas.page.editor.widen_selection().unwrap(); | ||
| 223 | } | ||
| 224 | assert_eq!(canvas.selection().unwrap(), [0, canvas.length()]); | ||
| 225 | } | ||
| 226 | |||
| 200 | #[test] | 227 | #[test] |
| 201 | fn undo_restores_typing() { | 228 | fn undo_restores_typing() { |
| 202 | let (_directory, section) = features(); | 229 | let (_directory, section) = features(); |
| ... | @@ -542,13 +569,24 @@ fn a_local_sections_reads_and_publications_go_through_the_hosts_coordination() { | ... | @@ -542,13 +569,24 @@ fn a_local_sections_reads_and_publications_go_through_the_hosts_coordination() { |
| 542 | let mut canvas = canvas(&section, "Paragraph controls"); | 569 | let mut canvas = canvas(&section, "Paragraph controls"); |
| 543 | focus(&mut canvas, "Collapsed parent"); | 570 | focus(&mut canvas, "Collapsed parent"); |
| 544 | canvas.insert("Coordinated ".into()).unwrap(); | 571 | canvas.insert("Coordinated ".into()).unwrap(); |
| 545 | section.shared.apply(canvas.edit().unwrap().unwrap()).unwrap(); | 572 | section |
| 573 | .shared | ||
| 574 | .apply(canvas.edit().unwrap().unwrap()) | ||
| 575 | .unwrap(); | ||
| 546 | assert!(section.flush(Duration::from_secs(20))); | 576 | assert!(section.flush(Duration::from_secs(20))); |
| 547 | let file = root.canonicalize().unwrap().join("Features.one"); | 577 | let file = root.canonicalize().unwrap().join("Features.one"); |
| 548 | let file = file.to_string_lossy(); | 578 | let file = file.to_string_lossy(); |
| 549 | let coordinated = COORDINATED.lock().unwrap(); | 579 | let coordinated = COORDINATED.lock().unwrap(); |
| 550 | assert!(coordinated.iter().any(|(path, write)| path == &*file && !write)); | 580 | assert!( |
| 551 | assert!(coordinated.iter().any(|(path, write)| path == &*file && *write)); | 581 | coordinated |
| 582 | .iter() | ||
| 583 | .any(|(path, write)| path == &*file && !write) | ||
| 584 | ); | ||
| 585 | assert!( | ||
| 586 | coordinated | ||
| 587 | .iter() | ||
| 588 | .any(|(path, write)| path == &*file && *write) | ||
| 589 | ); | ||
| 552 | } | 590 | } |
| 553 | 591 | ||
| 554 | #[test] | 592 | #[test] |
| ... | @@ -572,7 +610,10 @@ fn tapping_the_date_asks_for_it_and_a_new_date_is_stored() { | ... | @@ -572,7 +610,10 @@ fn tapping_the_date_asks_for_it_and_a_new_date_is_stored() { |
| 572 | // 2 January 2026, 10:30 UTC. | 610 | // 2 January 2026, 10:30 UTC. |
| 573 | assert!( | 611 | assert!( |
| 574 | canvas | 612 | canvas |
| 575 | .change_date(1_767_349_800, ["Friday, January 2, 2026".into(), "10:30 AM".into()]) | 613 | .change_date( |
| 614 | 1_767_349_800, | ||
| 615 | ["Friday, January 2, 2026".into(), "10:30 AM".into()] | ||
| 616 | ) | ||
| 576 | .unwrap() | 617 | .unwrap() |
| 577 | ); | 618 | ); |
| 578 | let edit = canvas.edit().unwrap().unwrap(); | 619 | let edit = canvas.edit().unwrap().unwrap(); |