1use std::collections::HashMap;
17use std::sync::Arc;
18
19use eframe::egui::accesskit::Role;
20use eframe::egui::emath::TSTransform;
21use eframe::egui::text_selection::accesskit_text::update_accesskit_for_text_widget;
22use eframe::egui::{
23 Align, Color32, ColorImage, Context, CursorIcon, Galley, Pos2, Rect, Response, Sense, Stroke,
24 StrokeKind, TextFormat, TextureHandle, TextureOptions, Ui, pos2,
25 text::{CCursor, CCursorRange, LayoutJob},
26 text_selection::{LabelSelectionState, visuals::paint_text_selection},
27 vec2,
28};
29use egui_richedit::{Laid, OffsetMap, ParagraphJob};
30use odox_core::{
31 Border, Document, Element, Family, Node, Ns, Properties, TextAlign, TextProperties, edit,
32};
33
34use crate::find::Highlights;
35use crate::flow_model::PageEditor;
36use crate::format::{self, DEFAULT_SIZE};
37use crate::links;
38
39#[derive(Default)]
44pub struct Pictures {
45 textures: HashMap<String, Option<TextureHandle>>,
46}
47
48impl Pictures {
49 pub fn get(
51 &mut self,
52 ctx: &Context,
53 document: &Document,
54 href: &str,
55 ) -> Option<&TextureHandle> {
56 if !self.textures.contains_key(href) {
57 let texture = document
58 .picture(href)
59 .and_then(decode)
60 .map(|image| ctx.load_texture(href, image, TextureOptions::LINEAR));
61 self.textures.insert(href.to_owned(), texture);
62 }
63 self.textures.get(href).and_then(Option::as_ref)
64 }
65
66 pub fn clear(&mut self) {
68 self.textures.clear();
69 }
70}
71
72fn decode(bytes: &[u8]) -> Option<ColorImage> {
73 let decoded = image::load_from_memory(bytes).ok()?.to_rgba8();
74 let size = [decoded.width() as usize, decoded.height() as usize];
75 Some(ColorImage::from_rgba_unmultiplied(size, decoded.as_raw()))
76}
77
78pub struct Flow<'a> {
80 pub document: &'a Document,
82 pub pictures: &'a mut Pictures,
84 pub zoom: f32,
86 pub palette: format::Palette,
88 pub scroll_to_heading: Option<usize>,
94 pub scroll_to_place: Option<String>,
98 pub followed: Option<String>,
101 headings_seen: usize,
103 pub page: Option<&'a mut PageEditor>,
106 pub find: Option<Highlights<'a>>,
108 pub selectable: bool,
111 path: Vec<usize>,
114 detached: bool,
119}
120
121impl<'a> Flow<'a> {
122 pub fn new(document: &'a Document, pictures: &'a mut Pictures, zoom: f32) -> Self {
124 Self {
125 document,
126 pictures,
127 zoom,
128 palette: format::Palette::default(),
129 scroll_to_heading: None,
130 scroll_to_place: None,
131 followed: None,
132 headings_seen: 0,
133 page: None,
134 find: None,
135 selectable: true,
136 path: Vec::new(),
137 detached: false,
138 }
139 }
140
141 pub fn start_at(&mut self, prefix: Vec<usize>) {
145 self.path = prefix;
146 }
147}
148
149const LIST_STEP: f32 = 18.0;
151const LIST_GUTTER: f32 = 20.0;
153const LABEL_GAP: f32 = 5.0;
155
156struct Spacing {
158 left: f32,
160 right: f32,
162 indent: f32,
164 before: f32,
166 after: f32,
168}
169
170impl Spacing {
171 fn of(properties: &odox_core::ParagraphProperties, zoom: f32) -> Self {
172 let points = |length: Option<odox_core::Length>| {
173 length.map_or(0.0, odox_core::Length::points) * zoom
174 };
175 Self {
176 left: points(properties.margin.left),
177 right: points(properties.margin.right),
178 indent: points(properties.text_indent),
179 before: points(properties.margin.top),
180 after: points(properties.margin.bottom),
181 }
182 }
183}
184
185#[derive(Clone, Copy)]
190struct Run<'a> {
191 inherited: &'a TextProperties,
193 size: f32,
196 format: &'a TextFormat,
198 palette: format::Palette,
200 counted: bool,
203}
204
205#[derive(Default)]
207struct Counters(Vec<usize>);
208
209impl Counters {
210 fn bump(&mut self, level: usize, start: usize) -> usize {
211 while self.0.len() <= level {
212 self.0.push(0);
213 }
214 if self.0[level] == 0 {
217 self.0[level] = start;
218 } else {
219 self.0[level] += 1;
220 }
221 self.0.truncate(level + 1);
224 self.0[level]
225 }
226
227 fn at(&self, level: usize) -> usize {
228 self.0.get(level).copied().unwrap_or(1)
229 }
230}
231
232impl Flow<'_> {
233 pub fn blocks(&mut self, ui: &mut Ui, parent: &Element, width: f32) {
238 let mut counters = Counters::default();
239 self.blocks_with(ui, parent, width, &mut counters);
240 }
241
242 fn blocks_with(&mut self, ui: &mut Ui, parent: &Element, width: f32, counters: &mut Counters) {
243 for (index, element) in parent.elements_indexed() {
244 self.path.push(index);
245 match () {
246 () if element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h") => {
247 self.paragraph(ui, element, width, None);
248 }
249 () if element.is(&Ns::Text, "list") => {
250 let continues = element.attr(&Ns::Text, "continue-numbering") == Some("true")
255 || element.attr(&Ns::Text, "continue-list").is_some();
256 if !continues {
257 *counters = Counters::default();
258 }
259 self.list(ui, element, width, 0, None, counters);
260 }
261 () if element.is(&Ns::Table, "table") => self.table(ui, element, width),
262 () if element.is(&Ns::Draw, "frame") => self.frame(ui, element, width),
263 () if element.is(&Ns::Text, "soft-page-break") => self.page_break(ui, width),
264 () if edit::is_block_container(element) => {
265 self.blocks_with(ui, element, width, counters);
266 }
267 () => {}
268 }
269 self.path.pop();
270 }
271 }
272
273 fn style_of(&self, element: &Element, family: &Family) -> std::rc::Rc<Properties> {
275 let name = element
276 .attr(&Ns::Text, "style-name")
277 .or_else(|| element.attr(&Ns::Table, "style-name"))
278 .unwrap_or("Standard");
279 self.document.styles.resolve(family, name)
280 }
281
282 fn show_matches(&self, ui: &Ui, galley: &mut Arc<Galley>, map: &OffsetMap, origin: Pos2) {
285 let Some(find) = self.find.as_ref().filter(|_| !self.detached) else {
286 return;
287 };
288 let hits = find.within(&self.path);
289 if hits.is_empty() {
290 return;
291 }
292 let at = |offset: usize| CCursor::new(map.to_galley(offset.min(map.model_len())));
293 for (range, current) in hits {
294 let mut visuals = ui.visuals().clone();
295 visuals.selection.bg_fill = format::match_fill(current);
296 visuals.selection.stroke.color = Color32::BLACK;
297 let cursors = CCursorRange::two(at(range.start), at(range.end));
298 paint_text_selection(galley, &visuals, &cursors, None);
299 if current && find.reveal() {
300 let start = galley.pos_from_cursor(at(range.start));
301 let end = galley.pos_from_cursor(at(range.end));
302 ui.scroll_to_rect(
303 start.union(end).translate(origin.to_vec2()),
304 Some(Align::Center),
305 );
306 }
307 }
308 }
309
310 #[allow(clippy::too_many_arguments)]
314 fn follow_link(
315 &mut self,
316 ui: &Ui,
317 response: &Response,
318 links: &[(std::ops::Range<usize>, &str)],
319 galley: &Galley,
320 map: &OffsetMap,
321 origin: Pos2,
322 editing: bool,
323 ) {
324 let Some(pointer) = response
325 .hover_pos()
326 .or_else(|| response.interact_pointer_pos())
327 else {
328 return;
329 };
330 let cursor = galley.cursor_from_pos(pointer - origin);
331 let caret = galley.pos_from_cursor(cursor).center().x + origin.x;
332 let character = if pointer.x < caret {
333 cursor.index.0.saturating_sub(1)
334 } else {
335 cursor.index.0
336 };
337 let offset = map.to_model(character);
338 let Some(href) = links
339 .iter()
340 .find(|(range, _)| range.contains(&offset))
341 .map(|&(_, href)| href)
342 else {
343 return;
344 };
345 let Some(target) = links::target(href) else {
346 return;
347 };
348 let reaching = !editing || ui.input(|input| input.modifiers.command);
349 if reaching {
350 ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
351 }
352 if response.hovered() {
353 response.clone().on_hover_text(href);
354 }
355 if reaching && response.clicked() {
356 match target {
357 links::Target::External(address) => links::open(ui.ctx(), address),
358 links::Target::Within(place) => self.followed = Some(place.to_owned()),
359 }
360 }
361 }
362
363 fn paragraph(&mut self, ui: &mut Ui, element: &Element, width: f32, label: Option<&str>) {
365 let properties = self.style_of(element, &Family::Paragraph);
366 let zoom = self.zoom;
367 let size = format::size_of(&properties.text, DEFAULT_SIZE);
368
369 let Spacing {
370 left,
371 right,
372 indent,
373 before,
374 after,
375 } = Spacing::of(&properties.paragraph, zoom);
376
377 let body_width = (width - left - right).max(1.0);
381 let wrap = (body_width - indent.max(0.0)).max(1.0);
382
383 if before > 0.0 {
384 ui.add_space(before);
385 }
386
387 let base = format::text_format(&properties.text, DEFAULT_SIZE, zoom, self.palette);
388
389 let (job, frames) = self.layout_job(element, &properties, &base, size, wrap);
390 let (job, map) = job.into_parts();
391
392 let galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(job));
393 let height = galley.size().y;
394 let edited = !self.detached && self.page.is_some();
395 let sense = if self.selectable {
404 Sense::click_and_drag()
405 } else {
406 Sense::click()
407 };
408 let (rect, response) = ui.allocate_exact_size(vec2(width, height), sense);
409
410 if element.is(&Ns::Text, "h") {
411 if self.scroll_to_heading == Some(self.headings_seen) {
412 ui.scroll_to_rect(rect, Some(Align::TOP));
413 }
414 self.headings_seen += 1;
415 }
416 if let Some(place) = &self.scroll_to_place
417 && holds_place(element, place)
418 {
419 ui.scroll_to_rect(rect, Some(Align::TOP));
420 }
421
422 if let Some(fill) = properties.paragraph.background {
423 ui.painter().rect_filled(rect, 0.0, format::color32(fill));
424 }
425 paint_borders(ui, rect, &properties.paragraph.border, zoom);
426
427 let anchor = match properties.paragraph.align.unwrap_or(TextAlign::Start) {
431 TextAlign::Center => rect.left() + left + indent.max(0.0) + wrap / 2.0,
432 TextAlign::End => rect.left() + left + indent.max(0.0) + wrap,
433 _ => rect.left() + left + indent.max(0.0),
434 };
435 let origin = pos2(anchor, rect.top());
444 let mut galley = galley;
445 self.show_matches(ui, &mut galley, &map, origin);
446 let readable = Arc::clone(&galley);
447 let links = edit::links(element);
448 let hit = (!links.is_empty()).then(|| (Arc::clone(&galley), map.clone()));
449 if let Some(page) = self.page.as_deref_mut().filter(|_| edited) {
450 let laid = Laid {
451 galley,
452 map,
453 origin,
454 };
455 page.paragraph(ui, &response, &self.path, laid);
456 } else {
457 LabelSelectionState::label_text_selection(
458 ui,
459 &response,
460 origin,
461 galley,
462 base.color,
463 Stroke::NONE,
464 );
465 }
466
467 expose(ui, &response, element, &readable, origin, edited);
468
469 if let Some((galley, map)) = hit {
470 self.follow_link(ui, &response, &links, &galley, &map, origin, edited);
471 }
472
473 if let Some(label) = label {
474 let mut label_job = LayoutJob::default();
475 label_job.append(label, 0.0, base.clone());
476 let label_galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(label_job));
477 let x = rect.left() + left - label_galley.size().x - LABEL_GAP * zoom;
481 ui.painter()
482 .galley(pos2(x, rect.top()), label_galley, base.color);
483 }
484
485 let was_detached = self.detached;
488 self.detached = true;
489 for frame in frames {
490 self.frame(ui, &frame, body_width);
491 }
492 self.detached = was_detached;
493 if after > 0.0 {
494 ui.add_space(after);
495 }
496 }
497
498 fn layout_job(
501 &self,
502 element: &Element,
503 properties: &Properties,
504 base: &TextFormat,
505 size: f32,
506 wrap: f32,
507 ) -> (ParagraphJob, Vec<Element>) {
508 let job = LayoutJob {
509 wrap: eframe::egui::text::TextWrapping {
510 max_width: wrap,
511 ..Default::default()
512 },
513 halign: match properties.paragraph.align.unwrap_or(TextAlign::Start) {
514 TextAlign::Center => Align::Center,
515 TextAlign::End => Align::Max,
516 _ => Align::Min,
517 },
518 justify: properties.paragraph.align == Some(TextAlign::Justify),
519 ..LayoutJob::default()
520 };
521 let mut job = ParagraphJob::new(job);
522 let mut frames = Vec::new();
523 let run = Run {
524 inherited: &properties.text,
525 size,
526 format: base,
527 palette: self.palette,
528 counted: true,
529 };
530 self.runs(element, &run, &mut job, &mut frames);
531
532 if job.is_empty() {
535 job.atom(" ", 0, base.clone());
536 }
537 if let Some(measure) = properties.paragraph.line_height {
538 for format in job.formats_mut() {
542 let own = format.font_id.size / self.zoom;
543 format.line_height = Some(measure.resolve(own) * self.zoom);
544 }
545 }
546 (job, frames)
547 }
548
549 fn runs(
561 &self,
562 parent: &Element,
563 run: &Run<'_>,
564 job: &mut ParagraphJob,
565 frames: &mut Vec<Element>,
566 ) {
567 let Run {
568 inherited,
569 size,
570 format,
571 palette,
572 counted,
573 } = *run;
574 let put = |job: &mut ParagraphJob, shown: &str, model_len: usize| {
575 if !counted {
576 job.atom(shown, 0, format.clone());
577 } else if shown.chars().count() == model_len {
578 job.text(shown, format.clone());
579 } else {
580 job.atom(shown, model_len, format.clone());
581 }
582 };
583 for child in &parent.children {
584 match child {
585 Node::Text(text) | Node::CData(text) => put(job, text, text.chars().count()),
586 Node::Comment(_) | Node::ProcessingInstruction(_) => {}
587 Node::Element(element) => {
588 if element.is(&Ns::Text, "s") {
589 let count = element.attr_usize(&Ns::Text, "c").unwrap_or(1);
590 put(job, &" ".repeat(count.min(256)), count);
591 } else if element.is(&Ns::Text, "tab") {
592 put(job, " ", 1);
597 } else if element.is(&Ns::Text, "line-break") {
598 put(job, "\n", 1);
599 } else if element.is(&Ns::Text, "span") {
600 let style = self.style_of(element, &Family::Text);
601 let merged = style.text.over(inherited);
602 let inner_size = format::size_of(&merged, size);
603 let inner = format::text_format(&merged, size, self.zoom, palette);
604 let run = Run {
605 inherited: &merged,
606 size: inner_size,
607 format: &inner,
608 palette,
609 counted,
610 };
611 self.runs(element, &run, job, frames);
612 } else if element.is(&Ns::Text, "a") {
613 let style = self.style_of(element, &Family::Text);
614 let merged = style.text.over(inherited);
615 let inner_size = format::size_of(&merged, size);
616 let inner = format::link_format(&merged, size, self.zoom, palette);
617 let run = Run {
618 inherited: &merged,
619 size: inner_size,
620 format: &inner,
621 palette,
622 counted,
623 };
624 self.runs(element, &run, job, frames);
625 } else if element.is(&Ns::Draw, "frame") {
626 frames.push(element.clone());
627 } else if element.is(&Ns::Text, "note") {
628 if let Some(citation) = element.child(&Ns::Text, "note-citation") {
632 let mut raised = format.clone();
633 raised.font_id.size *= 0.7;
634 raised.valign = Align::TOP;
635 job.atom(&citation.plain_text(), 0, raised);
636 }
637 } else if edit::is_inline_passthrough(element) {
638 let run = Run {
642 counted: counted && edit::holds_text(element),
643 ..*run
644 };
645 self.runs(element, &run, job, frames);
646 }
647 }
648 }
649 }
650 }
651
652 fn list(
654 &mut self,
655 ui: &mut Ui,
656 element: &Element,
657 width: f32,
658 level: usize,
659 inherited_style: Option<&str>,
660 counters: &mut Counters,
661 ) {
662 let style_name = element
663 .attr(&Ns::Text, "style-name")
664 .or(inherited_style)
665 .unwrap_or_default()
666 .to_owned();
667 #[allow(clippy::cast_precision_loss)]
671 let indent = LIST_STEP * self.zoom * (level as f32 + 1.0);
672 let gutter = LIST_GUTTER * self.zoom;
673
674 for (item_index, item) in element.elements_indexed() {
675 let numbered = item.is(&Ns::Text, "list-item");
676 if !numbered && !item.is(&Ns::Text, "list-header") {
677 continue;
678 }
679 self.path.push(item_index);
680 let label = if numbered {
681 let level_style = self.level_style(&style_name, level);
682 let start = level_style
683 .as_ref()
684 .and_then(|s| s.attr_usize(&Ns::Text, "start-value"))
685 .unwrap_or(1);
686 let number = counters.bump(level, start);
687 Self::label(level_style.as_ref(), level, number, counters)
688 } else {
689 String::new()
692 };
693
694 let mut first = true;
695 for (block_index, block) in item.elements_indexed() {
696 self.path.push(block_index);
697 if block.is(&Ns::Text, "list") {
698 self.list(ui, block, width, level + 1, Some(&style_name), counters);
699 self.path.pop();
700 continue;
701 }
702 let on_this_block = if first && !label.is_empty() {
703 Some(label.as_str())
704 } else {
705 None
706 };
707 if block.is(&Ns::Text, "p") || block.is(&Ns::Text, "h") {
708 first = false;
709 ui.horizontal_top(|ui| {
713 ui.add_space(indent + gutter);
714 self.paragraph(ui, block, width - indent - gutter, on_this_block);
715 });
716 } else if block.is(&Ns::Table, "table") {
717 first = false;
718 ui.horizontal_top(|ui| {
719 ui.add_space(indent + gutter);
720 self.table(ui, block, width - indent - gutter);
721 });
722 } else if block.is(&Ns::Draw, "frame") {
723 first = false;
724 ui.horizontal_top(|ui| {
725 ui.add_space(indent + gutter);
726 self.frame(ui, block, width - indent - gutter);
727 });
728 }
729 self.path.pop();
730 }
731 self.path.pop();
732 }
733 }
734
735 fn level_style(&self, list_style: &str, level: usize) -> Option<Element> {
738 let style = self.document.styles.list_style(list_style)?;
739 style
740 .elements()
741 .find(|e| e.attr_usize(&Ns::Text, "level") == Some(level + 1))
742 .cloned()
743 }
744
745 fn label(
747 level_style: Option<&Element>,
748 level: usize,
749 number: usize,
750 counters: &Counters,
751 ) -> String {
752 let Some(style) = level_style else {
753 return "\u{2022}".to_owned();
756 };
757 if style.is(&Ns::Text, "list-level-style-bullet") {
758 return style
759 .attr(&Ns::Text, "bullet-char")
760 .unwrap_or("\u{2022}")
761 .to_owned();
762 }
763 if style.is(&Ns::Text, "list-level-style-number") {
764 let format = style.attr(&Ns::Style, "num-format").unwrap_or("1");
765 let prefix = style.attr(&Ns::Style, "num-prefix").unwrap_or_default();
766 let suffix = style.attr(&Ns::Style, "num-suffix").unwrap_or_default();
767 let display = style
770 .attr_usize(&Ns::Text, "display-levels")
771 .unwrap_or(1)
772 .max(1);
773 let mut numbers = Vec::new();
774 let first = (level + 1).saturating_sub(display);
775 for ancestor in first..level {
776 numbers.push(edit::number_text(counters.at(ancestor), format));
777 }
778 numbers.push(edit::number_text(number, format));
779 return format!("{prefix}{}{suffix}", numbers.join("."));
780 }
781 "\u{2022}".to_owned()
783 }
784
785 fn table(&mut self, ui: &mut Ui, table: &Element, width: f32) {
787 let columns = column_widths(self.document, table, width, self.zoom);
788 if columns.is_empty() {
789 return;
790 }
791 ui.add_space(4.0 * self.zoom);
792 self.rows(ui, table, &columns);
793 ui.add_space(4.0 * self.zoom);
794 }
795
796 fn rows(&mut self, ui: &mut Ui, parent: &Element, columns: &[f32]) {
797 for (index, element) in parent.elements_indexed() {
798 self.path.push(index);
799 if element.is(&Ns::Table, "table-row") {
800 self.row(ui, element, columns);
801 } else if element.is(&Ns::Table, "table-header-rows")
802 || element.is(&Ns::Table, "table-rows")
803 || element.is(&Ns::Table, "table-row-group")
804 {
805 self.rows(ui, element, columns);
806 }
807 self.path.pop();
808 }
809 }
810
811 fn row(&mut self, ui: &mut Ui, row: &Element, columns: &[f32]) {
812 let reserved = ui.painter().add(eframe::egui::Shape::Noop);
816 let mut painted = Vec::new();
817
818 let response = ui.horizontal_top(|ui| {
819 ui.spacing_mut().item_spacing.x = 0.0;
820 let mut column = 0usize;
821 for (cell_index, cell) in row.elements_indexed() {
822 let covered = cell.is(&Ns::Table, "covered-table-cell");
823 if !covered && !cell.is(&Ns::Table, "table-cell") {
824 continue;
825 }
826 let repeat = cell
827 .attr_usize(&Ns::Table, "number-columns-repeated")
828 .unwrap_or(1)
829 .max(1);
830 let spanned = cell
831 .attr_usize(&Ns::Table, "number-columns-spanned")
832 .unwrap_or(1)
833 .max(1);
834 for copy in 0..repeat {
835 let width: f32 = columns
841 .iter()
842 .skip(column)
843 .take(spanned)
844 .sum::<f32>()
845 .max(8.0);
846 if !covered {
847 let properties = self.style_of(cell, &Family::TableCell);
848 let padding = properties
849 .cell
850 .padding
851 .left
852 .map_or(2.0, odox_core::Length::points)
853 * self.zoom;
854 let inner = ui
855 .allocate_ui_with_layout(
856 vec2(width, 0.0),
857 eframe::egui::Layout::top_down(Align::Min),
858 |ui| {
859 ui.add_space(padding);
860 ui.set_min_width(width);
861 ui.set_max_width(width);
862 let content = (width - padding * 2.0).max(8.0);
863 self.path.push(cell_index);
864 let was_detached = self.detached;
865 self.detached |= copy > 0;
866 self.blocks(ui, cell, content);
867 self.detached = was_detached;
868 self.path.pop();
869 ui.add_space(padding);
870 },
871 )
872 .response
873 .rect;
874 painted.push((inner, properties));
875 }
876 column += 1;
877 }
878 }
879 });
880
881 let row_rect = response.response.rect;
882 let mut shapes = Vec::new();
883 for (rect, properties) in painted {
884 let cell_rect = Rect::from_min_max(
885 pos2(rect.left(), row_rect.top()),
886 pos2(rect.right(), row_rect.bottom()),
887 );
888 if let Some(fill) = properties.cell.background {
889 shapes.push(eframe::egui::Shape::rect_filled(
890 cell_rect,
891 0.0,
892 format::color32(fill),
893 ));
894 }
895 for (edge, from, to) in edges(cell_rect) {
896 if let Some(border) = edge_of(&properties.cell.border, edge) {
897 shapes.push(eframe::egui::Shape::line_segment(
898 [from, to],
899 Stroke::new(
900 (border.width.points() * self.zoom).max(1.0),
901 format::color32(border.color),
902 ),
903 ));
904 }
905 }
906 }
907 ui.painter().set(reserved, eframe::egui::Shape::Vec(shapes));
908 }
909
910 pub fn frame(&mut self, ui: &mut Ui, frame: &Element, width: f32) {
917 let zoom = self.zoom;
918 let declared = |local: &str| {
919 frame
920 .attr(&Ns::Svg, local)
921 .and_then(odox_core::Length::parse)
922 .map(|l| l.points() * zoom)
923 };
924
925 let found = frame
932 .elements()
933 .filter(|child| child.is(&Ns::Draw, "image"))
934 .find_map(|image| {
935 let href = image.attr(&Ns::Xlink, "href")?.to_owned();
936 self.pictures
937 .get(ui.ctx(), self.document, &href)
938 .map(|texture| (texture.id(), texture.size()))
939 });
940 if let Some((id, [pixels_wide, pixels_high])) = found {
941 #[allow(clippy::cast_precision_loss)]
942 let aspect = if pixels_wide == 0 {
943 1.0
944 } else {
945 pixels_high as f32 / pixels_wide as f32
946 };
947 let w = declared("width").unwrap_or(width).min(width);
948 let h = declared("height").unwrap_or(w * aspect);
949 let size = vec2(w, h);
950 ui.add(eframe::egui::Image::new((id, size)).fit_to_exact_size(size));
951 return;
952 }
953
954 if let Some((box_index, box_)) = frame
958 .elements_indexed()
959 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
960 {
961 let w = declared("width").unwrap_or(width).min(width);
962 ui.allocate_ui_with_layout(
963 vec2(w, 0.0),
964 eframe::egui::Layout::top_down(Align::Min),
965 |ui| {
966 ui.set_max_width(w);
967 eframe::egui::Frame::group(ui.style()).show(ui, |ui| {
968 self.path.push(box_index);
969 self.blocks(ui, box_, w - 16.0 * zoom);
970 self.path.pop();
971 });
972 },
973 );
974 return;
975 }
976
977 let w = declared("width").unwrap_or(width).min(width);
978 let h = declared("height").unwrap_or(48.0 * zoom);
979 let (rect, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover());
980 ui.painter().rect_stroke(
981 rect,
982 2.0,
983 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
984 StrokeKind::Inside,
985 );
986 }
987
988 fn page_break(&mut self, ui: &mut Ui, width: f32) {
993 ui.add_space(8.0 * self.zoom);
994 let (rect, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
995 ui.painter().hline(
996 rect.x_range(),
997 rect.center().y,
998 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
999 );
1000 ui.add_space(8.0 * self.zoom);
1001 }
1002}
1003
1004#[derive(Clone, Copy)]
1006enum Edge {
1007 Left,
1008 Right,
1009 Top,
1010 Bottom,
1011}
1012
1013fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
1014 [
1015 (Edge::Left, rect.left_top(), rect.left_bottom()),
1016 (Edge::Right, rect.right_top(), rect.right_bottom()),
1017 (Edge::Top, rect.left_top(), rect.right_top()),
1018 (Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
1019 ]
1020}
1021
1022fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
1023 match edge {
1024 Edge::Left => borders.left,
1025 Edge::Right => borders.right,
1026 Edge::Top => borders.top,
1027 Edge::Bottom => borders.bottom,
1028 }
1029}
1030
1031fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
1032 for (edge, from, to) in edges(rect) {
1033 if let Some(border) = edge_of(borders, edge) {
1034 ui.painter().line_segment(
1035 [from, to],
1036 Stroke::new(
1037 (border.width.points() * zoom).max(1.0),
1038 format::color32(border.color),
1039 ),
1040 );
1041 }
1042 }
1043}
1044
1045fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
1050 let mut declared = Vec::new();
1051 collect_columns(document, table, &mut declared);
1052 if declared.is_empty() {
1053 return Vec::new();
1054 }
1055 let total: f32 = declared.iter().filter_map(|w| *w).sum();
1056 let unstated = declared.iter().filter(|w| w.is_none()).count();
1057
1058 let scaled = total * zoom;
1061 let factor = if scaled > width && scaled > 0.0 {
1062 width / scaled
1063 } else {
1064 1.0
1065 };
1066 #[allow(clippy::cast_precision_loss)]
1067 let share = if unstated == 0 {
1068 0.0
1069 } else {
1070 ((width - scaled * factor) / unstated as f32).max(16.0)
1071 };
1072 declared
1073 .into_iter()
1074 .map(|w| w.map_or(share, |points| points * zoom * factor))
1075 .collect()
1076}
1077
1078fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
1079 for element in parent.elements() {
1080 if element.is(&Ns::Table, "table-column") {
1081 let repeat = element
1082 .attr_usize(&Ns::Table, "number-columns-repeated")
1083 .unwrap_or(1)
1084 .clamp(1, 1024);
1085 let width = element
1086 .attr(&Ns::Table, "style-name")
1087 .map(|name| document.styles.resolve(&Family::TableColumn, name))
1088 .and_then(|p| p.column_width)
1089 .map(odox_core::Length::points);
1090 for _ in 0..repeat {
1091 into.push(width);
1092 }
1093 } else if element.is(&Ns::Table, "table-columns")
1094 || element.is(&Ns::Table, "table-header-columns")
1095 || element.is(&Ns::Table, "table-column-group")
1096 {
1097 collect_columns(document, element, into);
1098 }
1099 }
1100}
1101
1102fn expose(
1106 ui: &Ui,
1107 response: &Response,
1108 element: &Element,
1109 galley: &Galley,
1110 origin: Pos2,
1111 edited: bool,
1112) {
1113 let heading = edit::heading_level(element);
1114 if !edited {
1115 let to_global = ui
1116 .ctx()
1117 .layer_transform_to_global(ui.layer_id())
1118 .unwrap_or_default();
1119 update_accesskit_for_text_widget(
1120 ui.ctx(),
1121 response.id,
1122 None,
1123 if heading.is_some() {
1124 Role::Heading
1125 } else {
1126 Role::Paragraph
1127 },
1128 to_global * TSTransform::from_translation(origin.to_vec2()),
1129 galley,
1130 );
1131 }
1132 if let Some(level) = heading {
1133 ui.ctx().accesskit_node_builder(response.id, |node| {
1134 node.set_role(Role::Heading);
1135 node.set_level(usize::from(level));
1136 });
1137 }
1138}
1139
1140fn holds_place(element: &Element, place: &str) -> bool {
1143 if let Some(name) = place.strip_suffix("|outline") {
1144 return element.is(&Ns::Text, "h") && element.plain_text().trim() == name.trim();
1145 }
1146 element.elements().any(|e| {
1147 let marks = e.is(&Ns::Text, "bookmark") || e.is(&Ns::Text, "bookmark-start");
1148 (marks && e.attr(&Ns::Text, "name") == Some(place)) || holds_place(e, place)
1149 })
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use std::path::Path;
1155
1156 use super::{Flow, Pictures};
1157 use crate::format::{self, DEFAULT_SIZE};
1158 use odox_core::{Document, Element, Family, edit, media_type};
1159
1160 const ANY: &[&str] = &[
1161 media_type::TEXT,
1162 media_type::TEXT_WEB,
1163 media_type::SPREADSHEET,
1164 media_type::PRESENTATION,
1165 ];
1166
1167 fn documents(directory: &Path, into: &mut Vec<Document>) {
1168 let Ok(entries) = std::fs::read_dir(directory) else {
1169 return;
1170 };
1171 for path in entries.filter_map(Result::ok).map(|entry| entry.path()) {
1172 if path.is_dir() {
1173 documents(&path, into);
1174 } else if matches!(
1175 path.extension().and_then(|e| e.to_str()),
1176 Some("odt" | "ods" | "odp")
1177 ) {
1178 let bytes = std::fs::read(&path).expect("a corpus document reads");
1179 into.push(Document::read(&bytes, ANY).expect("a corpus document parses"));
1180 }
1181 }
1182 }
1183
1184 fn paragraphs<'a>(element: &'a Element, into: &mut Vec<&'a Element>) {
1185 for child in element.elements() {
1186 if edit::is_paragraph(child) {
1187 into.push(child);
1188 }
1189 paragraphs(child, into);
1190 }
1191 }
1192
1193 #[test]
1197 fn what_is_drawn_maps_onto_the_text_an_edit_changes() {
1198 let mut corpus = Vec::new();
1199 documents(
1200 &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"),
1201 &mut corpus,
1202 );
1203 assert!(!corpus.is_empty(), "the corpus is where it was");
1204 let mut checked = 0;
1205 for document in &corpus {
1206 let mut pictures = Pictures::default();
1207 let flow = Flow::new(document, &mut pictures, 1.0);
1208 let mut found = Vec::new();
1209 paragraphs(&document.content, &mut found);
1210 for paragraph in found {
1211 let properties = flow.style_of(paragraph, &Family::Paragraph);
1212 let base = format::text_format(&properties.text, DEFAULT_SIZE, 1.0, flow.palette);
1213 let size = format::size_of(&properties.text, DEFAULT_SIZE);
1214 let (job, _) = flow.layout_job(paragraph, &properties, &base, size, 400.0);
1215 let (_, map) = job.into_parts();
1216 let text = edit::text(paragraph);
1217 assert_eq!(map.model_len(), text.chars().count(), "{text:?}");
1218 checked += 1;
1219 }
1220 }
1221 assert!(checked > 0);
1222 }
1223}