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 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 = merge(inherited, &style.text);
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 = merge(inherited, &style.text);
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 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(number_text(counters.at(ancestor), format));
777 }
778 numbers.push(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
1004fn merge(parent: &TextProperties, child: &TextProperties) -> TextProperties {
1007 TextProperties {
1008 font_family: child
1009 .font_family
1010 .clone()
1011 .or_else(|| parent.font_family.clone()),
1012 size: child.size.or(parent.size),
1013 bold: child.bold.or(parent.bold),
1014 italic: child.italic.or(parent.italic),
1015 underline: child.underline.or(parent.underline),
1016 strike: child.strike.or(parent.strike),
1017 color: child.color.or(parent.color),
1018 background: child.background.or(parent.background),
1019 position: child.position.or(parent.position),
1020 uppercase: child.uppercase.or(parent.uppercase),
1021 }
1022}
1023
1024#[derive(Clone, Copy)]
1026enum Edge {
1027 Left,
1028 Right,
1029 Top,
1030 Bottom,
1031}
1032
1033fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
1034 [
1035 (Edge::Left, rect.left_top(), rect.left_bottom()),
1036 (Edge::Right, rect.right_top(), rect.right_bottom()),
1037 (Edge::Top, rect.left_top(), rect.right_top()),
1038 (Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
1039 ]
1040}
1041
1042fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
1043 match edge {
1044 Edge::Left => borders.left,
1045 Edge::Right => borders.right,
1046 Edge::Top => borders.top,
1047 Edge::Bottom => borders.bottom,
1048 }
1049}
1050
1051fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
1052 for (edge, from, to) in edges(rect) {
1053 if let Some(border) = edge_of(borders, edge) {
1054 ui.painter().line_segment(
1055 [from, to],
1056 Stroke::new(
1057 (border.width.points() * zoom).max(1.0),
1058 format::color32(border.color),
1059 ),
1060 );
1061 }
1062 }
1063}
1064
1065fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
1070 let mut declared = Vec::new();
1071 collect_columns(document, table, &mut declared);
1072 if declared.is_empty() {
1073 return Vec::new();
1074 }
1075 let total: f32 = declared.iter().filter_map(|w| *w).sum();
1076 let unstated = declared.iter().filter(|w| w.is_none()).count();
1077
1078 let scaled = total * zoom;
1081 let factor = if scaled > width && scaled > 0.0 {
1082 width / scaled
1083 } else {
1084 1.0
1085 };
1086 #[allow(clippy::cast_precision_loss)]
1087 let share = if unstated == 0 {
1088 0.0
1089 } else {
1090 ((width - scaled * factor) / unstated as f32).max(16.0)
1091 };
1092 declared
1093 .into_iter()
1094 .map(|w| w.map_or(share, |points| points * zoom * factor))
1095 .collect()
1096}
1097
1098fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
1099 for element in parent.elements() {
1100 if element.is(&Ns::Table, "table-column") {
1101 let repeat = element
1102 .attr_usize(&Ns::Table, "number-columns-repeated")
1103 .unwrap_or(1)
1104 .clamp(1, 1024);
1105 let width = element
1106 .attr(&Ns::Table, "style-name")
1107 .map(|name| document.styles.resolve(&Family::TableColumn, name))
1108 .and_then(|p| p.column_width)
1109 .map(odox_core::Length::points);
1110 for _ in 0..repeat {
1111 into.push(width);
1112 }
1113 } else if element.is(&Ns::Table, "table-columns")
1114 || element.is(&Ns::Table, "table-header-columns")
1115 || element.is(&Ns::Table, "table-column-group")
1116 {
1117 collect_columns(document, element, into);
1118 }
1119 }
1120}
1121
1122fn number_text(number: usize, format: &str) -> String {
1124 match format.chars().next() {
1125 Some('a') => alphabetic(number, b'a'),
1126 Some('A') => alphabetic(number, b'A'),
1127 Some('i') => roman(number).to_lowercase(),
1128 Some('I') => roman(number),
1129 None => String::new(),
1132 _ => number.to_string(),
1133 }
1134}
1135
1136fn alphabetic(number: usize, first: u8) -> String {
1138 let mut n = number;
1139 let mut out = Vec::new();
1140 while n > 0 {
1141 let remainder = (n - 1) % 26;
1142 out.push(first + u8::try_from(remainder).unwrap_or(0));
1143 n = (n - 1) / 26;
1144 }
1145 out.reverse();
1146 String::from_utf8(out).unwrap_or_default()
1147}
1148
1149fn roman(number: usize) -> String {
1150 const VALUES: [(usize, &str); 13] = [
1151 (1000, "M"),
1152 (900, "CM"),
1153 (500, "D"),
1154 (400, "CD"),
1155 (100, "C"),
1156 (90, "XC"),
1157 (50, "L"),
1158 (40, "XL"),
1159 (10, "X"),
1160 (9, "IX"),
1161 (5, "V"),
1162 (4, "IV"),
1163 (1, "I"),
1164 ];
1165 if number == 0 || number > 3999 {
1168 return number.to_string();
1169 }
1170 let mut left = number;
1171 let mut out = String::new();
1172 for (value, numeral) in VALUES {
1173 while left >= value {
1174 out.push_str(numeral);
1175 left -= value;
1176 }
1177 }
1178 out
1179}
1180
1181fn expose(
1185 ui: &Ui,
1186 response: &Response,
1187 element: &Element,
1188 galley: &Galley,
1189 origin: Pos2,
1190 edited: bool,
1191) {
1192 let heading = edit::heading_level(element);
1193 if !edited {
1194 let to_global = ui
1195 .ctx()
1196 .layer_transform_to_global(ui.layer_id())
1197 .unwrap_or_default();
1198 update_accesskit_for_text_widget(
1199 ui.ctx(),
1200 response.id,
1201 None,
1202 if heading.is_some() {
1203 Role::Heading
1204 } else {
1205 Role::Paragraph
1206 },
1207 to_global * TSTransform::from_translation(origin.to_vec2()),
1208 galley,
1209 );
1210 }
1211 if let Some(level) = heading {
1212 ui.ctx().accesskit_node_builder(response.id, |node| {
1213 node.set_role(Role::Heading);
1214 node.set_level(usize::from(level));
1215 });
1216 }
1217}
1218
1219fn holds_place(element: &Element, place: &str) -> bool {
1222 if let Some(name) = place.strip_suffix("|outline") {
1223 return element.is(&Ns::Text, "h") && element.plain_text().trim() == name.trim();
1224 }
1225 element.elements().any(|e| {
1226 let marks = e.is(&Ns::Text, "bookmark") || e.is(&Ns::Text, "bookmark-start");
1227 (marks && e.attr(&Ns::Text, "name") == Some(place)) || holds_place(e, place)
1228 })
1229}
1230
1231pub(crate) fn is_block_container(element: &Element) -> bool {
1233 element.name.ns == Ns::Text
1234 && matches!(
1235 &*element.name.local,
1236 "section"
1237 | "index-body"
1238 | "index-title"
1239 | "table-of-content"
1240 | "illustration-index"
1241 | "table-index"
1242 | "object-index"
1243 | "user-index"
1244 | "alphabetical-index"
1245 | "bibliography"
1246 | "tracked-changes"
1247 | "deletion"
1248 )
1249}
1250
1251fn is_inline_passthrough(element: &Element) -> bool {
1257 element.name.ns == Ns::Text
1258 && matches!(
1259 &*element.name.local,
1260 "bookmark"
1261 | "bookmark-start"
1262 | "bookmark-end"
1263 | "reference-mark"
1264 | "reference-mark-start"
1265 | "reference-mark-end"
1266 | "span"
1267 | "bibliography-mark"
1268 | "ruby"
1269 | "ruby-base"
1270 | "meta"
1271 | "meta-field"
1272 | "change-start"
1273 | "change-end"
1274 | "page-number"
1275 | "page-count"
1276 | "title"
1277 | "subject"
1278 | "author-name"
1279 | "author-initials"
1280 | "chapter"
1281 | "file-name"
1282 | "sheet-name"
1283 | "date"
1284 | "time"
1285 | "creator"
1286 | "description"
1287 | "keywords"
1288 | "sequence"
1289 | "bookmark-ref"
1290 | "sequence-ref"
1291 | "reference-ref"
1292 | "variable-get"
1293 | "variable-set"
1294 | "user-field-get"
1295 | "placeholder"
1296 | "conditional-text"
1297 | "hidden-text"
1298 | "text-input"
1299 )
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304 use std::path::Path;
1305
1306 use super::{Flow, Pictures};
1307 use crate::format::{self, DEFAULT_SIZE};
1308 use odox_core::{Document, Element, Family, edit, media_type};
1309
1310 const ANY: &[&str] = &[
1311 media_type::TEXT,
1312 media_type::TEXT_WEB,
1313 media_type::SPREADSHEET,
1314 media_type::PRESENTATION,
1315 ];
1316
1317 fn documents(directory: &Path, into: &mut Vec<Document>) {
1318 let Ok(entries) = std::fs::read_dir(directory) else {
1319 return;
1320 };
1321 for path in entries.filter_map(Result::ok).map(|entry| entry.path()) {
1322 if path.is_dir() {
1323 documents(&path, into);
1324 } else if matches!(
1325 path.extension().and_then(|e| e.to_str()),
1326 Some("odt" | "ods" | "odp")
1327 ) {
1328 let bytes = std::fs::read(&path).expect("a corpus document reads");
1329 into.push(Document::read(&bytes, ANY).expect("a corpus document parses"));
1330 }
1331 }
1332 }
1333
1334 fn paragraphs<'a>(element: &'a Element, into: &mut Vec<&'a Element>) {
1335 for child in element.elements() {
1336 if edit::is_paragraph(child) {
1337 into.push(child);
1338 }
1339 paragraphs(child, into);
1340 }
1341 }
1342
1343 #[test]
1347 fn what_is_drawn_maps_onto_the_text_an_edit_changes() {
1348 let mut corpus = Vec::new();
1349 documents(
1350 &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"),
1351 &mut corpus,
1352 );
1353 assert!(!corpus.is_empty(), "the corpus is where it was");
1354 let mut checked = 0;
1355 for document in &corpus {
1356 let mut pictures = Pictures::default();
1357 let flow = Flow::new(document, &mut pictures, 1.0);
1358 let mut found = Vec::new();
1359 paragraphs(&document.content, &mut found);
1360 for paragraph in found {
1361 let properties = flow.style_of(paragraph, &Family::Paragraph);
1362 let base = format::text_format(&properties.text, DEFAULT_SIZE, 1.0, flow.palette);
1363 let size = format::size_of(&properties.text, DEFAULT_SIZE);
1364 let (job, _) = flow.layout_job(paragraph, &properties, &base, size, 400.0);
1365 let (_, map) = job.into_parts();
1366 let text = edit::text(paragraph);
1367 assert_eq!(map.model_len(), text.chars().count(), "{text:?}");
1368 checked += 1;
1369 }
1370 }
1371 assert!(checked > 0);
1372 }
1373}