1//! OneNote 2010's Insert Space: what lies past a line across the page moves along one axis,
2//! and a body outline the line crosses parts between its paragraphs.
3
4use super::*;
5
6/// Something Insert Space moves.
7enum Piece {
8 /// A body outline, picture, file or ink drawing, placed anew.
9 Object(ExGuid),
10 /// The root nodes of a body outline from `at` on.
11 Tail { outline: ExGuid, at: usize },
12}
13
14impl CanvasEditor {
15 /// What starts at or past `line` along `axis` (0 for x, 1 for y) with its extent as
16 /// `[x0, y0, x1, y1]` page points, and the farthest end of what starts before the line.
17 fn spaced(&self, axis: usize, line: f32) -> (Vec<(Piece, [f32; 4])>, f32) {
18 let mut moved = Vec::new();
19 let mut floor = f32::NEG_INFINITY;
20 let mut stays = |rect: [f32; 4]| {
21 if rect[axis] < line {
22 floor = floor.max(rect[axis + 2]);
23 }
24 };
25 for outline in &self.outlines {
26 let bounds = outline.bounds();
27 let rect = [bounds.x0, bounds.y0, bounds.x1, bounds.y1].map(|value| value as f32);
28 if outline.title {
29 stays(rect);
30 continue;
31 }
32 if rect[axis] >= line {
33 moved.push((Piece::Object(outline.id), rect));
34 continue;
35 }
36 let top = outline.origin()[1];
37 let spans: Vec<Option<[f32; 2]>> = outline.shaped.spans().collect();
38 let at = (axis == 1)
39 .then(|| {
40 spans
41 .iter()
42 .position(|span| span.is_some_and(|[start, _]| top + start >= line))
43 })
44 .flatten();
45 let Some(at) = at else {
46 stays(rect);
47 continue;
48 };
49 let [start, _] = spans[at].unwrap();
50 moved.push((
51 Piece::Tail {
52 outline: outline.id,
53 at,
54 },
55 [rect[0], top + start, rect[2], rect[3]],
56 ));
57 let end = spans[..at]
58 .iter()
59 .flatten()
60 .map(|[_, end]| *end)
61 .fold(0.0, f32::max);
62 stays([rect[0], rect[1], rect[2], top + end]);
63 }
64 for content in &self.objects {
65 let (id, rect) = match content {
66 page::Content::Outline {
67 source,
68 layout,
69 below_title: None,
70 } => {
71 let [x, y] = crate::origin(&source.layout);
72 (source.id, [x, y, x + layout.size[0], y + layout.size[1]])
73 }
74 page::Content::Image(image) => {
75 let (origin, size) = self.image_placement(image.id).unwrap_or_default();
76 let rect = [
77 origin[0],
78 origin[1],
79 origin[0] + size[0],
80 origin[1] + size[1],
81 ];
82 (image.id, rect)
83 }
84 page::Content::File { source, .. } => (source.id, content.file().unwrap().1),
85 page::Content::Ink(ink) => match page::ink_bounds(ink) {
86 Some(rect) => (ink.id, rect),
87 None => continue,
88 },
89 page::Content::ReadOnly(object)
90 if matches!(object.source, PageObject::Outline(_) | PageObject::Image(_)) =>
91 {
92 (object.source.id(), object.rect())
93 }
94 page::Content::ReadOnly(object) => {
95 stays(object.rect());
96 continue;
97 }
98 page::Content::Outline { .. } | page::Content::Date { .. } => continue,
99 page::Content::Editable(_) => continue,
100 };
101 if rect[axis] >= line {
102 moved.push((Piece::Object(id), rect));
103 } else {
104 stays(rect);
105 }
106 }
107 (moved, floor)
108 }
109
110 /// How far back from `line` Insert Space can take the page along `axis`: never over what
111 /// stays before the line, so at most zero.
112 pub(crate) fn space_limit(&self, axis: usize, line: f32) -> f32 {
113 (self.spaced(axis, line).1 - line).min(0.0)
114 }
115
116 /// The extents, as `[x0, y0, x1, y1]` page points, of what Insert Space at `line` along
117 /// `axis` moves.
118 pub(crate) fn space_preview(&self, axis: usize, line: f32) -> Vec<[f32; 4]> {
119 self.spaced(axis, line)
120 .0
121 .into_iter()
122 .map(|(_, rect)| rect)
123 .collect()
124 }
125
126 /// Moves what starts at or past `line` along `axis` by `delta`, taking no more space back
127 /// than `space_limit` allows, as one undo step. False when nothing moves.
128 pub fn insert_space(
129 &mut self,
130 engine: &mut TextEngine,
131 axis: usize,
132 line: f32,
133 delta: f32,
134 ) -> Result<bool, EditorError> {
135 if !line.is_finite() || !delta.is_finite() || axis > 1 {
136 return Err(EditError::InvalidRange.into());
137 }
138 let (moved, floor) = self.spaced(axis, line);
139 let delta = delta.max((floor - line).min(0.0));
140 if delta == 0.0 || moved.is_empty() {
141 return Ok(false);
142 }
143 self.finish_composition();
144 self.whole = None;
145 if let Focus::Draft { index, .. } = self.active {
146 self.active = Focus::Outline(index);
147 }
148 let shift = |mut position: [f32; 2]| {
149 position[axis] += delta;
150 position
151 };
152 let depth = self.undo.len();
153 for (piece, rect) in moved {
154 let result = match piece {
155 Piece::Object(id) => {
156 let layout = self.object_layout_mut(id).unwrap();
157 // Undo restores an unset coordinate as the zero it stands for, as ops
158 // cannot unset one.
159 let origin = crate::origin(layout);
160 let previous = origin.map(Some);
161 [layout.x, layout.y] = shift(origin).map(Some);
162 self.undo.push(History::Position {
163 object: id,
164 position: previous,
165 });
166 self.record(self.placement_ops(&Placement {
167 id,
168 position: previous,
169 }));
170 Ok(())
171 }
172 Piece::Tail { outline, at } => {
173 let part = onestore::page::text::new_id()?;
174 self.split_outline(engine, outline, at, part, shift([rect[0], rect[1]]))
175 .map(|parents| {
176 self.undo.push(History::Join {
177 outline,
178 part,
179 parents,
180 })
181 })
182 }
183 };
184 if let Err(error) = result {
185 self.group(depth, false);
186 return Err(error);
187 }
188 }
189 self.group(depth, false);
190 self.redo.clear();
191 self.preferred_x = None;
192 Ok(true)
193 }
194
195 /// Parts outline `id`'s root nodes from `at` into new outline `part` at `position`, which
196 /// takes the rest of the outline's layout; returns each root `part` then holds with the
197 /// parent it had.
198 pub(super) fn split_outline(
199 &mut self,
200 engine: &mut TextEngine,
201 id: ExGuid,
202 at: usize,
203 part: ExGuid,
204 position: [f32; 2],
205 ) -> Result<Vec<(ExGuid, Option<ExGuid>)>, EditorError> {
206 let index = self
207 .outlines
208 .iter()
209 .position(|outline| outline.id == id)
210 .ok_or(EditError::InvalidRange)?;
211 let mut kept = self.outlines[index].snapshot();
212 let mut tail = kept.paragraphs.split_off(at);
213 let head: BTreeSet<ExGuid> = kept.paragraphs.iter().map(|node| node.id).collect();
214 let mut parents = Vec::new();
215 for node in &mut tail {
216 if node.parent.is_none_or(|parent| head.contains(&parent)) {
217 parents.push((node.id, node.parent.take()));
218 }
219 }
220 let [x, y] = position.map(Some);
221 let moved = Outline {
222 id: part,
223 layout: onestore::document::Layout {
224 x,
225 y,
226 max_height: None,
227 ..kept.layout.clone()
228 },
229 paragraphs: tail,
230 ..kept.clone()
231 };
232 let head = TextOutline::from_outline(engine, &kept, &self.definitions)?;
233 let tail = TextOutline::from_outline(engine, &moved, &self.definitions)?;
234 self.outlines[index] = head;
235 self.outlines.insert(index + 1, tail);
236 match &mut self.active {
237 Focus::Outline(active) | Focus::Caret { index: active, .. } if *active > index => {
238 *active += 1
239 }
240 _ => {}
241 }
242 if let Some(slot) = self
243 .objects
244 .iter()
245 .position(|object| matches!(object, page::Content::Editable(slot) if *slot == id))
246 {
247 self.objects.insert(slot + 1, page::Content::Editable(part));
248 }
249 let anchor = ops::anchor()?;
250 let lowered = self
251 .added(Outline {
252 paragraphs: vec![anchor.clone()],
253 ..moved
254 })
255 .map(|mut added| {
256 added.extend(parents.iter().map(|(root, _)| PageOp::Move {
257 object: *root,
258 parent: Some(part),
259 before: Some(anchor.id),
260 }));
261 added.push(PageOp::Delete { object: anchor.id });
262 added
263 });
264 self.record(lowered);
265 Ok(parents)
266 }
267
268 /// Returns outline `part`'s root nodes to the end of outline `id`, each under its parent
269 /// in `parents`, and removes `part`; returns where `split_outline` would part them again.
270 pub(super) fn join_outline(
271 &mut self,
272 engine: &mut TextEngine,
273 id: ExGuid,
274 part: ExGuid,
275 parents: &[(ExGuid, Option<ExGuid>)],
276 ) -> Result<(usize, [f32; 2]), EditorError> {
277 let find = |id| self.outlines.iter().position(|outline| outline.id == id);
278 let (Some(index), Some(from)) = (find(id), find(part)) else {
279 return Err(EditError::InvalidRange.into());
280 };
281 let mut joined = self.outlines[index].snapshot();
282 let at = joined.paragraphs.len();
283 let position = self.outlines[from].origin();
284 let parents: BTreeMap<ExGuid, Option<ExGuid>> = parents.iter().copied().collect();
285 joined
286 .paragraphs
287 .extend(self.outlines[from].document.nodes().iter().map(|node| {
288 let mut node = node.clone();
289 if let Some(parent) = parents.get(&node.id) {
290 node.parent = *parent;
291 }
292 node
293 }));
294 let roots: Vec<ExGuid> = self.outlines[from]
295 .document
296 .nodes()
297 .iter()
298 .filter(|node| node.parent.is_none())
299 .map(|node| node.id)
300 .collect();
301 self.outlines[index] = TextOutline::from_outline(engine, &joined, &self.definitions)?;
302 self.outlines.remove(from);
303 match &mut self.active {
304 Focus::Outline(active) if *active == from => {
305 *active = index - usize::from(index > from)
306 }
307 Focus::Outline(active) | Focus::Caret { index: active, .. } if *active > from => {
308 *active -= 1
309 }
310 _ => {}
311 }
312 self.objects
313 .retain(|object| !matches!(object, page::Content::Editable(slot) if *slot == part));
314 // Moving its last paragraph out removes the emptied outline.
315 let ops = roots
316 .into_iter()
317 .map(|root| PageOp::Move {
318 object: root,
319 parent: Some(parents.get(&root).copied().flatten().unwrap_or(id)),
320 before: None,
321 })
322 .collect();
323 self.record(Ok(ops));
324 Ok((at, position))
325 }
326}