mathtex-editor-session 0.3.0

Batteries included session for mathtex-editor: keymap, typesetting, undo, clipboard and host box tokens over the explicit core
Documentation
use mathtex_editor_core::{CaretPath, Editor, Point, RenderOutput, Source};
use mathtex_engine::font::FontLoader;
use mathtex_engine::{Diagnostic, HostBoxes, MathMode, Typeset, TypesetError, Typesetter};
use mathtex_ir::Fragment;

/// A typeset editor state with the geometry to paint over it.
#[derive(Debug, Clone, PartialEq)]
pub struct View {
    /// The laid out source.
    pub fragment: Fragment,
    /// Caret, selection, placeholders, menu anchor and host box placements over `fragment`.
    pub render: RenderOutput,
    /// The LaTeX that was typeset, with placeholders in empty slots.
    pub tex: String,
    /// Warnings TeX and the host boxes raised.
    pub warnings: Vec<Diagnostic>,
}

/// A source and its typeset fragment, or why it failed to typeset.
type Layout = Result<(Source, Typeset), TypesetError>;

/// Which editor state a cached layout or view belongs to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Key {
    revision: u64,
    epoch: u64,
}

/// Typesets an editor once per revision and keeps the geometry until the caret state changes.
#[derive(Debug, Default)]
pub struct RenderCache {
    epoch: u64,
    layout: Option<(Key, Layout)>,
    /// The view and the editor serial it was drawn at.
    view: Option<(Key, u64, View)>,
}

impl RenderCache {
    /// An empty cache.
    pub fn new() -> Self {
        Self::default()
    }

    /// Forget everything, for a change the revision does not show such as a host box size or another editor.
    pub fn invalidate(&mut self) {
        self.epoch += 1;
        self.layout = None;
        self.view = None;
    }

    /// The view of `editor`, typesetting only when its revision changed since the last call.
    pub fn view<L: FontLoader>(
        &mut self,
        editor: &Editor,
        typesetter: &mut Typesetter<L>,
        mode: MathMode,
        boxes: &dyn HostBoxes,
    ) -> Result<&View, TypesetError> {
        let key = self.key(editor);
        let serial = editor.input_context().serial;
        let view = match self.view.take() {
            Some((k, s, view)) if (k, s) == (key, serial) => view,
            _ => self.draw(editor, typesetter, mode, boxes)?,
        };
        Ok(&self.view.insert((key, serial, view)).2)
    }

    /// The caret under `at`, in the coordinates of [`View::render`].
    pub fn hit_test<L: FontLoader>(
        &mut self,
        editor: &Editor,
        typesetter: &mut Typesetter<L>,
        mode: MathMode,
        boxes: &dyn HostBoxes,
        at: Point,
    ) -> Result<Option<CaretPath>, TypesetError> {
        let (source, typeset) = self.layout(editor, typesetter, mode, boxes)?;
        Ok(editor.hit_test(source, &typeset.fragment, at).ok().flatten())
    }

    fn key(&self, editor: &Editor) -> Key {
        Key { revision: editor.revision(), epoch: self.epoch }
    }

    fn draw<L: FontLoader>(
        &mut self,
        editor: &Editor,
        typesetter: &mut Typesetter<L>,
        mode: MathMode,
        boxes: &dyn HostBoxes,
    ) -> Result<View, TypesetError> {
        for _ in 0..2 {
            let (source, typeset) = self.layout(editor, typesetter, mode, boxes)?;
            if let Ok(render) = editor.render(source, &typeset.fragment) {
                return Ok(View {
                    fragment: typeset.fragment.clone(),
                    render,
                    tex: source.tex.clone(),
                    warnings: typeset.warnings.clone(),
                });
            }
            // The layout came from another editor at the same revision, so typeset this one.
            self.invalidate();
        }
        Err(TypesetError::Format { message: "the exported source does not match its editor".into() })
    }

    fn layout<L: FontLoader>(
        &mut self,
        editor: &Editor,
        typesetter: &mut Typesetter<L>,
        mode: MathMode,
        boxes: &dyn HostBoxes,
    ) -> Result<&(Source, Typeset), TypesetError> {
        let key = self.key(editor);
        if self.layout.as_ref().is_some_and(|(k, _)| *k != key) {
            self.layout = None;
        }
        let (_, result) = self.layout.get_or_insert_with(|| {
            let source = editor.source();
            (key, typesetter.typeset(&source.tex, mode, boxes).map(|t| (source, t)))
        });
        result.as_ref().map_err(Clone::clone)
    }
}