1use std::cell::OnceCell;
14use std::ops::Range;
15
16use eframe::egui::Id;
17use egui_richedit::{Edit, Mark, Model, Position, RichEdit, Selection};
18use odox_core::edit::{self, is_paragraph};
19use odox_core::{Element, ListKind, 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
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Block {
41 Heading(u8),
44 Body,
46 Bullets,
48 Numbers,
50}
51
52pub struct FlowModel<'a> {
54 content: &'a mut Element,
55 styles: &'a mut Styles,
56 root: Vec<usize>,
58 editing: &'a mut Editing,
59 scope: Vec<usize>,
62 tag: Vec<usize>,
65 order: OnceCell<Vec<Vec<usize>>>,
68}
69
70impl<'a> FlowModel<'a> {
71 pub fn new(
74 content: &'a mut Element,
75 styles: &'a mut Styles,
76 root: Vec<usize>,
77 editing: &'a mut Editing,
78 ) -> Self {
79 Self {
80 content,
81 styles,
82 root,
83 editing,
84 scope: Vec::new(),
85 tag: Vec::new(),
86 order: OnceCell::new(),
87 }
88 }
89
90 #[must_use]
93 pub fn tagged(mut self, tag: Vec<usize>) -> Self {
94 self.tag = tag;
95 self
96 }
97
98 #[must_use]
102 pub fn within(mut self, scope: Vec<usize>) -> Self {
103 self.scope = scope;
104 self
105 }
106
107 fn root(&self) -> Option<&Element> {
108 self.content.at(&self.root)
109 }
110
111 fn paragraph_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
112 self.content
113 .at_mut(&self.root)?
114 .at_mut(path)
115 .filter(|element| is_paragraph(element))
116 }
117
118 fn order(&self) -> &[Vec<usize>] {
119 self.order.get_or_init(|| {
120 let mut out = self.root().map(paragraph_paths).unwrap_or_default();
121 out.retain(|path| path.starts_with(&self.scope));
122 out
123 })
124 }
125
126 fn neighbour(&self, paragraph: &[usize], step: isize) -> Option<Vec<usize>> {
127 let order = self.order();
128 let index = order.iter().position(|p| p == paragraph)?;
129 order.get(index.checked_add_signed(step)?).cloned()
130 }
131
132 fn replace(
133 &mut self,
134 from: &Position<Vec<usize>>,
135 to: &Position<Vec<usize>>,
136 text: &str,
137 ) -> Option<()> {
138 let root = self.content.at_mut(&self.root)?;
139 match edit::replace_range(
140 root,
141 (&from.paragraph, from.offset),
142 (&to.paragraph, to.offset),
143 text,
144 ) {
145 Ok(()) => Some(()),
146 Err(Refused::Structure) => self.clear_between(from, to, text),
147 Err(_) => None,
148 }
149 }
150
151 fn clear_between(
157 &mut self,
158 from: &Position<Vec<usize>>,
159 to: &Position<Vec<usize>>,
160 text: &str,
161 ) -> Option<()> {
162 let ranges = self.covered(from, to)?;
163 if text.is_empty() && ranges.iter().all(|(_, range)| range.is_empty()) {
164 return None;
165 }
166 for (path, range) in ranges.into_iter().rev() {
167 let with = if path == from.paragraph { text } else { "" };
168 let paragraph = self.paragraph_mut(&path)?;
169 edit::replace(paragraph, range, with);
170 }
171 Some(())
172 }
173
174 fn covered(
177 &self,
178 from: &Position<Vec<usize>>,
179 to: &Position<Vec<usize>>,
180 ) -> Option<Vec<(Vec<usize>, Range<usize>)>> {
181 let order = self.order();
182 let first = order.iter().position(|p| *p == from.paragraph)?;
183 let last = order.iter().position(|p| *p == to.paragraph)?;
184 let covered: Vec<Vec<usize>> = order.get(first..=last)?.to_vec();
185 Some(
186 covered
187 .into_iter()
188 .map(|path| {
189 let len = self.text(&path).map_or(0, |t| t.chars().count());
190 let start = if path == from.paragraph {
191 from.offset
192 } else {
193 0
194 };
195 let end = if path == to.paragraph { to.offset } else { len };
196 (path, start..end.max(start))
197 })
198 .collect(),
199 )
200 }
201
202 fn selected_paragraphs(&self, selection: &Selection<Vec<usize>>) -> Option<Vec<Vec<usize>>> {
204 let order = self.order();
205 let anchor = order
206 .iter()
207 .position(|p| *p == selection.anchor.paragraph)?;
208 let focus = order.iter().position(|p| *p == selection.focus.paragraph)?;
209 Some(order[anchor.min(focus)..=anchor.max(focus)].to_vec())
210 }
211
212 pub fn is_block(&self, selection: &Selection<Vec<usize>>, block: Block) -> bool {
215 let Some(paragraphs) = self.selected_paragraphs(selection) else {
216 return false;
217 };
218 paragraphs.iter().all(|paragraph| {
219 let path = [self.root.as_slice(), paragraph].concat();
220 let (heading, list) = edit::block_state(self.content, &path, self.styles);
221 match block {
222 Block::Heading(level) => heading == Some(level),
223 Block::Body => heading.is_none(),
224 Block::Bullets => list == Some(ListKind::Bullet),
225 Block::Numbers => list == Some(ListKind::Number),
226 }
227 })
228 }
229
230 pub fn apply_block(&mut self, editor: &mut PageEditor, block: Block) -> bool {
235 let Some(selection) = editor.selection().cloned() else {
236 return false;
237 };
238 let Some(paragraphs) = self.selected_paragraphs(&selection) else {
239 return false;
240 };
241 let place = |path: &Vec<usize>, order: &[Vec<usize>]| order.iter().position(|p| p == path);
242 let (Some(anchor), Some(focus)) = (
243 place(&selection.anchor.paragraph, self.order()),
244 place(&selection.focus.paragraph, self.order()),
245 ) else {
246 return false;
247 };
248 let lit = self.is_block(&selection, block);
249 let before = self.content.clone();
250 let done = match block {
251 Block::Heading(_) | Block::Body => {
252 let level = match block {
253 Block::Heading(level) if !lit => Some(level),
254 _ => None,
255 };
256 self.set_headings(¶graphs, level)
257 }
258 Block::Bullets | Block::Numbers => {
259 let kind = match block {
260 Block::Bullets => ListKind::Bullet,
261 _ => ListKind::Number,
262 };
263 self.set_lists(¶graphs, (!lit).then_some(kind))
264 }
265 };
266 if done.is_none() || *self.content == before {
267 *self.content = before;
268 return false;
269 }
270 self.order = OnceCell::new();
271 let begins = Position::new(
272 [self.tag.as_slice(), &selection.focus.paragraph].concat(),
273 selection.focus.offset,
274 );
275 self.editing.touch();
276 self.editing.record_snapshot(before, Some(begins));
277 let order = self.order().to_vec();
278 if let (Some(a), Some(f)) = (order.get(anchor), order.get(focus)) {
279 editor.select(Selection {
280 anchor: Position::new(a.clone(), selection.anchor.offset),
281 focus: Position::new(f.clone(), selection.focus.offset),
282 });
283 }
284 true
285 }
286
287 fn set_headings(&mut self, paragraphs: &[Vec<usize>], level: Option<u8>) -> Option<()> {
288 let automatic =
289 |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
290 for paragraph in paragraphs {
291 let had = automatic(self.content);
292 let path = [self.root.as_slice(), paragraph].concat();
293 edit::set_heading(self.content, &path, level, self.styles).ok()?;
294 self.follow_styles(had);
295 }
296 Some(())
297 }
298
299 fn set_lists(&mut self, paragraphs: &[Vec<usize>], kind: Option<ListKind>) -> Option<()> {
300 let had = self
301 .content
302 .child(&Ns::Office, "automatic-styles")
303 .is_some();
304 let paths: Vec<Vec<usize>> = paragraphs
305 .iter()
306 .map(|paragraph| [self.root.as_slice(), paragraph].concat())
307 .collect();
308 edit::set_list(self.content, &paths, kind, self.styles).ok()?;
309 self.follow_styles(had);
310 Some(())
311 }
312
313 fn follow_styles(&mut self, had: bool) {
316 if !had
317 && self
318 .content
319 .child(&Ns::Office, "automatic-styles")
320 .is_some()
321 && let Some(first) = self.root.first_mut()
322 {
323 *first += 1;
324 }
325 }
326
327 fn format(
330 &mut self,
331 from: &Position<Vec<usize>>,
332 to: &Position<Vec<usize>>,
333 mark: edit::Mark,
334 on: bool,
335 ) -> Option<()> {
336 let automatic =
337 |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
338 for (path, range) in self.covered(from, to)? {
339 if range.is_empty() {
340 continue;
341 }
342 let had = automatic(self.content);
343 let mut at = self.root.clone();
344 at.extend(&path);
345 edit::format(self.content, &at, range, mark, on, self.styles).ok()?;
346 if !had
349 && automatic(self.content)
350 && let Some(first) = self.root.first_mut()
351 {
352 *first += 1;
353 }
354 }
355 Some(())
356 }
357}
358
359fn core_mark(mark: Mark) -> edit::Mark {
361 match mark {
362 Mark::Bold => edit::Mark::Bold,
363 Mark::Italic => edit::Mark::Italic,
364 Mark::Underline => edit::Mark::Underline,
365 Mark::Strike => edit::Mark::Strike,
366 }
367}
368
369impl Model for FlowModel<'_> {
370 type Paragraph = Vec<usize>;
371
372 fn text(&self, paragraph: &Vec<usize>) -> Option<String> {
373 self.root()?
374 .at(paragraph)
375 .filter(|element| is_paragraph(element))
376 .map(edit::text)
377 }
378
379 fn next(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
380 self.neighbour(paragraph, 1)
381 }
382
383 fn previous(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
384 self.neighbour(paragraph, -1)
385 }
386
387 fn first(&self) -> Option<Vec<usize>> {
388 self.order().first().cloned()
389 }
390
391 fn last(&self) -> Option<Vec<usize>> {
392 self.order().last().cloned()
393 }
394
395 fn marked(
396 &self,
397 from: &Position<Vec<usize>>,
398 to: &Position<Vec<usize>>,
399 mark: Mark,
400 ) -> Option<bool> {
401 let root = self.root()?;
402 let mark = core_mark(mark);
403 let ranges: Vec<_> = self
404 .covered(from, to)?
405 .into_iter()
406 .filter(|(_, range)| !range.is_empty())
407 .collect();
408 if ranges.is_empty() {
409 let at = from.offset..from.offset;
410 return edit::marked(root.at(&from.paragraph)?, at, mark, self.styles);
411 }
412 let mut answer = None;
413 for (path, range) in ranges {
414 let value = edit::marked(root.at(&path)?, range, mark, self.styles)?;
415 if answer.is_some_and(|a| a != value) {
416 return None;
417 }
418 answer = Some(value);
419 }
420 answer
421 }
422
423 fn apply(
424 &mut self,
425 edit: Edit<'_, Vec<usize>>,
426 new_step: bool,
427 ) -> Option<Position<Vec<usize>>> {
428 let new_step = new_step || !self.editing.modified();
432 let before = new_step.then(|| self.content.clone());
436 let begins = match &edit {
437 Edit::Replace { from, .. } | Edit::Format { from, .. } => from,
438 Edit::Split { at } => at,
439 };
440 let begins = Position::new(
441 [self.tag.as_slice(), &begins.paragraph].concat(),
442 begins.offset,
443 );
444 let at = match edit {
445 Edit::Replace { from, to, text } => self
446 .replace(&from, &to, text)
447 .map(|()| Position::new(from.paragraph, from.offset + text.chars().count())),
448 Edit::Split { at } => {
449 let root = self.content.at_mut(&self.root)?;
450 edit::split_at(root, &at.paragraph, at.offset)
451 .ok()
452 .map(|second| Position::new(second, 0))
453 }
454 Edit::Format { from, to, mark, on } => {
455 self.format(&from, &to, core_mark(mark), on).map(|()| to)
456 }
457 };
458 if at.is_some() {
459 self.editing.touch();
460 if let Some(before) = before {
461 self.editing.record_snapshot(before, Some(begins));
462 }
463 self.order = OnceCell::new();
464 }
465 at
466 }
467}
468
469pub(crate) fn paragraph_paths(root: &Element) -> Vec<Vec<usize>> {
472 let mut out = Vec::new();
473 blocks(root, &mut Vec::new(), &mut out);
474 out
475}
476
477fn blocks(parent: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
480 for (index, element) in parent.elements_indexed() {
481 path.push(index);
482 block(element, path, out);
483 path.pop();
484 }
485}
486
487fn block(element: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
488 if is_paragraph(element) {
489 out.push(path.clone());
490 } else if element.is(&Ns::Text, "list") {
491 for (item_index, item) in element.elements_indexed() {
492 if item.is(&Ns::Text, "list-item") || item.is(&Ns::Text, "list-header") {
493 path.push(item_index);
494 blocks(item, path, out);
495 path.pop();
496 }
497 }
498 } else if element.is(&Ns::Table, "table")
499 || element.is(&Ns::Table, "table-header-rows")
500 || element.is(&Ns::Table, "table-rows")
501 || element.is(&Ns::Table, "table-row-group")
502 {
503 blocks(element, path, out);
504 } else if element.is(&Ns::Table, "table-row") {
505 for (cell_index, cell) in element.elements_indexed() {
506 if cell.is(&Ns::Table, "table-cell") {
507 path.push(cell_index);
508 blocks(cell, path, out);
509 path.pop();
510 }
511 }
512 } else if element.is(&Ns::Draw, "frame") {
513 if let Some((box_index, text_box)) = element
514 .elements_indexed()
515 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
516 {
517 path.push(box_index);
518 blocks(text_box, path, out);
519 path.pop();
520 }
521 } else if element.name.ns == Ns::Draw {
522 blocks(element, path, out);
525 } else if is_block_container(element) {
526 blocks(element, path, out);
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::FlowModel;
533 use crate::Editing;
534 use egui_richedit::{Edit, Model, Position};
535 use odox_core::{Element, Node, Ns, Styles};
536
537 fn element(prefix: &str, local: &str, ns: Ns, children: Vec<Node>) -> Node {
538 let mut e = Element::new(prefix, local, ns);
539 e.children = children;
540 Node::Element(e)
541 }
542
543 fn p(text: &str) -> Node {
544 element("text", "p", Ns::Text, vec![Node::Text(text.to_owned())])
545 }
546
547 fn content() -> Element {
550 let list = element(
551 "text",
552 "list",
553 Ns::Text,
554 vec![element("text", "list-item", Ns::Text, vec![p("item")])],
555 );
556 let table = element(
557 "table",
558 "table",
559 Ns::Table,
560 vec![element(
561 "table",
562 "table-row",
563 Ns::Table,
564 vec![element("table", "table-cell", Ns::Table, vec![p("cell")])],
565 )],
566 );
567 let body = element(
568 "office",
569 "text",
570 Ns::Office,
571 vec![p("one"), list, table, p("two")],
572 );
573 let mut root = Element::new("office", "document-content", Ns::Office);
574 root.children = vec![element("office", "body", Ns::Office, vec![body])];
575 root
576 }
577
578 #[test]
579 fn paragraphs_follow_one_another_through_lists_and_tables() {
580 let mut content = content();
581 let mut styles = Styles::collect(Some(&content), None);
582 let mut editing = Editing::default();
583 let model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
584 let mut at = vec![0];
585 let mut seen = vec![model.text(&at).expect("the first")];
586 while let Some(next) = model.next(&at) {
587 seen.push(model.text(&next).expect("a paragraph"));
588 assert_eq!(model.previous(&next), Some(at));
589 at = next;
590 }
591 assert_eq!(seen, ["one", "item", "cell", "two"]);
592 }
593
594 #[test]
595 fn a_selection_over_a_whole_table_takes_it_and_joins_the_ends() {
596 let mut content = content();
597 let mut styles = Styles::collect(Some(&content), None);
598 let mut editing = Editing::default();
599 editing.reset();
600 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
601 let at = model.apply(
603 Edit::Replace {
604 from: Position::new(vec![1, 0, 0], 2),
605 to: Position::new(vec![3], 1),
606 text: "X",
607 },
608 true,
609 );
610 assert_eq!(at, Some(Position::new(vec![1, 0, 0], 3)));
611 assert_eq!(model.text(&vec![1, 0, 0]).as_deref(), Some("itXwo"));
612 assert_eq!(
613 model.next(&vec![1, 0, 0]),
614 None,
615 "the table and \"two\" are gone"
616 );
617 }
618
619 #[test]
620 fn a_selection_out_of_a_cell_takes_the_text_and_leaves_the_cell() {
621 let mut content = content();
622 let mut styles = Styles::collect(Some(&content), None);
623 let mut editing = Editing::default();
624 editing.reset();
625 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
626 let at = model.apply(
628 Edit::Replace {
629 from: Position::new(vec![2, 0, 0, 0], 2),
630 to: Position::new(vec![3], 1),
631 text: "",
632 },
633 true,
634 );
635 assert_eq!(at, Some(Position::new(vec![2, 0, 0, 0], 2)));
636 assert_eq!(model.text(&vec![2, 0, 0, 0]).as_deref(), Some("ce"));
637 assert_eq!(model.text(&vec![3]).as_deref(), Some("wo"));
638 }
639
640 #[test]
641 fn backspace_at_a_list_item_joins_it_to_the_paragraph_before() {
642 let mut content = content();
643 let mut styles = Styles::collect(Some(&content), None);
644 let mut editing = Editing::default();
645 editing.reset();
646 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
647 let at = model.apply(
648 Edit::Replace {
649 from: Position::new(vec![0], 3),
650 to: Position::new(vec![1, 0, 0], 0),
651 text: "",
652 },
653 true,
654 );
655 assert_eq!(at, Some(Position::new(vec![0], 3)));
656 assert_eq!(model.text(&vec![0]).as_deref(), Some("oneitem"));
657 assert_eq!(model.text(&vec![1, 0, 0, 0]).as_deref(), Some("cell"));
659 }
660
661 #[test]
662 fn a_tagged_model_remembers_the_caret_under_its_tag() {
663 let mut content = content();
664 let mut styles = Styles::collect(Some(&content), None);
665 let mut editing = Editing::default();
666 editing.reset();
667 let mut model =
668 FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing).tagged(vec![7]);
669 model.apply(
670 Edit::Split {
671 at: Position::new(vec![0], 1),
672 },
673 true,
674 );
675 let current = content.clone();
676 let (_, caret) = editing.undo(¤t, None).expect("a step to undo");
677 assert_eq!(caret, Some(Position::new(vec![7, 0], 1)));
678 }
679
680 #[test]
681 fn typing_that_runs_on_past_a_save_is_a_change() {
682 let mut content = content();
683 let mut styles = Styles::collect(Some(&content), None);
684 let mut editing = Editing::default();
685 editing.reset();
686 let at = |offset| Position::new(vec![0], offset);
687 let typed = |offset| Edit::Replace {
688 from: at(offset),
689 to: at(offset),
690 text: "x",
691 };
692 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
693 model.apply(typed(0), true);
694 model.editing.mark_saved();
695 model.apply(typed(1), false);
697 assert!(
698 editing.modified(),
699 "what was typed after the save is unsaved"
700 );
701 }
702
703 #[test]
704 fn an_edit_that_begins_a_step_is_recorded_once_it_succeeds() {
705 let mut content = content();
706 let mut styles = Styles::collect(Some(&content), None);
707 let mut editing = Editing::default();
708 editing.reset();
709 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
710 let at = model.apply(
711 Edit::Split {
712 at: Position::new(vec![0], 1),
713 },
714 true,
715 );
716 assert_eq!(at, Some(Position::new(vec![1], 0)));
717 assert_eq!(model.text(&vec![0]).as_deref(), Some("o"));
718 assert_eq!(model.text(&vec![1]).as_deref(), Some("ne"));
719
720 let refused = model.apply(
723 Edit::Replace {
724 from: Position::new(vec![2, 0, 0], 4),
725 to: Position::new(vec![3, 0, 0, 0], 0),
726 text: "",
727 },
728 true,
729 );
730 assert_eq!(refused, None);
731 assert!(editing.can_undo());
732 let mut undone = 0;
733 let mut current = content.clone();
734 while let Some(previous) = editing.undo(¤t, None).map(|(tree, _)| tree) {
735 current = previous;
736 undone += 1;
737 }
738 assert_eq!(undone, 1);
739 }
740}