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
9use canvas::{
10 editor::Pen,
11 gpu::page::PageScene,
12 interaction::{PageView, ink::Tool},
13 layout::TextEngine,
14};
15use onestore::{
16 Arena, ExGuid, Section, Store,
17 op::{Edit, Op},
18 page::{Page, PageObject},
19};
20use std::time::{Duration, Instant};
21
22const SOURCE: &[u8] =
23 include_bytes!("../../../corpus/m6/native-page-direction-03/notebook/synthetic.one");
24
25fn 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
34fn 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]
52fn 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}