| 1 | //! A right-to-left page through the page view, as a host drives it. OneNote 2010 keeps such a |
| 2 | //! page's positions in the same left-to-right frame, its margin origin some 10,800 pt right of |
| 3 | //! the page origin, and opens it scrolled to the right end of its content; the view does the |
| 4 | //! same, and a click, a stroke and a drag store positions OneNote reads back where they were |
| 5 | //! put. `SNOWBOUND_RTL_EXPORT` names a new directory receiving the edited section as a notebook |
| 6 | //! for a cold reopen in OneNote 2010 (`corpus/rtl-page`). |
| 7 | #![cfg(feature = "interaction")] |
| 8 | |
| 9 | use canvas::{ |
| 10 | editor::Pen, |
| 11 | gpu::page::PageScene, |
| 12 | interaction::{PageView, ink::Tool}, |
| 13 | layout::TextEngine, |
| 14 | }; |
| 15 | use onestore::{ |
| 16 | Arena, ExGuid, Section, Store, |
| 17 | op::{Edit, Op}, |
| 18 | page::{Page, PageObject}, |
| 19 | }; |
| 20 | use std::time::{Duration, Instant}; |
| 21 | |
| 22 | const SOURCE: &[u8] = |
| 23 | include_bytes!("../../../corpus/m6/native-page-direction-03/notebook/synthetic.one"); |
| 24 | |
| 25 | fn drag(view: &mut PageView, path: &[[f32; 2]]) { |
| 26 | let _ = view.pointer_moved(path[0]).unwrap(); |
| 27 | let _ = view.pointer_pressed(Instant::now()).unwrap(); |
| 28 | for point in &path[1..] { |
| 29 | let _ = view.pointer_moved(*point).unwrap(); |
| 30 | } |
| 31 | let _ = view.pointer_released().unwrap(); |
| 32 | } |
| 33 | |
| 34 | fn outline<'a>(page: &'a Page, text: &str) -> &'a onestore::page::Outline { |
| 35 | page.objects |
| 36 | .iter() |
| 37 | .find_map(|object| match object { |
| 38 | PageObject::Outline(outline) |
| 39 | if outline.paragraphs.iter().any(|paragraph| { |
| 40 | matches!(&paragraph.content, |
| 41 | onestore::page::ParagraphContent::Text(run) if run.text.text() == text) |
| 42 | }) => |
| 43 | { |
| 44 | Some(outline) |
| 45 | } |
| 46 | _ => None, |
| 47 | }) |
| 48 | .unwrap() |
| 49 | } |
| 50 | |
| 51 | #[test] |
| 52 | fn a_right_to_left_page_opens_at_its_right_end_and_stores_edits_in_onenotes_frame() { |
| 53 | let arena = Arena::default(); |
| 54 | let mut section = Section::open(&arena, SOURCE.to_vec()).unwrap(); |
| 55 | let space: ExGuid = section.pages().unwrap()[0].0; |
| 56 | let page = section.page(space).unwrap(); |
| 57 | assert!(page.rtl); |
| 58 | let margin = page.margin_origin; |
| 59 | let ink_right = page |
| 60 | .objects |
| 61 | .iter() |
| 62 | .filter_map(|object| match object { |
| 63 | PageObject::Ink(ink) => ink.bounds().map(|b| b[0] + b[2] + ink.layout.x.unwrap()), |
| 64 | _ => None, |
| 65 | }) |
| 66 | .fold(f32::NEG_INFINITY, f32::max); |
| 67 | let mut engine = TextEngine::default(); |
| 68 | let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap(); |
| 69 | let mut view = PageView::new( |
| 70 | editor, |
| 71 | engine, |
| 72 | Some((scene, [0.0; 2])), |
| 73 | [800, 600], |
| 74 | 1.0, |
| 75 | Duration::from_millis(500), |
| 76 | ); |
| 77 | |
| 78 | // The view opens at the right end of the content, the drawings at its right edge. |
| 79 | let scroll = view.scroll(); |
| 80 | assert_eq!(view.viewport.origin[0], -scroll.max[0]); |
| 81 | let right = view.viewport.document_point([800.0, 0.0])[0]; |
| 82 | assert!( |
| 83 | ink_right <= right && right < ink_right + 12.0, |
| 84 | "{ink_right} {right}" |
| 85 | ); |
| 86 | assert_eq!(scroll.min[0], 0.0); |
| 87 | // Resizing keeps the right edge, as OneNote's does. |
| 88 | let _ = view.resized([600, 600]).unwrap(); |
| 89 | assert_eq!(view.viewport.document_point([600.0, 0.0])[0], right); |
| 90 | let _ = view.resized([800, 600]).unwrap(); |
| 91 | assert_eq!(view.viewport.document_point([800.0, 0.0])[0], right); |
| 92 | |
| 93 | let mut at = 134_100_000_000_000_000; |
| 94 | let mut store = |view: &mut PageView, section: &mut Section| { |
| 95 | let ops = view.editor.take_ops().unwrap(); |
| 96 | assert!(!ops.is_empty()); |
| 97 | at += 10_000_000; |
| 98 | let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect(); |
| 99 | section.apply("Snowbound Test", &Edit { at, ops }).unwrap(); |
| 100 | section.page(space).unwrap() |
| 101 | }; |
| 102 | |
| 103 | // A click on blank page puts an outline there, on the grid anchored at the margin origin. |
| 104 | let click = [240.0, 480.0]; |
| 105 | let point = view.viewport.document_point(click); |
| 106 | let _ = view.pointer_moved(click).unwrap(); |
| 107 | let _ = view.pointer_pressed(Instant::now()).unwrap(); |
| 108 | let _ = view.pointer_released().unwrap(); |
| 109 | let _ = view |
| 110 | .insert_text("Typed on a right-to-left page".into()) |
| 111 | .unwrap(); |
| 112 | let page = store(&mut view, &mut section); |
| 113 | let typed = outline(&page, "Typed on a right-to-left page") |
| 114 | .layout |
| 115 | .clone(); |
| 116 | let [x, y] = [typed.x.unwrap(), typed.y.unwrap()]; |
| 117 | for (stored, clicked, origin) in [(x, point[0], margin[0]), (y, point[1], margin[1])] { |
| 118 | assert!( |
| 119 | ((stored - origin) / 18.0).fract().abs() < 1e-3, |
| 120 | "{stored} {origin}" |
| 121 | ); |
| 122 | assert!((stored - clicked).abs() <= 18.0, "{stored} {clicked}"); |
| 123 | } |
| 124 | assert!(x > margin[0]); |
| 125 | |
| 126 | // A stroke lands in page points where the pen went. |
| 127 | let _ = view.set_tool(Tool::Pen(Pen::new(1.0, Some(0x1f4e79)))); |
| 128 | let path: Vec<[f32; 2]> = (0..=20) |
| 129 | .map(|step| [300.0 + step as f32 * 5.0, 520.0 + step as f32 * 2.0]) |
| 130 | .collect(); |
| 131 | drag(&mut view, &path); |
| 132 | let known: Vec<ExGuid> = page.objects.iter().map(PageObject::id).collect(); |
| 133 | let page = store(&mut view, &mut section); |
| 134 | let [PageObject::Ink(ink)] = page |
| 135 | .objects |
| 136 | .iter() |
| 137 | .filter(|object| !known.contains(&object.id())) |
| 138 | .collect::<Vec<_>>()[..] |
| 139 | else { |
| 140 | panic!("the stroke is not one new drawing") |
| 141 | }; |
| 142 | let offset = [ink.layout.x.unwrap_or(0.0), ink.layout.y.unwrap_or(0.0)]; |
| 143 | let first = ink.strokes[0].points[0]; |
| 144 | let expected = view.viewport.document_point(path[0]); |
| 145 | for axis in 0..2 { |
| 146 | assert!((first[axis] + offset[axis] - expected[axis]).abs() < 0.5); |
| 147 | } |
| 148 | let _ = view.set_tool(Tool::Select); |
| 149 | |
| 150 | // The header drags an outline two grid columns right, onto the grid anchored at the |
| 151 | // margin origin. |
| 152 | let before = outline(&page, "Fictitious positioned outline.") |
| 153 | .layout |
| 154 | .clone(); |
| 155 | let header = [ |
| 156 | before.x.unwrap() * view.viewport.scale + view.viewport.origin[0] + 40.0, |
| 157 | before.y.unwrap() * view.viewport.scale + view.viewport.origin[1] - 8.0, |
| 158 | ]; |
| 159 | let steps: Vec<[f32; 2]> = (0..=12) |
| 160 | .map(|step| [header[0] + step as f32 * 4.0, header[1] + step as f32 * 2.0]) |
| 161 | .collect(); |
| 162 | drag(&mut view, &steps); |
| 163 | let page = store(&mut view, &mut section); |
| 164 | let after = outline(&page, "Fictitious positioned outline.") |
| 165 | .layout |
| 166 | .clone(); |
| 167 | assert_eq!(after.x.unwrap() - before.x.unwrap(), 36.0); |
| 168 | assert!(after.y.unwrap() > before.y.unwrap()); |
| 169 | assert_eq!(((after.y.unwrap() - margin[1]) / 18.0).fract(), 0.0); |
| 170 | |
| 171 | section.seal().unwrap(); |
| 172 | let written = section.image(); |
| 173 | if let Some(directory) = std::env::var_os("SNOWBOUND_RTL_EXPORT") { |
| 174 | let directory = std::path::PathBuf::from(directory); |
| 175 | std::fs::create_dir(&directory).unwrap(); |
| 176 | std::fs::write(directory.join("synthetic.one"), &written).unwrap(); |
| 177 | let file_id = Store::parse(&written).unwrap().header.file_id; |
| 178 | std::fs::write( |
| 179 | directory.join("Open Notebook.onetoc2"), |
| 180 | onestore::create_table_of_contents( |
| 181 | "Open Notebook.onetoc2", |
| 182 | &[("synthetic.one", file_id)], |
| 183 | ) |
| 184 | .unwrap(), |
| 185 | ) |
| 186 | .unwrap(); |
| 187 | } |
| 188 | } |