1use std::collections::HashMap;
17
18use eframe::egui::{
19 Align, ColorImage, Context, Id, Key, Pos2, Rect, Sense, Stroke, StrokeKind, TextEdit,
20 TextFormat, TextureHandle, TextureOptions, Ui, pos2,
21 text::{CCursor, CCursorRange, CharIndex, LayoutJob},
22 text_selection::LabelSelectionState,
23 vec2,
24};
25use odox_core::{
26 Border, Document, Element, Family, Node, Ns, Properties, TextAlign, TextProperties,
27};
28
29use crate::format::{self, DEFAULT_SIZE};
30
31#[derive(Default)]
36pub struct Pictures {
37 textures: HashMap<String, Option<TextureHandle>>,
38}
39
40impl Pictures {
41 pub fn get(
43 &mut self,
44 ctx: &Context,
45 document: &Document,
46 href: &str,
47 ) -> Option<&TextureHandle> {
48 if !self.textures.contains_key(href) {
49 let texture = document
50 .picture(href)
51 .and_then(decode)
52 .map(|image| ctx.load_texture(href, image, TextureOptions::LINEAR));
53 self.textures.insert(href.to_owned(), texture);
54 }
55 self.textures.get(href).and_then(Option::as_ref)
56 }
57
58 pub fn clear(&mut self) {
60 self.textures.clear();
61 }
62}
63
64fn decode(bytes: &[u8]) -> Option<ColorImage> {
65 let decoded = image::load_from_memory(bytes).ok()?.to_rgba8();
66 let size = [decoded.width() as usize, decoded.height() as usize];
67 Some(ColorImage::from_rgba_unmultiplied(size, decoded.as_raw()))
68}
69
70pub struct Flow<'a> {
72 pub document: &'a Document,
74 pub pictures: &'a mut Pictures,
76 pub zoom: f32,
78 pub palette: format::Palette,
80 pub scroll_to_heading: Option<usize>,
86 headings_seen: usize,
88 pub edit_mode: bool,
90 pub editor: Option<&'a mut Editor>,
93 pub clicked: Option<Vec<usize>>,
96 pub outcome: Option<Outcome>,
98 pub selectable: bool,
101 path: Vec<usize>,
104 detached: bool,
107}
108
109impl<'a> Flow<'a> {
110 pub fn new(document: &'a Document, pictures: &'a mut Pictures, zoom: f32) -> Self {
112 Self {
113 document,
114 pictures,
115 zoom,
116 palette: format::Palette::default(),
117 scroll_to_heading: None,
118 headings_seen: 0,
119 edit_mode: false,
120 editor: None,
121 clicked: None,
122 outcome: None,
123 selectable: true,
124 path: Vec::new(),
125 detached: false,
126 }
127 }
128
129 pub fn start_at(&mut self, prefix: Vec<usize>) {
133 self.path = prefix;
134 }
135}
136
137pub struct Editor {
145 pub path: Vec<usize>,
147 pub text: String,
149 pub original: String,
152 pub opened: bool,
154 pub caret: Option<usize>,
156}
157
158impl Editor {
159 pub fn open(path: Vec<usize>, text: String, caret: Option<usize>) -> Self {
161 Self {
162 path,
163 original: text.clone(),
164 text,
165 opened: true,
166 caret,
167 }
168 }
169
170 pub fn changed(&self) -> bool {
172 self.text != self.original
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum Outcome {
179 Commit,
181 Cancel,
183 JoinPrevious,
186}
187
188const LIST_STEP: f32 = 18.0;
190const LIST_GUTTER: f32 = 20.0;
192const LABEL_GAP: f32 = 5.0;
194
195struct Spacing {
197 left: f32,
199 right: f32,
201 indent: f32,
203 before: f32,
205 after: f32,
207}
208
209impl Spacing {
210 fn of(properties: &odox_core::ParagraphProperties, zoom: f32) -> Self {
211 let points = |length: Option<odox_core::Length>| {
212 length.map_or(0.0, odox_core::Length::points) * zoom
213 };
214 Self {
215 left: points(properties.margin.left),
216 right: points(properties.margin.right),
217 indent: points(properties.text_indent),
218 before: points(properties.margin.top),
219 after: points(properties.margin.bottom),
220 }
221 }
222}
223
224#[derive(Clone, Copy)]
229struct Run<'a> {
230 inherited: &'a TextProperties,
232 size: f32,
235 format: &'a TextFormat,
237 palette: format::Palette,
239}
240
241#[derive(Default)]
243struct Counters(Vec<usize>);
244
245impl Counters {
246 fn bump(&mut self, level: usize, start: usize) -> usize {
247 while self.0.len() <= level {
248 self.0.push(0);
249 }
250 if self.0[level] == 0 {
253 self.0[level] = start;
254 } else {
255 self.0[level] += 1;
256 }
257 self.0.truncate(level + 1);
260 self.0[level]
261 }
262
263 fn at(&self, level: usize) -> usize {
264 self.0.get(level).copied().unwrap_or(1)
265 }
266}
267
268impl Flow<'_> {
269 pub fn blocks(&mut self, ui: &mut Ui, parent: &Element, width: f32) {
274 let mut counters = Counters::default();
275 self.blocks_with(ui, parent, width, &mut counters);
276 }
277
278 fn blocks_with(&mut self, ui: &mut Ui, parent: &Element, width: f32, counters: &mut Counters) {
279 for (index, element) in parent.elements_indexed() {
280 self.path.push(index);
281 match () {
282 () if element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h") => {
283 self.paragraph(ui, element, width, None);
284 }
285 () if element.is(&Ns::Text, "list") => {
286 let continues = element.attr(&Ns::Text, "continue-numbering") == Some("true")
291 || element.attr(&Ns::Text, "continue-list").is_some();
292 if !continues {
293 *counters = Counters::default();
294 }
295 self.list(ui, element, width, 0, None, counters);
296 }
297 () if element.is(&Ns::Table, "table") => self.table(ui, element, width),
298 () if element.is(&Ns::Draw, "frame") => self.frame(ui, element, width),
299 () if element.is(&Ns::Text, "soft-page-break") => self.page_break(ui, width),
300 () if is_block_container(element) => {
301 self.blocks_with(ui, element, width, counters);
302 }
303 () => {}
304 }
305 self.path.pop();
306 }
307 }
308
309 fn style_of(&self, element: &Element, family: &Family) -> std::rc::Rc<Properties> {
311 let name = element
312 .attr(&Ns::Text, "style-name")
313 .or_else(|| element.attr(&Ns::Table, "style-name"))
314 .unwrap_or("Standard");
315 self.document.styles.resolve(family, name)
316 }
317
318 fn paragraph(&mut self, ui: &mut Ui, element: &Element, width: f32, label: Option<&str>) {
320 let properties = self.style_of(element, &Family::Paragraph);
321 let zoom = self.zoom;
322 let size = format::size_of(&properties.text, DEFAULT_SIZE);
323
324 let Spacing {
325 left,
326 right,
327 indent,
328 before,
329 after,
330 } = Spacing::of(&properties.paragraph, zoom);
331
332 let body_width = (width - left - right).max(1.0);
336 let wrap = (body_width - indent.max(0.0)).max(1.0);
337
338 if before > 0.0 {
339 ui.add_space(before);
340 }
341
342 let base = format::text_format(&properties.text, DEFAULT_SIZE, zoom, self.palette);
343
344 if !self.detached && self.editor.as_ref().is_some_and(|e| e.path == self.path) {
345 let (job, _) = self.layout_job(element, &properties, &base, size, wrap);
348 let first = job
349 .sections
350 .first()
351 .map_or_else(|| base.clone(), |section| section.format.clone());
352 let mut editor = self.editor.take();
355 if let Some(editor) = editor.as_deref_mut() {
356 self.edit_box(ui, editor, &first, left, wrap);
357 }
358 self.editor = editor;
359 if after > 0.0 {
360 ui.add_space(after);
361 }
362 return;
363 }
364 let (job, frames) = self.layout_job(element, &properties, &base, size, wrap);
365
366 let galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(job));
367 let height = galley.size().y;
368 let sense = if self.selectable {
375 Sense::click_and_drag()
376 } else {
377 Sense::click()
378 };
379 let (rect, response) = ui.allocate_exact_size(vec2(width, height), sense);
380
381 if self.edit_mode && !self.detached && response.clicked() {
382 self.clicked = Some(self.path.clone());
383 }
384
385 if element.is(&Ns::Text, "h") {
386 if self.scroll_to_heading == Some(self.headings_seen) {
387 ui.scroll_to_rect(rect, Some(Align::TOP));
388 }
389 self.headings_seen += 1;
390 }
391
392 if let Some(fill) = properties.paragraph.background {
393 ui.painter().rect_filled(rect, 0.0, format::color32(fill));
394 }
395 paint_borders(ui, rect, &properties.paragraph.border, zoom);
396
397 let anchor = match properties.paragraph.align.unwrap_or(TextAlign::Start) {
401 TextAlign::Center => rect.left() + left + indent.max(0.0) + wrap / 2.0,
402 TextAlign::End => rect.left() + left + indent.max(0.0) + wrap,
403 _ => rect.left() + left + indent.max(0.0),
404 };
405 LabelSelectionState::label_text_selection(
413 ui,
414 &response,
415 pos2(anchor, rect.top()),
416 galley,
417 base.color,
418 Stroke::NONE,
419 );
420
421 if let Some(label) = label {
422 let mut label_job = LayoutJob::default();
423 label_job.append(label, 0.0, base.clone());
424 let label_galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(label_job));
425 let x = rect.left() + left - label_galley.size().x - LABEL_GAP * zoom;
429 ui.painter()
430 .galley(pos2(x, rect.top()), label_galley, base.color);
431 }
432
433 let was_detached = self.detached;
436 self.detached = true;
437 for frame in frames {
438 self.frame(ui, &frame, body_width);
439 }
440 self.detached = was_detached;
441 if after > 0.0 {
442 ui.add_space(after);
443 }
444 }
445
446 fn layout_job(
449 &self,
450 element: &Element,
451 properties: &Properties,
452 base: &TextFormat,
453 size: f32,
454 wrap: f32,
455 ) -> (LayoutJob, Vec<Element>) {
456 let mut job = LayoutJob {
457 wrap: eframe::egui::text::TextWrapping {
458 max_width: wrap,
459 ..Default::default()
460 },
461 halign: match properties.paragraph.align.unwrap_or(TextAlign::Start) {
462 TextAlign::Center => Align::Center,
463 TextAlign::End => Align::Max,
464 _ => Align::Min,
465 },
466 justify: properties.paragraph.align == Some(TextAlign::Justify),
467 ..LayoutJob::default()
468 };
469 let mut frames = Vec::new();
470 let run = Run {
471 inherited: &properties.text,
472 size,
473 format: base,
474 palette: self.palette,
475 };
476 self.runs(element, &run, &mut job, &mut frames);
477
478 if job.text.is_empty() {
481 job.append(" ", 0.0, base.clone());
482 }
483 if let Some(measure) = properties.paragraph.line_height {
484 for section in &mut job.sections {
488 let own = section.format.font_id.size / self.zoom;
489 section.format.line_height = Some(measure.resolve(own) * self.zoom);
490 }
491 }
492 (job, frames)
493 }
494
495 fn edit_box(
504 &mut self,
505 ui: &mut Ui,
506 editor: &mut Editor,
507 base: &TextFormat,
508 left: f32,
509 wrap: f32,
510 ) {
511 let id = Id::new("paragraph-editor");
512 let at_start = TextEdit::load_state(ui.ctx(), id)
513 .and_then(|state| state.cursor.char_range())
514 .is_some_and(|range| {
515 range.primary.index == CharIndex(0) && range.secondary.index == CharIndex(0)
516 });
517 let (backspace, keep) = ui.input(|input| {
518 (
519 input.key_pressed(Key::Backspace),
520 input.modifiers.command && input.key_pressed(Key::Enter),
521 )
522 });
523 if !editor.opened && at_start && backspace && self.outcome.is_none() {
524 self.outcome = Some(Outcome::JoinPrevious);
525 }
526
527 if ui.is_sizing_pass() {
531 let galley = ui.fonts_mut(|fonts| {
532 fonts.layout(editor.text.clone(), base.font_id.clone(), base.color, wrap)
533 });
534 ui.horizontal_top(|ui| {
535 ui.add_space(left);
536 ui.allocate_exact_size(vec2(wrap, galley.size().y + 4.0), Sense::hover());
537 });
538 return;
539 }
540
541 let response = ui
542 .horizontal_top(|ui| {
543 ui.add_space(left);
544 ui.add(
545 TextEdit::multiline(&mut editor.text)
546 .id(id)
547 .font(base.font_id.clone())
548 .text_color(base.color)
549 .background_color(self.palette.paper)
550 .desired_width(wrap)
551 .desired_rows(1)
552 .margin(vec2(2.0, 2.0)),
553 )
554 })
555 .inner;
556
557 if editor.opened {
558 editor.opened = false;
559 response.request_focus();
560 let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
561 let at = editor
562 .caret
563 .unwrap_or_else(|| editor.text.chars().count())
564 .min(editor.text.chars().count());
565 state
566 .cursor
567 .set_char_range(Some(CCursorRange::one(CCursor::new(at))));
568 TextEdit::store_state(ui.ctx(), id, state);
569 return;
570 }
571 if keep && response.has_focus() {
572 response.surrender_focus();
573 self.outcome = Some(Outcome::Commit);
574 } else if response.lost_focus() && self.outcome.is_none() {
575 self.outcome = Some(if ui.input(|input| input.key_pressed(Key::Escape)) {
576 Outcome::Cancel
577 } else {
578 Outcome::Commit
579 });
580 }
581 }
582
583 fn runs(
590 &self,
591 parent: &Element,
592 run: &Run<'_>,
593 job: &mut LayoutJob,
594 frames: &mut Vec<Element>,
595 ) {
596 let Run {
597 inherited,
598 size,
599 format,
600 palette,
601 } = *run;
602 for child in &parent.children {
603 match child {
604 Node::Text(text) | Node::CData(text) => job.append(text, 0.0, format.clone()),
605 Node::Comment(_) | Node::ProcessingInstruction(_) => {}
606 Node::Element(element) => {
607 if element.is(&Ns::Text, "s") {
608 let count = element.attr_usize(&Ns::Text, "c").unwrap_or(1).min(256);
609 job.append(&" ".repeat(count), 0.0, format.clone());
610 } else if element.is(&Ns::Text, "tab") {
611 job.append(" ", 0.0, format.clone());
616 } else if element.is(&Ns::Text, "line-break") {
617 job.append("\n", 0.0, format.clone());
618 } else if element.is(&Ns::Text, "span") {
619 let style = self.style_of(element, &Family::Text);
620 let merged = merge(inherited, &style.text);
621 let inner_size = format::size_of(&merged, size);
622 let inner = format::text_format(&merged, size, self.zoom, palette);
623 let run = Run {
624 inherited: &merged,
625 size: inner_size,
626 format: &inner,
627 palette,
628 };
629 self.runs(element, &run, job, frames);
630 } else if element.is(&Ns::Text, "a") {
631 let style = self.style_of(element, &Family::Text);
632 let merged = merge(inherited, &style.text);
633 let inner_size = format::size_of(&merged, size);
634 let inner = format::link_format(&merged, size, self.zoom, palette);
635 let run = Run {
636 inherited: &merged,
637 size: inner_size,
638 format: &inner,
639 palette,
640 };
641 self.runs(element, &run, job, frames);
642 } else if element.is(&Ns::Draw, "frame") {
643 frames.push(element.clone());
644 } else if element.is(&Ns::Text, "note") {
645 if let Some(citation) = element.child(&Ns::Text, "note-citation") {
649 let mut raised = format.clone();
650 raised.font_id.size *= 0.7;
651 raised.valign = Align::TOP;
652 job.append(&citation.plain_text(), 0.0, raised);
653 }
654 } else if is_inline_passthrough(element) {
655 self.runs(element, run, job, frames);
656 }
657 }
658 }
659 }
660 }
661
662 fn list(
664 &mut self,
665 ui: &mut Ui,
666 element: &Element,
667 width: f32,
668 level: usize,
669 inherited_style: Option<&str>,
670 counters: &mut Counters,
671 ) {
672 let style_name = element
673 .attr(&Ns::Text, "style-name")
674 .or(inherited_style)
675 .unwrap_or_default()
676 .to_owned();
677 #[allow(clippy::cast_precision_loss)]
681 let indent = LIST_STEP * self.zoom * (level as f32 + 1.0);
682 let gutter = LIST_GUTTER * self.zoom;
683
684 for (item_index, item) in element.elements_indexed() {
685 let numbered = item.is(&Ns::Text, "list-item");
686 if !numbered && !item.is(&Ns::Text, "list-header") {
687 continue;
688 }
689 self.path.push(item_index);
690 let label = if numbered {
691 let level_style = self.level_style(&style_name, level);
692 let start = level_style
693 .as_ref()
694 .and_then(|s| s.attr_usize(&Ns::Text, "start-value"))
695 .unwrap_or(1);
696 let number = counters.bump(level, start);
697 Self::label(level_style.as_ref(), level, number, counters)
698 } else {
699 String::new()
702 };
703
704 let mut first = true;
705 for (block_index, block) in item.elements_indexed() {
706 self.path.push(block_index);
707 if block.is(&Ns::Text, "list") {
708 self.list(ui, block, width, level + 1, Some(&style_name), counters);
709 self.path.pop();
710 continue;
711 }
712 let on_this_block = if first && !label.is_empty() {
713 Some(label.as_str())
714 } else {
715 None
716 };
717 if block.is(&Ns::Text, "p") || block.is(&Ns::Text, "h") {
718 first = false;
719 ui.horizontal_top(|ui| {
723 ui.add_space(indent + gutter);
724 self.paragraph(ui, block, width - indent - gutter, on_this_block);
725 });
726 } else if block.is(&Ns::Table, "table") {
727 first = false;
728 ui.horizontal_top(|ui| {
729 ui.add_space(indent + gutter);
730 self.table(ui, block, width - indent - gutter);
731 });
732 } else if block.is(&Ns::Draw, "frame") {
733 first = false;
734 ui.horizontal_top(|ui| {
735 ui.add_space(indent + gutter);
736 self.frame(ui, block, width - indent - gutter);
737 });
738 }
739 self.path.pop();
740 }
741 self.path.pop();
742 }
743 }
744
745 fn level_style(&self, list_style: &str, level: usize) -> Option<Element> {
748 let style = self.document.styles.list_style(list_style)?;
749 style
750 .elements()
751 .find(|e| e.attr_usize(&Ns::Text, "level") == Some(level + 1))
752 .cloned()
753 }
754
755 fn label(
757 level_style: Option<&Element>,
758 level: usize,
759 number: usize,
760 counters: &Counters,
761 ) -> String {
762 let Some(style) = level_style else {
763 return "\u{2022}".to_owned();
766 };
767 if style.is(&Ns::Text, "list-level-style-bullet") {
768 return style
769 .attr(&Ns::Text, "bullet-char")
770 .unwrap_or("\u{2022}")
771 .to_owned();
772 }
773 if style.is(&Ns::Text, "list-level-style-number") {
774 let format = style.attr(&Ns::Style, "num-format").unwrap_or("1");
775 let prefix = style.attr(&Ns::Text, "num-prefix").unwrap_or_default();
776 let suffix = style.attr(&Ns::Text, "num-suffix").unwrap_or_default();
777 let display = style
780 .attr_usize(&Ns::Text, "display-levels")
781 .unwrap_or(1)
782 .max(1);
783 let mut numbers = Vec::new();
784 let first = (level + 1).saturating_sub(display);
785 for ancestor in first..level {
786 numbers.push(number_text(counters.at(ancestor), format));
787 }
788 numbers.push(number_text(number, format));
789 return format!("{prefix}{}{suffix}", numbers.join("."));
790 }
791 "\u{2022}".to_owned()
793 }
794
795 fn table(&mut self, ui: &mut Ui, table: &Element, width: f32) {
797 let columns = column_widths(self.document, table, width, self.zoom);
798 if columns.is_empty() {
799 return;
800 }
801 ui.add_space(4.0 * self.zoom);
802 self.rows(ui, table, &columns);
803 ui.add_space(4.0 * self.zoom);
804 }
805
806 fn rows(&mut self, ui: &mut Ui, parent: &Element, columns: &[f32]) {
807 for (index, element) in parent.elements_indexed() {
808 self.path.push(index);
809 if element.is(&Ns::Table, "table-row") {
810 self.row(ui, element, columns);
811 } else if element.is(&Ns::Table, "table-header-rows")
812 || element.is(&Ns::Table, "table-rows")
813 || element.is(&Ns::Table, "table-row-group")
814 {
815 self.rows(ui, element, columns);
816 }
817 self.path.pop();
818 }
819 }
820
821 fn row(&mut self, ui: &mut Ui, row: &Element, columns: &[f32]) {
822 let reserved = ui.painter().add(eframe::egui::Shape::Noop);
826 let mut painted = Vec::new();
827
828 let response = ui.horizontal_top(|ui| {
829 ui.spacing_mut().item_spacing.x = 0.0;
830 let mut column = 0usize;
831 for (cell_index, cell) in row.elements_indexed() {
832 let covered = cell.is(&Ns::Table, "covered-table-cell");
833 if !covered && !cell.is(&Ns::Table, "table-cell") {
834 continue;
835 }
836 let repeat = cell
837 .attr_usize(&Ns::Table, "number-columns-repeated")
838 .unwrap_or(1)
839 .max(1);
840 let spanned = cell
841 .attr_usize(&Ns::Table, "number-columns-spanned")
842 .unwrap_or(1)
843 .max(1);
844 for _ in 0..repeat {
845 let width: f32 = columns
851 .iter()
852 .skip(column)
853 .take(spanned)
854 .sum::<f32>()
855 .max(8.0);
856 if !covered {
857 let properties = self.style_of(cell, &Family::TableCell);
858 let padding = properties
859 .cell
860 .padding
861 .left
862 .map_or(2.0, odox_core::Length::points)
863 * self.zoom;
864 let inner = ui
865 .allocate_ui_with_layout(
866 vec2(width, 0.0),
867 eframe::egui::Layout::top_down(Align::Min),
868 |ui| {
869 ui.add_space(padding);
870 ui.set_min_width(width);
871 ui.set_max_width(width);
872 let content = (width - padding * 2.0).max(8.0);
873 self.path.push(cell_index);
874 self.blocks(ui, cell, content);
875 self.path.pop();
876 ui.add_space(padding);
877 },
878 )
879 .response
880 .rect;
881 painted.push((inner, properties));
882 }
883 column += 1;
884 }
885 }
886 });
887
888 let row_rect = response.response.rect;
889 let mut shapes = Vec::new();
890 for (rect, properties) in painted {
891 let cell_rect = Rect::from_min_max(
892 pos2(rect.left(), row_rect.top()),
893 pos2(rect.right(), row_rect.bottom()),
894 );
895 if let Some(fill) = properties.cell.background {
896 shapes.push(eframe::egui::Shape::rect_filled(
897 cell_rect,
898 0.0,
899 format::color32(fill),
900 ));
901 }
902 for (edge, from, to) in edges(cell_rect) {
903 if let Some(border) = edge_of(&properties.cell.border, edge) {
904 shapes.push(eframe::egui::Shape::line_segment(
905 [from, to],
906 Stroke::new(
907 (border.width.points() * self.zoom).max(1.0),
908 format::color32(border.color),
909 ),
910 ));
911 }
912 }
913 }
914 ui.painter().set(reserved, eframe::egui::Shape::Vec(shapes));
915 }
916
917 pub fn frame(&mut self, ui: &mut Ui, frame: &Element, width: f32) {
924 let zoom = self.zoom;
925 let declared = |local: &str| {
926 frame
927 .attr(&Ns::Svg, local)
928 .and_then(odox_core::Length::parse)
929 .map(|l| l.points() * zoom)
930 };
931
932 let found = frame
939 .elements()
940 .filter(|child| child.is(&Ns::Draw, "image"))
941 .find_map(|image| {
942 let href = image.attr(&Ns::Xlink, "href")?.to_owned();
943 self.pictures
944 .get(ui.ctx(), self.document, &href)
945 .map(|texture| (texture.id(), texture.size()))
946 });
947 if let Some((id, [pixels_wide, pixels_high])) = found {
948 #[allow(clippy::cast_precision_loss)]
949 let aspect = if pixels_wide == 0 {
950 1.0
951 } else {
952 pixels_high as f32 / pixels_wide as f32
953 };
954 let w = declared("width").unwrap_or(width).min(width);
955 let h = declared("height").unwrap_or(w * aspect);
956 let size = vec2(w, h);
957 ui.add(eframe::egui::Image::new((id, size)).fit_to_exact_size(size));
958 return;
959 }
960
961 if let Some((box_index, box_)) = frame
965 .elements_indexed()
966 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
967 {
968 let w = declared("width").unwrap_or(width).min(width);
969 ui.allocate_ui_with_layout(
970 vec2(w, 0.0),
971 eframe::egui::Layout::top_down(Align::Min),
972 |ui| {
973 ui.set_max_width(w);
974 eframe::egui::Frame::group(ui.style()).show(ui, |ui| {
975 self.path.push(box_index);
976 self.blocks(ui, box_, w - 16.0 * zoom);
977 self.path.pop();
978 });
979 },
980 );
981 return;
982 }
983
984 let w = declared("width").unwrap_or(width).min(width);
985 let h = declared("height").unwrap_or(48.0 * zoom);
986 let (rect, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover());
987 ui.painter().rect_stroke(
988 rect,
989 2.0,
990 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
991 StrokeKind::Inside,
992 );
993 }
994
995 fn page_break(&mut self, ui: &mut Ui, width: f32) {
1000 ui.add_space(8.0 * self.zoom);
1001 let (rect, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
1002 ui.painter().hline(
1003 rect.x_range(),
1004 rect.center().y,
1005 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
1006 );
1007 ui.add_space(8.0 * self.zoom);
1008 }
1009}
1010
1011fn merge(parent: &TextProperties, child: &TextProperties) -> TextProperties {
1014 TextProperties {
1015 font_family: child
1016 .font_family
1017 .clone()
1018 .or_else(|| parent.font_family.clone()),
1019 size: child.size.or(parent.size),
1020 bold: child.bold.or(parent.bold),
1021 italic: child.italic.or(parent.italic),
1022 underline: child.underline.or(parent.underline),
1023 strike: child.strike.or(parent.strike),
1024 color: child.color.or(parent.color),
1025 background: child.background.or(parent.background),
1026 position: child.position.or(parent.position),
1027 uppercase: child.uppercase.or(parent.uppercase),
1028 }
1029}
1030
1031#[derive(Clone, Copy)]
1033enum Edge {
1034 Left,
1035 Right,
1036 Top,
1037 Bottom,
1038}
1039
1040fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
1041 [
1042 (Edge::Left, rect.left_top(), rect.left_bottom()),
1043 (Edge::Right, rect.right_top(), rect.right_bottom()),
1044 (Edge::Top, rect.left_top(), rect.right_top()),
1045 (Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
1046 ]
1047}
1048
1049fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
1050 match edge {
1051 Edge::Left => borders.left,
1052 Edge::Right => borders.right,
1053 Edge::Top => borders.top,
1054 Edge::Bottom => borders.bottom,
1055 }
1056}
1057
1058fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
1059 for (edge, from, to) in edges(rect) {
1060 if let Some(border) = edge_of(borders, edge) {
1061 ui.painter().line_segment(
1062 [from, to],
1063 Stroke::new(
1064 (border.width.points() * zoom).max(1.0),
1065 format::color32(border.color),
1066 ),
1067 );
1068 }
1069 }
1070}
1071
1072fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
1077 let mut declared = Vec::new();
1078 collect_columns(document, table, &mut declared);
1079 if declared.is_empty() {
1080 return Vec::new();
1081 }
1082 let total: f32 = declared.iter().filter_map(|w| *w).sum();
1083 let unstated = declared.iter().filter(|w| w.is_none()).count();
1084
1085 let scaled = total * zoom;
1088 let factor = if scaled > width && scaled > 0.0 {
1089 width / scaled
1090 } else {
1091 1.0
1092 };
1093 #[allow(clippy::cast_precision_loss)]
1094 let share = if unstated == 0 {
1095 0.0
1096 } else {
1097 ((width - scaled * factor) / unstated as f32).max(16.0)
1098 };
1099 declared
1100 .into_iter()
1101 .map(|w| w.map_or(share, |points| points * zoom * factor))
1102 .collect()
1103}
1104
1105fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
1106 for element in parent.elements() {
1107 if element.is(&Ns::Table, "table-column") {
1108 let repeat = element
1109 .attr_usize(&Ns::Table, "number-columns-repeated")
1110 .unwrap_or(1)
1111 .clamp(1, 1024);
1112 let width = element
1113 .attr(&Ns::Table, "style-name")
1114 .map(|name| document.styles.resolve(&Family::TableColumn, name))
1115 .and_then(|p| p.column_width)
1116 .map(odox_core::Length::points);
1117 for _ in 0..repeat {
1118 into.push(width);
1119 }
1120 } else if element.is(&Ns::Table, "table-columns")
1121 || element.is(&Ns::Table, "table-header-columns")
1122 || element.is(&Ns::Table, "table-column-group")
1123 {
1124 collect_columns(document, element, into);
1125 }
1126 }
1127}
1128
1129fn number_text(number: usize, format: &str) -> String {
1131 match format.chars().next() {
1132 Some('a') => alphabetic(number, b'a'),
1133 Some('A') => alphabetic(number, b'A'),
1134 Some('i') => roman(number).to_lowercase(),
1135 Some('I') => roman(number),
1136 None => String::new(),
1139 _ => number.to_string(),
1140 }
1141}
1142
1143fn alphabetic(number: usize, first: u8) -> String {
1145 let mut n = number;
1146 let mut out = Vec::new();
1147 while n > 0 {
1148 let remainder = (n - 1) % 26;
1149 out.push(first + u8::try_from(remainder).unwrap_or(0));
1150 n = (n - 1) / 26;
1151 }
1152 out.reverse();
1153 String::from_utf8(out).unwrap_or_default()
1154}
1155
1156fn roman(number: usize) -> String {
1157 const VALUES: [(usize, &str); 13] = [
1158 (1000, "M"),
1159 (900, "CM"),
1160 (500, "D"),
1161 (400, "CD"),
1162 (100, "C"),
1163 (90, "XC"),
1164 (50, "L"),
1165 (40, "XL"),
1166 (10, "X"),
1167 (9, "IX"),
1168 (5, "V"),
1169 (4, "IV"),
1170 (1, "I"),
1171 ];
1172 if number == 0 || number > 3999 {
1175 return number.to_string();
1176 }
1177 let mut left = number;
1178 let mut out = String::new();
1179 for (value, numeral) in VALUES {
1180 while left >= value {
1181 out.push_str(numeral);
1182 left -= value;
1183 }
1184 }
1185 out
1186}
1187
1188fn is_block_container(element: &Element) -> bool {
1190 element.name.ns == Ns::Text
1191 && matches!(
1192 &*element.name.local,
1193 "section"
1194 | "index-body"
1195 | "index-title"
1196 | "table-of-content"
1197 | "illustration-index"
1198 | "table-index"
1199 | "object-index"
1200 | "user-index"
1201 | "alphabetical-index"
1202 | "bibliography"
1203 | "tracked-changes"
1204 | "deletion"
1205 )
1206}
1207
1208fn is_inline_passthrough(element: &Element) -> bool {
1214 element.name.ns == Ns::Text
1215 && matches!(
1216 &*element.name.local,
1217 "bookmark"
1218 | "bookmark-start"
1219 | "bookmark-end"
1220 | "reference-mark"
1221 | "reference-mark-start"
1222 | "reference-mark-end"
1223 | "span"
1224 | "bibliography-mark"
1225 | "ruby"
1226 | "ruby-base"
1227 | "meta"
1228 | "meta-field"
1229 | "change-start"
1230 | "change-end"
1231 | "page-number"
1232 | "page-count"
1233 | "title"
1234 | "subject"
1235 | "author-name"
1236 | "author-initials"
1237 | "chapter"
1238 | "file-name"
1239 | "sheet-name"
1240 | "date"
1241 | "time"
1242 | "creator"
1243 | "description"
1244 | "keywords"
1245 | "sequence"
1246 | "bookmark-ref"
1247 | "sequence-ref"
1248 | "reference-ref"
1249 | "variable-get"
1250 | "variable-set"
1251 | "user-field-get"
1252 | "placeholder"
1253 | "conditional-text"
1254 | "hidden-text"
1255 | "text-input"
1256 )
1257}