1use std::cell::OnceCell;
14use std::ops::Range;
15
16use eframe::egui::Id;
17use egui_richedit::{Edit, Mark, Model, Position, RichEdit};
18use odox_core::edit::{self, is_paragraph};
19use odox_core::{Element, Ns, Refused, Styles};
20
21use crate::Editing;
22use crate::flow::is_block_container;
23
24pub type PageEditor = RichEdit<Vec<usize>>;
26
27pub(crate) fn page_editor_id() -> Id {
30 Id::new("odox-page-editor")
31}
32
33pub fn page_editor() -> PageEditor {
35 RichEdit::new(page_editor_id())
36}
37
38pub struct FlowModel<'a> {
40 content: &'a mut Element,
41 styles: &'a mut Styles,
42 root: Vec<usize>,
44 editing: &'a mut Editing,
45 scope: Vec<usize>,
48 order: OnceCell<Vec<Vec<usize>>>,
51}
52
53impl<'a> FlowModel<'a> {
54 pub fn new(
57 content: &'a mut Element,
58 styles: &'a mut Styles,
59 root: Vec<usize>,
60 editing: &'a mut Editing,
61 ) -> Self {
62 Self {
63 content,
64 styles,
65 root,
66 editing,
67 scope: Vec::new(),
68 order: OnceCell::new(),
69 }
70 }
71
72 #[must_use]
76 pub fn within(mut self, scope: Vec<usize>) -> Self {
77 self.scope = scope;
78 self
79 }
80
81 fn root(&self) -> Option<&Element> {
82 self.content.at(&self.root)
83 }
84
85 fn paragraph_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
86 self.content
87 .at_mut(&self.root)?
88 .at_mut(path)
89 .filter(|element| is_paragraph(element))
90 }
91
92 fn order(&self) -> &[Vec<usize>] {
93 self.order.get_or_init(|| {
94 let mut out = Vec::new();
95 if let Some(root) = self.root() {
96 blocks(root, &mut Vec::new(), &mut out);
97 }
98 out.retain(|path| path.starts_with(&self.scope));
99 out
100 })
101 }
102
103 fn neighbour(&self, paragraph: &[usize], step: isize) -> Option<Vec<usize>> {
104 let order = self.order();
105 let index = order.iter().position(|p| p == paragraph)?;
106 order.get(index.checked_add_signed(step)?).cloned()
107 }
108
109 fn replace(
110 &mut self,
111 from: &Position<Vec<usize>>,
112 to: &Position<Vec<usize>>,
113 text: &str,
114 ) -> Option<()> {
115 let root = self.content.at_mut(&self.root)?;
116 match edit::replace_range(
117 root,
118 (&from.paragraph, from.offset),
119 (&to.paragraph, to.offset),
120 text,
121 ) {
122 Ok(()) => Some(()),
123 Err(Refused::Structure) => self.clear_between(from, to, text),
124 Err(_) => None,
125 }
126 }
127
128 fn clear_between(
134 &mut self,
135 from: &Position<Vec<usize>>,
136 to: &Position<Vec<usize>>,
137 text: &str,
138 ) -> Option<()> {
139 let ranges = self.covered(from, to)?;
140 if text.is_empty() && ranges.iter().all(|(_, range)| range.is_empty()) {
141 return None;
142 }
143 for (path, range) in ranges.into_iter().rev() {
144 let with = if path == from.paragraph { text } else { "" };
145 let paragraph = self.paragraph_mut(&path)?;
146 edit::replace(paragraph, range, with);
147 }
148 Some(())
149 }
150
151 fn covered(
154 &self,
155 from: &Position<Vec<usize>>,
156 to: &Position<Vec<usize>>,
157 ) -> Option<Vec<(Vec<usize>, Range<usize>)>> {
158 let order = self.order();
159 let first = order.iter().position(|p| *p == from.paragraph)?;
160 let last = order.iter().position(|p| *p == to.paragraph)?;
161 let covered: Vec<Vec<usize>> = order.get(first..=last)?.to_vec();
162 Some(
163 covered
164 .into_iter()
165 .map(|path| {
166 let len = self.text(&path).map_or(0, |t| t.chars().count());
167 let start = if path == from.paragraph {
168 from.offset
169 } else {
170 0
171 };
172 let end = if path == to.paragraph { to.offset } else { len };
173 (path, start..end.max(start))
174 })
175 .collect(),
176 )
177 }
178
179 fn format(
182 &mut self,
183 from: &Position<Vec<usize>>,
184 to: &Position<Vec<usize>>,
185 mark: edit::Mark,
186 on: bool,
187 ) -> Option<()> {
188 let automatic =
189 |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
190 for (path, range) in self.covered(from, to)? {
191 if range.is_empty() {
192 continue;
193 }
194 let had = automatic(self.content);
195 let mut at = self.root.clone();
196 at.extend(&path);
197 edit::format(self.content, &at, range, mark, on, self.styles).ok()?;
198 if !had
201 && automatic(self.content)
202 && let Some(first) = self.root.first_mut()
203 {
204 *first += 1;
205 }
206 }
207 Some(())
208 }
209}
210
211fn core_mark(mark: Mark) -> edit::Mark {
213 match mark {
214 Mark::Bold => edit::Mark::Bold,
215 Mark::Italic => edit::Mark::Italic,
216 Mark::Underline => edit::Mark::Underline,
217 Mark::Strike => edit::Mark::Strike,
218 }
219}
220
221impl Model for FlowModel<'_> {
222 type Paragraph = Vec<usize>;
223
224 fn text(&self, paragraph: &Vec<usize>) -> Option<String> {
225 self.root()?
226 .at(paragraph)
227 .filter(|element| is_paragraph(element))
228 .map(edit::text)
229 }
230
231 fn next(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
232 self.neighbour(paragraph, 1)
233 }
234
235 fn previous(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
236 self.neighbour(paragraph, -1)
237 }
238
239 fn first(&self) -> Option<Vec<usize>> {
240 self.order().first().cloned()
241 }
242
243 fn last(&self) -> Option<Vec<usize>> {
244 self.order().last().cloned()
245 }
246
247 fn marked(
248 &self,
249 from: &Position<Vec<usize>>,
250 to: &Position<Vec<usize>>,
251 mark: Mark,
252 ) -> Option<bool> {
253 let root = self.root()?;
254 let mark = core_mark(mark);
255 let ranges: Vec<_> = self
256 .covered(from, to)?
257 .into_iter()
258 .filter(|(_, range)| !range.is_empty())
259 .collect();
260 if ranges.is_empty() {
261 let at = from.offset..from.offset;
262 return edit::marked(root.at(&from.paragraph)?, at, mark, self.styles);
263 }
264 let mut answer = None;
265 for (path, range) in ranges {
266 let value = edit::marked(root.at(&path)?, range, mark, self.styles)?;
267 if answer.is_some_and(|a| a != value) {
268 return None;
269 }
270 answer = Some(value);
271 }
272 answer
273 }
274
275 fn apply(
276 &mut self,
277 edit: Edit<'_, Vec<usize>>,
278 new_step: bool,
279 ) -> Option<Position<Vec<usize>>> {
280 let new_step = new_step || !self.editing.modified();
284 let before = new_step.then(|| self.content.clone());
288 let begins = match &edit {
289 Edit::Replace { from, .. } | Edit::Format { from, .. } => from.clone(),
290 Edit::Split { at } => at.clone(),
291 };
292 let at = match edit {
293 Edit::Replace { from, to, text } => self
294 .replace(&from, &to, text)
295 .map(|()| Position::new(from.paragraph, from.offset + text.chars().count())),
296 Edit::Split { at } => {
297 let root = self.content.at_mut(&self.root)?;
298 edit::split_at(root, &at.paragraph, at.offset)
299 .ok()
300 .map(|second| Position::new(second, 0))
301 }
302 Edit::Format { from, to, mark, on } => {
303 self.format(&from, &to, core_mark(mark), on).map(|()| to)
304 }
305 };
306 if at.is_some() {
307 if let Some(before) = before {
308 self.editing.record_snapshot(before, Some(begins));
309 }
310 self.order = OnceCell::new();
311 }
312 at
313 }
314}
315
316fn blocks(parent: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
319 for (index, element) in parent.elements_indexed() {
320 path.push(index);
321 block(element, path, out);
322 path.pop();
323 }
324}
325
326fn block(element: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
327 if is_paragraph(element) {
328 out.push(path.clone());
329 } else if element.is(&Ns::Text, "list") {
330 for (item_index, item) in element.elements_indexed() {
331 if item.is(&Ns::Text, "list-item") || item.is(&Ns::Text, "list-header") {
332 path.push(item_index);
333 blocks(item, path, out);
334 path.pop();
335 }
336 }
337 } else if element.is(&Ns::Table, "table")
338 || element.is(&Ns::Table, "table-header-rows")
339 || element.is(&Ns::Table, "table-rows")
340 || element.is(&Ns::Table, "table-row-group")
341 {
342 blocks(element, path, out);
343 } else if element.is(&Ns::Table, "table-row") {
344 for (cell_index, cell) in element.elements_indexed() {
345 if cell.is(&Ns::Table, "table-cell") {
346 path.push(cell_index);
347 blocks(cell, path, out);
348 path.pop();
349 }
350 }
351 } else if element.is(&Ns::Draw, "frame") {
352 if let Some((box_index, text_box)) = element
353 .elements_indexed()
354 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
355 {
356 path.push(box_index);
357 blocks(text_box, path, out);
358 path.pop();
359 }
360 } else if element.name.ns == Ns::Draw {
361 blocks(element, path, out);
364 } else if is_block_container(element) {
365 blocks(element, path, out);
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::FlowModel;
372 use crate::Editing;
373 use egui_richedit::{Edit, Model, Position};
374 use odox_core::{Element, Node, Ns, Styles};
375
376 fn element(prefix: &str, local: &str, ns: Ns, children: Vec<Node>) -> Node {
377 let mut e = Element::new(prefix, local, ns);
378 e.children = children;
379 Node::Element(e)
380 }
381
382 fn p(text: &str) -> Node {
383 element("text", "p", Ns::Text, vec![Node::Text(text.to_owned())])
384 }
385
386 fn content() -> Element {
389 let list = element(
390 "text",
391 "list",
392 Ns::Text,
393 vec![element("text", "list-item", Ns::Text, vec![p("item")])],
394 );
395 let table = element(
396 "table",
397 "table",
398 Ns::Table,
399 vec![element(
400 "table",
401 "table-row",
402 Ns::Table,
403 vec![element("table", "table-cell", Ns::Table, vec![p("cell")])],
404 )],
405 );
406 let body = element(
407 "office",
408 "text",
409 Ns::Office,
410 vec![p("one"), list, table, p("two")],
411 );
412 let mut root = Element::new("office", "document-content", Ns::Office);
413 root.children = vec![element("office", "body", Ns::Office, vec![body])];
414 root
415 }
416
417 #[test]
418 fn paragraphs_follow_one_another_through_lists_and_tables() {
419 let mut content = content();
420 let mut styles = Styles::collect(Some(&content), None);
421 let mut editing = Editing::default();
422 let model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
423 let mut at = vec![0];
424 let mut seen = vec![model.text(&at).expect("the first")];
425 while let Some(next) = model.next(&at) {
426 seen.push(model.text(&next).expect("a paragraph"));
427 assert_eq!(model.previous(&next), Some(at));
428 at = next;
429 }
430 assert_eq!(seen, ["one", "item", "cell", "two"]);
431 }
432
433 #[test]
434 fn a_selection_over_a_whole_table_takes_it_and_joins_the_ends() {
435 let mut content = content();
436 let mut styles = Styles::collect(Some(&content), None);
437 let mut editing = Editing::default();
438 editing.reset();
439 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
440 let at = model.apply(
442 Edit::Replace {
443 from: Position::new(vec![1, 0, 0], 2),
444 to: Position::new(vec![3], 1),
445 text: "X",
446 },
447 true,
448 );
449 assert_eq!(at, Some(Position::new(vec![1, 0, 0], 3)));
450 assert_eq!(model.text(&vec![1, 0, 0]).as_deref(), Some("itXwo"));
451 assert_eq!(
452 model.next(&vec![1, 0, 0]),
453 None,
454 "the table and \"two\" are gone"
455 );
456 }
457
458 #[test]
459 fn a_selection_out_of_a_cell_takes_the_text_and_leaves_the_cell() {
460 let mut content = content();
461 let mut styles = Styles::collect(Some(&content), None);
462 let mut editing = Editing::default();
463 editing.reset();
464 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
465 let at = model.apply(
467 Edit::Replace {
468 from: Position::new(vec![2, 0, 0, 0], 2),
469 to: Position::new(vec![3], 1),
470 text: "",
471 },
472 true,
473 );
474 assert_eq!(at, Some(Position::new(vec![2, 0, 0, 0], 2)));
475 assert_eq!(model.text(&vec![2, 0, 0, 0]).as_deref(), Some("ce"));
476 assert_eq!(model.text(&vec![3]).as_deref(), Some("wo"));
477 }
478
479 #[test]
480 fn backspace_at_a_list_item_joins_it_to_the_paragraph_before() {
481 let mut content = content();
482 let mut styles = Styles::collect(Some(&content), None);
483 let mut editing = Editing::default();
484 editing.reset();
485 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
486 let at = model.apply(
487 Edit::Replace {
488 from: Position::new(vec![0], 3),
489 to: Position::new(vec![1, 0, 0], 0),
490 text: "",
491 },
492 true,
493 );
494 assert_eq!(at, Some(Position::new(vec![0], 3)));
495 assert_eq!(model.text(&vec![0]).as_deref(), Some("oneitem"));
496 assert_eq!(model.text(&vec![1, 0, 0, 0]).as_deref(), Some("cell"));
498 }
499
500 #[test]
501 fn typing_that_runs_on_past_a_save_is_a_change() {
502 let mut content = content();
503 let mut styles = Styles::collect(Some(&content), None);
504 let mut editing = Editing::default();
505 editing.reset();
506 let at = |offset| Position::new(vec![0], offset);
507 let typed = |offset| Edit::Replace {
508 from: at(offset),
509 to: at(offset),
510 text: "x",
511 };
512 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
513 model.apply(typed(0), true);
514 model.editing.mark_saved();
515 model.apply(typed(1), false);
517 assert!(
518 editing.modified(),
519 "what was typed after the save is unsaved"
520 );
521 }
522
523 #[test]
524 fn an_edit_that_begins_a_step_is_recorded_once_it_succeeds() {
525 let mut content = content();
526 let mut styles = Styles::collect(Some(&content), None);
527 let mut editing = Editing::default();
528 editing.reset();
529 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
530 let at = model.apply(
531 Edit::Split {
532 at: Position::new(vec![0], 1),
533 },
534 true,
535 );
536 assert_eq!(at, Some(Position::new(vec![1], 0)));
537 assert_eq!(model.text(&vec![0]).as_deref(), Some("o"));
538 assert_eq!(model.text(&vec![1]).as_deref(), Some("ne"));
539
540 let refused = model.apply(
543 Edit::Replace {
544 from: Position::new(vec![2, 0, 0], 4),
545 to: Position::new(vec![3, 0, 0, 0], 0),
546 text: "",
547 },
548 true,
549 );
550 assert_eq!(refused, None);
551 assert!(editing.can_undo());
552 let mut undone = 0;
553 let mut current = content.clone();
554 while let Some(previous) = editing.undo(¤t, None).map(|(tree, _)| tree) {
555 current = previous;
556 undone += 1;
557 }
558 assert_eq!(undone, 1);
559 }
560}