1use std::collections::HashMap;
17
18use eframe::egui::{
19 Align, ColorImage, Context, Pos2, Rect, Sense, Stroke, StrokeKind, TextFormat, TextureHandle,
20 TextureOptions, Ui, pos2, text::LayoutJob, text_selection::LabelSelectionState, vec2,
21};
22use egui_richedit::{Laid, ParagraphJob};
23use odox_core::{
24 Border, Document, Element, Family, Node, Ns, Properties, TextAlign, TextProperties, edit,
25};
26
27use crate::flow_model::PageEditor;
28use crate::format::{self, DEFAULT_SIZE};
29
30#[derive(Default)]
35pub struct Pictures {
36 textures: HashMap<String, Option<TextureHandle>>,
37}
38
39impl Pictures {
40 pub fn get(
42 &mut self,
43 ctx: &Context,
44 document: &Document,
45 href: &str,
46 ) -> Option<&TextureHandle> {
47 if !self.textures.contains_key(href) {
48 let texture = document
49 .picture(href)
50 .and_then(decode)
51 .map(|image| ctx.load_texture(href, image, TextureOptions::LINEAR));
52 self.textures.insert(href.to_owned(), texture);
53 }
54 self.textures.get(href).and_then(Option::as_ref)
55 }
56
57 pub fn clear(&mut self) {
59 self.textures.clear();
60 }
61}
62
63fn decode(bytes: &[u8]) -> Option<ColorImage> {
64 let decoded = image::load_from_memory(bytes).ok()?.to_rgba8();
65 let size = [decoded.width() as usize, decoded.height() as usize];
66 Some(ColorImage::from_rgba_unmultiplied(size, decoded.as_raw()))
67}
68
69pub struct Flow<'a> {
71 pub document: &'a Document,
73 pub pictures: &'a mut Pictures,
75 pub zoom: f32,
77 pub palette: format::Palette,
79 pub scroll_to_heading: Option<usize>,
85 headings_seen: usize,
87 pub page: Option<&'a mut PageEditor>,
90 pub selectable: bool,
93 path: Vec<usize>,
96 detached: bool,
99}
100
101impl<'a> Flow<'a> {
102 pub fn new(document: &'a Document, pictures: &'a mut Pictures, zoom: f32) -> Self {
104 Self {
105 document,
106 pictures,
107 zoom,
108 palette: format::Palette::default(),
109 scroll_to_heading: None,
110 headings_seen: 0,
111 page: None,
112 selectable: true,
113 path: Vec::new(),
114 detached: false,
115 }
116 }
117
118 pub fn start_at(&mut self, prefix: Vec<usize>) {
122 self.path = prefix;
123 }
124}
125
126const LIST_STEP: f32 = 18.0;
128const LIST_GUTTER: f32 = 20.0;
130const LABEL_GAP: f32 = 5.0;
132
133struct Spacing {
135 left: f32,
137 right: f32,
139 indent: f32,
141 before: f32,
143 after: f32,
145}
146
147impl Spacing {
148 fn of(properties: &odox_core::ParagraphProperties, zoom: f32) -> Self {
149 let points = |length: Option<odox_core::Length>| {
150 length.map_or(0.0, odox_core::Length::points) * zoom
151 };
152 Self {
153 left: points(properties.margin.left),
154 right: points(properties.margin.right),
155 indent: points(properties.text_indent),
156 before: points(properties.margin.top),
157 after: points(properties.margin.bottom),
158 }
159 }
160}
161
162#[derive(Clone, Copy)]
167struct Run<'a> {
168 inherited: &'a TextProperties,
170 size: f32,
173 format: &'a TextFormat,
175 palette: format::Palette,
177 counted: bool,
180}
181
182#[derive(Default)]
184struct Counters(Vec<usize>);
185
186impl Counters {
187 fn bump(&mut self, level: usize, start: usize) -> usize {
188 while self.0.len() <= level {
189 self.0.push(0);
190 }
191 if self.0[level] == 0 {
194 self.0[level] = start;
195 } else {
196 self.0[level] += 1;
197 }
198 self.0.truncate(level + 1);
201 self.0[level]
202 }
203
204 fn at(&self, level: usize) -> usize {
205 self.0.get(level).copied().unwrap_or(1)
206 }
207}
208
209impl Flow<'_> {
210 pub fn blocks(&mut self, ui: &mut Ui, parent: &Element, width: f32) {
215 let mut counters = Counters::default();
216 self.blocks_with(ui, parent, width, &mut counters);
217 }
218
219 fn blocks_with(&mut self, ui: &mut Ui, parent: &Element, width: f32, counters: &mut Counters) {
220 for (index, element) in parent.elements_indexed() {
221 self.path.push(index);
222 match () {
223 () if element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h") => {
224 self.paragraph(ui, element, width, None);
225 }
226 () if element.is(&Ns::Text, "list") => {
227 let continues = element.attr(&Ns::Text, "continue-numbering") == Some("true")
232 || element.attr(&Ns::Text, "continue-list").is_some();
233 if !continues {
234 *counters = Counters::default();
235 }
236 self.list(ui, element, width, 0, None, counters);
237 }
238 () if element.is(&Ns::Table, "table") => self.table(ui, element, width),
239 () if element.is(&Ns::Draw, "frame") => self.frame(ui, element, width),
240 () if element.is(&Ns::Text, "soft-page-break") => self.page_break(ui, width),
241 () if is_block_container(element) => {
242 self.blocks_with(ui, element, width, counters);
243 }
244 () => {}
245 }
246 self.path.pop();
247 }
248 }
249
250 fn style_of(&self, element: &Element, family: &Family) -> std::rc::Rc<Properties> {
252 let name = element
253 .attr(&Ns::Text, "style-name")
254 .or_else(|| element.attr(&Ns::Table, "style-name"))
255 .unwrap_or("Standard");
256 self.document.styles.resolve(family, name)
257 }
258
259 fn paragraph(&mut self, ui: &mut Ui, element: &Element, width: f32, label: Option<&str>) {
261 let properties = self.style_of(element, &Family::Paragraph);
262 let zoom = self.zoom;
263 let size = format::size_of(&properties.text, DEFAULT_SIZE);
264
265 let Spacing {
266 left,
267 right,
268 indent,
269 before,
270 after,
271 } = Spacing::of(&properties.paragraph, zoom);
272
273 let body_width = (width - left - right).max(1.0);
277 let wrap = (body_width - indent.max(0.0)).max(1.0);
278
279 if before > 0.0 {
280 ui.add_space(before);
281 }
282
283 let base = format::text_format(&properties.text, DEFAULT_SIZE, zoom, self.palette);
284
285 let (job, frames) = self.layout_job(element, &properties, &base, size, wrap);
286 let (job, map) = job.into_parts();
287
288 let galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(job));
289 let height = galley.size().y;
290 let edited = !self.detached && self.page.is_some();
291 let sense = if self.selectable {
300 Sense::click_and_drag()
301 } else {
302 Sense::click()
303 };
304 let (rect, response) = ui.allocate_exact_size(vec2(width, height), sense);
305
306 if element.is(&Ns::Text, "h") {
307 if self.scroll_to_heading == Some(self.headings_seen) {
308 ui.scroll_to_rect(rect, Some(Align::TOP));
309 }
310 self.headings_seen += 1;
311 }
312
313 if let Some(fill) = properties.paragraph.background {
314 ui.painter().rect_filled(rect, 0.0, format::color32(fill));
315 }
316 paint_borders(ui, rect, &properties.paragraph.border, zoom);
317
318 let anchor = match properties.paragraph.align.unwrap_or(TextAlign::Start) {
322 TextAlign::Center => rect.left() + left + indent.max(0.0) + wrap / 2.0,
323 TextAlign::End => rect.left() + left + indent.max(0.0) + wrap,
324 _ => rect.left() + left + indent.max(0.0),
325 };
326 let origin = pos2(anchor, rect.top());
335 if let Some(page) = self.page.as_deref_mut().filter(|_| edited) {
336 let laid = Laid {
337 galley,
338 map,
339 origin,
340 };
341 page.paragraph(ui, &response, &self.path, laid);
342 } else {
343 LabelSelectionState::label_text_selection(
344 ui,
345 &response,
346 origin,
347 galley,
348 base.color,
349 Stroke::NONE,
350 );
351 }
352
353 if let Some(label) = label {
354 let mut label_job = LayoutJob::default();
355 label_job.append(label, 0.0, base.clone());
356 let label_galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(label_job));
357 let x = rect.left() + left - label_galley.size().x - LABEL_GAP * zoom;
361 ui.painter()
362 .galley(pos2(x, rect.top()), label_galley, base.color);
363 }
364
365 let was_detached = self.detached;
368 self.detached = true;
369 for frame in frames {
370 self.frame(ui, &frame, body_width);
371 }
372 self.detached = was_detached;
373 if after > 0.0 {
374 ui.add_space(after);
375 }
376 }
377
378 fn layout_job(
381 &self,
382 element: &Element,
383 properties: &Properties,
384 base: &TextFormat,
385 size: f32,
386 wrap: f32,
387 ) -> (ParagraphJob, Vec<Element>) {
388 let job = LayoutJob {
389 wrap: eframe::egui::text::TextWrapping {
390 max_width: wrap,
391 ..Default::default()
392 },
393 halign: match properties.paragraph.align.unwrap_or(TextAlign::Start) {
394 TextAlign::Center => Align::Center,
395 TextAlign::End => Align::Max,
396 _ => Align::Min,
397 },
398 justify: properties.paragraph.align == Some(TextAlign::Justify),
399 ..LayoutJob::default()
400 };
401 let mut job = ParagraphJob::new(job);
402 let mut frames = Vec::new();
403 let run = Run {
404 inherited: &properties.text,
405 size,
406 format: base,
407 palette: self.palette,
408 counted: true,
409 };
410 self.runs(element, &run, &mut job, &mut frames);
411
412 if job.is_empty() {
415 job.atom(" ", 0, base.clone());
416 }
417 if let Some(measure) = properties.paragraph.line_height {
418 for format in job.formats_mut() {
422 let own = format.font_id.size / self.zoom;
423 format.line_height = Some(measure.resolve(own) * self.zoom);
424 }
425 }
426 (job, frames)
427 }
428
429 fn runs(
441 &self,
442 parent: &Element,
443 run: &Run<'_>,
444 job: &mut ParagraphJob,
445 frames: &mut Vec<Element>,
446 ) {
447 let Run {
448 inherited,
449 size,
450 format,
451 palette,
452 counted,
453 } = *run;
454 let put = |job: &mut ParagraphJob, shown: &str, model_len: usize| {
455 if !counted {
456 job.atom(shown, 0, format.clone());
457 } else if shown.chars().count() == model_len {
458 job.text(shown, format.clone());
459 } else {
460 job.atom(shown, model_len, format.clone());
461 }
462 };
463 for child in &parent.children {
464 match child {
465 Node::Text(text) | Node::CData(text) => put(job, text, text.chars().count()),
466 Node::Comment(_) | Node::ProcessingInstruction(_) => {}
467 Node::Element(element) => {
468 if element.is(&Ns::Text, "s") {
469 let count = element.attr_usize(&Ns::Text, "c").unwrap_or(1);
470 put(job, &" ".repeat(count.min(256)), count);
471 } else if element.is(&Ns::Text, "tab") {
472 put(job, " ", 1);
477 } else if element.is(&Ns::Text, "line-break") {
478 put(job, "\n", 1);
479 } else if element.is(&Ns::Text, "span") {
480 let style = self.style_of(element, &Family::Text);
481 let merged = merge(inherited, &style.text);
482 let inner_size = format::size_of(&merged, size);
483 let inner = format::text_format(&merged, size, self.zoom, palette);
484 let run = Run {
485 inherited: &merged,
486 size: inner_size,
487 format: &inner,
488 palette,
489 counted,
490 };
491 self.runs(element, &run, job, frames);
492 } else if element.is(&Ns::Text, "a") {
493 let style = self.style_of(element, &Family::Text);
494 let merged = merge(inherited, &style.text);
495 let inner_size = format::size_of(&merged, size);
496 let inner = format::link_format(&merged, size, self.zoom, palette);
497 let run = Run {
498 inherited: &merged,
499 size: inner_size,
500 format: &inner,
501 palette,
502 counted,
503 };
504 self.runs(element, &run, job, frames);
505 } else if element.is(&Ns::Draw, "frame") {
506 frames.push(element.clone());
507 } else if element.is(&Ns::Text, "note") {
508 if let Some(citation) = element.child(&Ns::Text, "note-citation") {
512 let mut raised = format.clone();
513 raised.font_id.size *= 0.7;
514 raised.valign = Align::TOP;
515 job.atom(&citation.plain_text(), 0, raised);
516 }
517 } else if is_inline_passthrough(element) {
518 let run = Run {
522 counted: counted && edit::holds_text(element),
523 ..*run
524 };
525 self.runs(element, &run, job, frames);
526 }
527 }
528 }
529 }
530 }
531
532 fn list(
534 &mut self,
535 ui: &mut Ui,
536 element: &Element,
537 width: f32,
538 level: usize,
539 inherited_style: Option<&str>,
540 counters: &mut Counters,
541 ) {
542 let style_name = element
543 .attr(&Ns::Text, "style-name")
544 .or(inherited_style)
545 .unwrap_or_default()
546 .to_owned();
547 #[allow(clippy::cast_precision_loss)]
551 let indent = LIST_STEP * self.zoom * (level as f32 + 1.0);
552 let gutter = LIST_GUTTER * self.zoom;
553
554 for (item_index, item) in element.elements_indexed() {
555 let numbered = item.is(&Ns::Text, "list-item");
556 if !numbered && !item.is(&Ns::Text, "list-header") {
557 continue;
558 }
559 self.path.push(item_index);
560 let label = if numbered {
561 let level_style = self.level_style(&style_name, level);
562 let start = level_style
563 .as_ref()
564 .and_then(|s| s.attr_usize(&Ns::Text, "start-value"))
565 .unwrap_or(1);
566 let number = counters.bump(level, start);
567 Self::label(level_style.as_ref(), level, number, counters)
568 } else {
569 String::new()
572 };
573
574 let mut first = true;
575 for (block_index, block) in item.elements_indexed() {
576 self.path.push(block_index);
577 if block.is(&Ns::Text, "list") {
578 self.list(ui, block, width, level + 1, Some(&style_name), counters);
579 self.path.pop();
580 continue;
581 }
582 let on_this_block = if first && !label.is_empty() {
583 Some(label.as_str())
584 } else {
585 None
586 };
587 if block.is(&Ns::Text, "p") || block.is(&Ns::Text, "h") {
588 first = false;
589 ui.horizontal_top(|ui| {
593 ui.add_space(indent + gutter);
594 self.paragraph(ui, block, width - indent - gutter, on_this_block);
595 });
596 } else if block.is(&Ns::Table, "table") {
597 first = false;
598 ui.horizontal_top(|ui| {
599 ui.add_space(indent + gutter);
600 self.table(ui, block, width - indent - gutter);
601 });
602 } else if block.is(&Ns::Draw, "frame") {
603 first = false;
604 ui.horizontal_top(|ui| {
605 ui.add_space(indent + gutter);
606 self.frame(ui, block, width - indent - gutter);
607 });
608 }
609 self.path.pop();
610 }
611 self.path.pop();
612 }
613 }
614
615 fn level_style(&self, list_style: &str, level: usize) -> Option<Element> {
618 let style = self.document.styles.list_style(list_style)?;
619 style
620 .elements()
621 .find(|e| e.attr_usize(&Ns::Text, "level") == Some(level + 1))
622 .cloned()
623 }
624
625 fn label(
627 level_style: Option<&Element>,
628 level: usize,
629 number: usize,
630 counters: &Counters,
631 ) -> String {
632 let Some(style) = level_style else {
633 return "\u{2022}".to_owned();
636 };
637 if style.is(&Ns::Text, "list-level-style-bullet") {
638 return style
639 .attr(&Ns::Text, "bullet-char")
640 .unwrap_or("\u{2022}")
641 .to_owned();
642 }
643 if style.is(&Ns::Text, "list-level-style-number") {
644 let format = style.attr(&Ns::Style, "num-format").unwrap_or("1");
645 let prefix = style.attr(&Ns::Text, "num-prefix").unwrap_or_default();
646 let suffix = style.attr(&Ns::Text, "num-suffix").unwrap_or_default();
647 let display = style
650 .attr_usize(&Ns::Text, "display-levels")
651 .unwrap_or(1)
652 .max(1);
653 let mut numbers = Vec::new();
654 let first = (level + 1).saturating_sub(display);
655 for ancestor in first..level {
656 numbers.push(number_text(counters.at(ancestor), format));
657 }
658 numbers.push(number_text(number, format));
659 return format!("{prefix}{}{suffix}", numbers.join("."));
660 }
661 "\u{2022}".to_owned()
663 }
664
665 fn table(&mut self, ui: &mut Ui, table: &Element, width: f32) {
667 let columns = column_widths(self.document, table, width, self.zoom);
668 if columns.is_empty() {
669 return;
670 }
671 ui.add_space(4.0 * self.zoom);
672 self.rows(ui, table, &columns);
673 ui.add_space(4.0 * self.zoom);
674 }
675
676 fn rows(&mut self, ui: &mut Ui, parent: &Element, columns: &[f32]) {
677 for (index, element) in parent.elements_indexed() {
678 self.path.push(index);
679 if element.is(&Ns::Table, "table-row") {
680 self.row(ui, element, columns);
681 } else if element.is(&Ns::Table, "table-header-rows")
682 || element.is(&Ns::Table, "table-rows")
683 || element.is(&Ns::Table, "table-row-group")
684 {
685 self.rows(ui, element, columns);
686 }
687 self.path.pop();
688 }
689 }
690
691 fn row(&mut self, ui: &mut Ui, row: &Element, columns: &[f32]) {
692 let reserved = ui.painter().add(eframe::egui::Shape::Noop);
696 let mut painted = Vec::new();
697
698 let response = ui.horizontal_top(|ui| {
699 ui.spacing_mut().item_spacing.x = 0.0;
700 let mut column = 0usize;
701 for (cell_index, cell) in row.elements_indexed() {
702 let covered = cell.is(&Ns::Table, "covered-table-cell");
703 if !covered && !cell.is(&Ns::Table, "table-cell") {
704 continue;
705 }
706 let repeat = cell
707 .attr_usize(&Ns::Table, "number-columns-repeated")
708 .unwrap_or(1)
709 .max(1);
710 let spanned = cell
711 .attr_usize(&Ns::Table, "number-columns-spanned")
712 .unwrap_or(1)
713 .max(1);
714 for _ in 0..repeat {
715 let width: f32 = columns
721 .iter()
722 .skip(column)
723 .take(spanned)
724 .sum::<f32>()
725 .max(8.0);
726 if !covered {
727 let properties = self.style_of(cell, &Family::TableCell);
728 let padding = properties
729 .cell
730 .padding
731 .left
732 .map_or(2.0, odox_core::Length::points)
733 * self.zoom;
734 let inner = ui
735 .allocate_ui_with_layout(
736 vec2(width, 0.0),
737 eframe::egui::Layout::top_down(Align::Min),
738 |ui| {
739 ui.add_space(padding);
740 ui.set_min_width(width);
741 ui.set_max_width(width);
742 let content = (width - padding * 2.0).max(8.0);
743 self.path.push(cell_index);
744 self.blocks(ui, cell, content);
745 self.path.pop();
746 ui.add_space(padding);
747 },
748 )
749 .response
750 .rect;
751 painted.push((inner, properties));
752 }
753 column += 1;
754 }
755 }
756 });
757
758 let row_rect = response.response.rect;
759 let mut shapes = Vec::new();
760 for (rect, properties) in painted {
761 let cell_rect = Rect::from_min_max(
762 pos2(rect.left(), row_rect.top()),
763 pos2(rect.right(), row_rect.bottom()),
764 );
765 if let Some(fill) = properties.cell.background {
766 shapes.push(eframe::egui::Shape::rect_filled(
767 cell_rect,
768 0.0,
769 format::color32(fill),
770 ));
771 }
772 for (edge, from, to) in edges(cell_rect) {
773 if let Some(border) = edge_of(&properties.cell.border, edge) {
774 shapes.push(eframe::egui::Shape::line_segment(
775 [from, to],
776 Stroke::new(
777 (border.width.points() * self.zoom).max(1.0),
778 format::color32(border.color),
779 ),
780 ));
781 }
782 }
783 }
784 ui.painter().set(reserved, eframe::egui::Shape::Vec(shapes));
785 }
786
787 pub fn frame(&mut self, ui: &mut Ui, frame: &Element, width: f32) {
794 let zoom = self.zoom;
795 let declared = |local: &str| {
796 frame
797 .attr(&Ns::Svg, local)
798 .and_then(odox_core::Length::parse)
799 .map(|l| l.points() * zoom)
800 };
801
802 let found = frame
809 .elements()
810 .filter(|child| child.is(&Ns::Draw, "image"))
811 .find_map(|image| {
812 let href = image.attr(&Ns::Xlink, "href")?.to_owned();
813 self.pictures
814 .get(ui.ctx(), self.document, &href)
815 .map(|texture| (texture.id(), texture.size()))
816 });
817 if let Some((id, [pixels_wide, pixels_high])) = found {
818 #[allow(clippy::cast_precision_loss)]
819 let aspect = if pixels_wide == 0 {
820 1.0
821 } else {
822 pixels_high as f32 / pixels_wide as f32
823 };
824 let w = declared("width").unwrap_or(width).min(width);
825 let h = declared("height").unwrap_or(w * aspect);
826 let size = vec2(w, h);
827 ui.add(eframe::egui::Image::new((id, size)).fit_to_exact_size(size));
828 return;
829 }
830
831 if let Some((box_index, box_)) = frame
835 .elements_indexed()
836 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
837 {
838 let w = declared("width").unwrap_or(width).min(width);
839 ui.allocate_ui_with_layout(
840 vec2(w, 0.0),
841 eframe::egui::Layout::top_down(Align::Min),
842 |ui| {
843 ui.set_max_width(w);
844 eframe::egui::Frame::group(ui.style()).show(ui, |ui| {
845 self.path.push(box_index);
846 self.blocks(ui, box_, w - 16.0 * zoom);
847 self.path.pop();
848 });
849 },
850 );
851 return;
852 }
853
854 let w = declared("width").unwrap_or(width).min(width);
855 let h = declared("height").unwrap_or(48.0 * zoom);
856 let (rect, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover());
857 ui.painter().rect_stroke(
858 rect,
859 2.0,
860 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
861 StrokeKind::Inside,
862 );
863 }
864
865 fn page_break(&mut self, ui: &mut Ui, width: f32) {
870 ui.add_space(8.0 * self.zoom);
871 let (rect, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
872 ui.painter().hline(
873 rect.x_range(),
874 rect.center().y,
875 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
876 );
877 ui.add_space(8.0 * self.zoom);
878 }
879}
880
881fn merge(parent: &TextProperties, child: &TextProperties) -> TextProperties {
884 TextProperties {
885 font_family: child
886 .font_family
887 .clone()
888 .or_else(|| parent.font_family.clone()),
889 size: child.size.or(parent.size),
890 bold: child.bold.or(parent.bold),
891 italic: child.italic.or(parent.italic),
892 underline: child.underline.or(parent.underline),
893 strike: child.strike.or(parent.strike),
894 color: child.color.or(parent.color),
895 background: child.background.or(parent.background),
896 position: child.position.or(parent.position),
897 uppercase: child.uppercase.or(parent.uppercase),
898 }
899}
900
901#[derive(Clone, Copy)]
903enum Edge {
904 Left,
905 Right,
906 Top,
907 Bottom,
908}
909
910fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
911 [
912 (Edge::Left, rect.left_top(), rect.left_bottom()),
913 (Edge::Right, rect.right_top(), rect.right_bottom()),
914 (Edge::Top, rect.left_top(), rect.right_top()),
915 (Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
916 ]
917}
918
919fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
920 match edge {
921 Edge::Left => borders.left,
922 Edge::Right => borders.right,
923 Edge::Top => borders.top,
924 Edge::Bottom => borders.bottom,
925 }
926}
927
928fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
929 for (edge, from, to) in edges(rect) {
930 if let Some(border) = edge_of(borders, edge) {
931 ui.painter().line_segment(
932 [from, to],
933 Stroke::new(
934 (border.width.points() * zoom).max(1.0),
935 format::color32(border.color),
936 ),
937 );
938 }
939 }
940}
941
942fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
947 let mut declared = Vec::new();
948 collect_columns(document, table, &mut declared);
949 if declared.is_empty() {
950 return Vec::new();
951 }
952 let total: f32 = declared.iter().filter_map(|w| *w).sum();
953 let unstated = declared.iter().filter(|w| w.is_none()).count();
954
955 let scaled = total * zoom;
958 let factor = if scaled > width && scaled > 0.0 {
959 width / scaled
960 } else {
961 1.0
962 };
963 #[allow(clippy::cast_precision_loss)]
964 let share = if unstated == 0 {
965 0.0
966 } else {
967 ((width - scaled * factor) / unstated as f32).max(16.0)
968 };
969 declared
970 .into_iter()
971 .map(|w| w.map_or(share, |points| points * zoom * factor))
972 .collect()
973}
974
975fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
976 for element in parent.elements() {
977 if element.is(&Ns::Table, "table-column") {
978 let repeat = element
979 .attr_usize(&Ns::Table, "number-columns-repeated")
980 .unwrap_or(1)
981 .clamp(1, 1024);
982 let width = element
983 .attr(&Ns::Table, "style-name")
984 .map(|name| document.styles.resolve(&Family::TableColumn, name))
985 .and_then(|p| p.column_width)
986 .map(odox_core::Length::points);
987 for _ in 0..repeat {
988 into.push(width);
989 }
990 } else if element.is(&Ns::Table, "table-columns")
991 || element.is(&Ns::Table, "table-header-columns")
992 || element.is(&Ns::Table, "table-column-group")
993 {
994 collect_columns(document, element, into);
995 }
996 }
997}
998
999fn number_text(number: usize, format: &str) -> String {
1001 match format.chars().next() {
1002 Some('a') => alphabetic(number, b'a'),
1003 Some('A') => alphabetic(number, b'A'),
1004 Some('i') => roman(number).to_lowercase(),
1005 Some('I') => roman(number),
1006 None => String::new(),
1009 _ => number.to_string(),
1010 }
1011}
1012
1013fn alphabetic(number: usize, first: u8) -> String {
1015 let mut n = number;
1016 let mut out = Vec::new();
1017 while n > 0 {
1018 let remainder = (n - 1) % 26;
1019 out.push(first + u8::try_from(remainder).unwrap_or(0));
1020 n = (n - 1) / 26;
1021 }
1022 out.reverse();
1023 String::from_utf8(out).unwrap_or_default()
1024}
1025
1026fn roman(number: usize) -> String {
1027 const VALUES: [(usize, &str); 13] = [
1028 (1000, "M"),
1029 (900, "CM"),
1030 (500, "D"),
1031 (400, "CD"),
1032 (100, "C"),
1033 (90, "XC"),
1034 (50, "L"),
1035 (40, "XL"),
1036 (10, "X"),
1037 (9, "IX"),
1038 (5, "V"),
1039 (4, "IV"),
1040 (1, "I"),
1041 ];
1042 if number == 0 || number > 3999 {
1045 return number.to_string();
1046 }
1047 let mut left = number;
1048 let mut out = String::new();
1049 for (value, numeral) in VALUES {
1050 while left >= value {
1051 out.push_str(numeral);
1052 left -= value;
1053 }
1054 }
1055 out
1056}
1057
1058pub(crate) fn is_block_container(element: &Element) -> bool {
1060 element.name.ns == Ns::Text
1061 && matches!(
1062 &*element.name.local,
1063 "section"
1064 | "index-body"
1065 | "index-title"
1066 | "table-of-content"
1067 | "illustration-index"
1068 | "table-index"
1069 | "object-index"
1070 | "user-index"
1071 | "alphabetical-index"
1072 | "bibliography"
1073 | "tracked-changes"
1074 | "deletion"
1075 )
1076}
1077
1078fn is_inline_passthrough(element: &Element) -> bool {
1084 element.name.ns == Ns::Text
1085 && matches!(
1086 &*element.name.local,
1087 "bookmark"
1088 | "bookmark-start"
1089 | "bookmark-end"
1090 | "reference-mark"
1091 | "reference-mark-start"
1092 | "reference-mark-end"
1093 | "span"
1094 | "bibliography-mark"
1095 | "ruby"
1096 | "ruby-base"
1097 | "meta"
1098 | "meta-field"
1099 | "change-start"
1100 | "change-end"
1101 | "page-number"
1102 | "page-count"
1103 | "title"
1104 | "subject"
1105 | "author-name"
1106 | "author-initials"
1107 | "chapter"
1108 | "file-name"
1109 | "sheet-name"
1110 | "date"
1111 | "time"
1112 | "creator"
1113 | "description"
1114 | "keywords"
1115 | "sequence"
1116 | "bookmark-ref"
1117 | "sequence-ref"
1118 | "reference-ref"
1119 | "variable-get"
1120 | "variable-set"
1121 | "user-field-get"
1122 | "placeholder"
1123 | "conditional-text"
1124 | "hidden-text"
1125 | "text-input"
1126 )
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use std::path::Path;
1132
1133 use super::{Flow, Pictures};
1134 use crate::format::{self, DEFAULT_SIZE};
1135 use odox_core::{Document, Element, Family, edit, media_type};
1136
1137 const ANY: &[&str] = &[
1138 media_type::TEXT,
1139 media_type::TEXT_WEB,
1140 media_type::SPREADSHEET,
1141 media_type::PRESENTATION,
1142 ];
1143
1144 fn documents(directory: &Path, into: &mut Vec<Document>) {
1145 let Ok(entries) = std::fs::read_dir(directory) else {
1146 return;
1147 };
1148 for path in entries.filter_map(Result::ok).map(|entry| entry.path()) {
1149 if path.is_dir() {
1150 documents(&path, into);
1151 } else if matches!(
1152 path.extension().and_then(|e| e.to_str()),
1153 Some("odt" | "ods" | "odp")
1154 ) {
1155 let bytes = std::fs::read(&path).expect("a corpus document reads");
1156 into.push(Document::read(&bytes, ANY).expect("a corpus document parses"));
1157 }
1158 }
1159 }
1160
1161 fn paragraphs<'a>(element: &'a Element, into: &mut Vec<&'a Element>) {
1162 for child in element.elements() {
1163 if edit::is_paragraph(child) {
1164 into.push(child);
1165 }
1166 paragraphs(child, into);
1167 }
1168 }
1169
1170 #[test]
1174 fn what_is_drawn_maps_onto_the_text_an_edit_changes() {
1175 let mut corpus = Vec::new();
1176 documents(
1177 &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"),
1178 &mut corpus,
1179 );
1180 assert!(!corpus.is_empty(), "the corpus is where it was");
1181 let mut checked = 0;
1182 for document in &corpus {
1183 let mut pictures = Pictures::default();
1184 let flow = Flow::new(document, &mut pictures, 1.0);
1185 let mut found = Vec::new();
1186 paragraphs(&document.content, &mut found);
1187 for paragraph in found {
1188 let properties = flow.style_of(paragraph, &Family::Paragraph);
1189 let base = format::text_format(&properties.text, DEFAULT_SIZE, 1.0, flow.palette);
1190 let size = format::size_of(&properties.text, DEFAULT_SIZE);
1191 let (job, _) = flow.layout_job(paragraph, &properties, &base, size, 400.0);
1192 let (_, map) = job.into_parts();
1193 let text = edit::text(paragraph);
1194 assert_eq!(map.model_len(), text.chars().count(), "{text:?}");
1195 checked += 1;
1196 }
1197 }
1198 assert!(checked > 0);
1199 }
1200}