Skip to main content

mathtex_editor_session/
cache.rs

1use mathtex_editor_core::{CaretPath, Editor, Point, RenderOutput, Source};
2use mathtex_engine::font::FontLoader;
3use mathtex_engine::{Diagnostic, HostBoxes, MathMode, Typeset, TypesetError, Typesetter};
4use mathtex_ir::Fragment;
5
6/// A typeset editor state with the geometry to paint over it.
7#[derive(Debug, Clone, PartialEq)]
8pub struct View {
9    /// The laid out source.
10    pub fragment: Fragment,
11    /// Caret, selection, placeholders, menu anchor and host box placements over `fragment`.
12    pub render: RenderOutput,
13    /// The LaTeX that was typeset, with placeholders in empty slots.
14    pub tex: String,
15    /// Warnings TeX and the host boxes raised.
16    pub warnings: Vec<Diagnostic>,
17}
18
19/// A source and its typeset fragment, or why it failed to typeset.
20type Layout = Result<(Source, Typeset), TypesetError>;
21
22/// Which editor state a cached layout or view belongs to.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24struct Key {
25    revision: u64,
26    epoch: u64,
27}
28
29/// Typesets an editor once per revision and keeps the geometry until the caret state changes.
30#[derive(Debug, Default)]
31pub struct RenderCache {
32    epoch: u64,
33    layout: Option<(Key, Layout)>,
34    /// The view and the editor serial it was drawn at.
35    view: Option<(Key, u64, View)>,
36}
37
38impl RenderCache {
39    /// An empty cache.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Forget everything, for a change the revision does not show such as a host box size or another editor.
45    pub fn invalidate(&mut self) {
46        self.epoch += 1;
47        self.layout = None;
48        self.view = None;
49    }
50
51    /// The view of `editor`, typesetting only when its revision changed since the last call.
52    pub fn view<L: FontLoader>(
53        &mut self,
54        editor: &Editor,
55        typesetter: &mut Typesetter<L>,
56        mode: MathMode,
57        boxes: &dyn HostBoxes,
58    ) -> Result<&View, TypesetError> {
59        let key = self.key(editor);
60        let serial = editor.input_context().serial;
61        let view = match self.view.take() {
62            Some((k, s, view)) if (k, s) == (key, serial) => view,
63            _ => self.draw(editor, typesetter, mode, boxes)?,
64        };
65        Ok(&self.view.insert((key, serial, view)).2)
66    }
67
68    /// The caret under `at`, in the coordinates of [`View::render`].
69    pub fn hit_test<L: FontLoader>(
70        &mut self,
71        editor: &Editor,
72        typesetter: &mut Typesetter<L>,
73        mode: MathMode,
74        boxes: &dyn HostBoxes,
75        at: Point,
76    ) -> Result<Option<CaretPath>, TypesetError> {
77        let (source, typeset) = self.layout(editor, typesetter, mode, boxes)?;
78        Ok(editor.hit_test(source, &typeset.fragment, at).ok().flatten())
79    }
80
81    fn key(&self, editor: &Editor) -> Key {
82        Key { revision: editor.revision(), epoch: self.epoch }
83    }
84
85    fn draw<L: FontLoader>(
86        &mut self,
87        editor: &Editor,
88        typesetter: &mut Typesetter<L>,
89        mode: MathMode,
90        boxes: &dyn HostBoxes,
91    ) -> Result<View, TypesetError> {
92        for _ in 0..2 {
93            let (source, typeset) = self.layout(editor, typesetter, mode, boxes)?;
94            if let Ok(render) = editor.render(source, &typeset.fragment) {
95                return Ok(View {
96                    fragment: typeset.fragment.clone(),
97                    render,
98                    tex: source.tex.clone(),
99                    warnings: typeset.warnings.clone(),
100                });
101            }
102            // The layout came from another editor at the same revision, so typeset this one.
103            self.invalidate();
104        }
105        Err(TypesetError::Format { message: "the exported source does not match its editor".into() })
106    }
107
108    fn layout<L: FontLoader>(
109        &mut self,
110        editor: &Editor,
111        typesetter: &mut Typesetter<L>,
112        mode: MathMode,
113        boxes: &dyn HostBoxes,
114    ) -> Result<&(Source, Typeset), TypesetError> {
115        let key = self.key(editor);
116        if self.layout.as_ref().is_some_and(|(k, _)| *k != key) {
117            self.layout = None;
118        }
119        let (_, result) = self.layout.get_or_insert_with(|| {
120            let source = editor.source();
121            (key, typesetter.typeset(&source.tex, mode, boxes).map(|t| (source, t)))
122        });
123        result.as_ref().map_err(Clone::clone)
124    }
125}