Skip to main content

mathtex_editor_session/
lib.rs

1//! A complete math editor in one type: editor, keymap, typesetter, undo, clipboard and host box tokens.
2
3mod boxes;
4mod cache;
5mod tokens;
6mod undo;
7
8#[cfg(test)]
9mod tests;
10
11use std::collections::BTreeSet;
12use std::fmt;
13
14use mathtex_editor_core::{
15    CaretPath, Command, Dir, Document, Editor, ExitDir, HostBoxEntry, HostBoxPolicy, MenuView, Point, Repair,
16    Selection, Side,
17};
18use mathtex_editor_keymap::{KeyInput, Keymap};
19use mathtex_engine::font::FontLoader;
20use mathtex_engine::{HostBoxes, MathMode, TypesetError, Typesetter};
21use mathtex_ir::Length;
22
23use crate::boxes::SessionBoxes;
24pub use crate::cache::{RenderCache, View};
25pub use crate::tokens::{TokenError, TokenRegistry};
26pub use crate::undo::{DEFAULT_UNDO_LIMIT, UndoStack};
27
28/// What one session call did, for the host to act on.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Update {
32    /// The document changed.
33    pub changed: bool,
34    /// The caret tried to leave the formula in this direction.
35    pub exit: Option<ExitDir>,
36    /// Deleting in an empty formula asks the host to end math mode.
37    pub close: bool,
38    /// Motion stopped in front of a host box, see [`Session::step_over_host_box`].
39    pub entered_host_box: Option<HostBoxEntry>,
40    /// The document, caret, selection, or menu changed, so the view should be drawn again.
41    pub needs_redraw: bool,
42    /// `(old, new)` for every host box a paste gave a fresh token, in document order.
43    pub reminted: Vec<(u32, u32)>,
44}
45
46/// A selection as the two clipboard flavors a host writes.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ClipboardData {
49    /// The selection as versioned document JSON, which [`Session::paste_json`] reads back.
50    pub json: String,
51    /// The selection as clean LaTeX for other applications.
52    pub tex: String,
53}
54
55/// Why [`Session::paste_json`] pasted nothing.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum PasteError {
58    /// The text is not a valid document, with serde's message.
59    Json(String),
60    /// The document holds no nodes.
61    Empty,
62    /// No fresh tokens were left for its host boxes.
63    Tokens(TokenError),
64}
65
66impl fmt::Display for PasteError {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            PasteError::Json(e) => write!(f, "not a document: {e}"),
70            PasteError::Empty => write!(f, "the document is empty"),
71            PasteError::Tokens(e) => e.fmt(f),
72        }
73    }
74}
75
76impl std::error::Error for PasteError {}
77
78/// What the host sees, compared before and after a call to fill [`Update::needs_redraw`].
79#[derive(PartialEq)]
80struct Visible {
81    revision: u64,
82    cursor: CaretPath,
83    selection: Option<Selection>,
84    menu: Option<MenuView>,
85}
86
87/// An editor with keymap, typesetting, undo, clipboard, and host box tokens, driven by a few input calls.
88pub struct Session<L: FontLoader> {
89    editor: Editor,
90    keymap: Keymap,
91    typesetter: Typesetter<L>,
92    mode: MathMode,
93    /// The editor's host box policy, which the editor does not report back.
94    policy: HostBoxPolicy,
95    cache: RenderCache,
96    history: UndoStack,
97    tokens: TokenRegistry,
98    clipboard: Option<Document>,
99    boxes: SessionBoxes,
100}
101
102impl<L: FontLoader> fmt::Debug for Session<L> {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.debug_struct("Session")
105            .field("revision", &self.editor.revision())
106            .field("typesetter", &self.typesetter)
107            .field("mode", &self.mode)
108            .finish_non_exhaustive()
109    }
110}
111
112impl<L: FontLoader> Session<L> {
113    /// An empty editor typeset in display style by `typesetter`.
114    pub fn new(typesetter: Typesetter<L>) -> Self {
115        Self {
116            editor: Editor::new(),
117            keymap: Keymap::new(),
118            typesetter,
119            mode: MathMode::Display,
120            policy: HostBoxPolicy::Skip,
121            cache: RenderCache::new(),
122            history: UndoStack::default(),
123            tokens: TokenRegistry::new(),
124            clipboard: None,
125            boxes: SessionBoxes::default(),
126        }
127    }
128
129    /// Replace the document, repairing what [`Document::validate`] rejects, and clear the history.
130    pub fn load(&mut self, mut doc: Document) -> Vec<Repair> {
131        let repairs = doc.repair();
132        // A repaired document always validates, so the old editor stays only if that ever broke.
133        if let Ok(mut editor) = Editor::from_document(&doc) {
134            editor.set_host_box_policy(self.policy);
135            self.editor = editor;
136        }
137        self.tokens.reserve_document(&doc);
138        self.keymap.reset();
139        self.history.clear();
140        self.cache.invalidate();
141        repairs
142    }
143
144    /// The editor, for queries such as `document`, `menu`, and `matrix_shape`.
145    pub fn editor(&self) -> &Editor {
146        &self.editor
147    }
148
149    /// The keymap, for its palette entries.
150    pub fn keymap(&self) -> &Keymap {
151        &self.keymap
152    }
153
154    /// The keymap, for defining words and switching autocorrect.
155    pub fn keymap_mut(&mut self) -> &mut Keymap {
156        &mut self.keymap
157    }
158
159    /// The typesetter, whose font loader draws the faces a view's glyph runs name.
160    pub fn typesetter(&self) -> &Typesetter<L> {
161        &self.typesetter
162    }
163
164    /// The undo history.
165    pub fn history(&self) -> &UndoStack {
166        &self.history
167    }
168
169    /// Keep at most `limit` undo steps.
170    pub fn set_undo_limit(&mut self, limit: usize) {
171        self.history.set_limit(limit);
172    }
173
174    /// Typeset inline or in display style.
175    pub fn set_math_mode(&mut self, mode: MathMode) {
176        self.mode = mode;
177        self.cache.invalidate();
178    }
179
180    /// Choose whether horizontal motion steps over host boxes or stops and reports them.
181    pub fn set_host_box_policy(&mut self, policy: HostBoxPolicy) {
182        self.policy = policy;
183        self.editor.set_host_box_policy(policy);
184    }
185
186    /// Feed a key event through the keymap, its commands form one undo step.
187    pub fn key(&mut self, input: &KeyInput) -> Update {
188        let ctx = self.editor.input_context();
189        let commands = self.keymap.map_key(input, &ctx);
190        self.run(commands)
191    }
192
193    /// Feed committed text such as IME output, as if each character were typed.
194    pub fn text(&mut self, s: &str) -> Update {
195        let ctx = self.editor.input_context();
196        let commands = self.keymap.map_text(s, &ctx);
197        self.run(commands)
198    }
199
200    /// Place the caret at `at`, or extend the selection to it, in the coordinates of [`View::render`].
201    pub fn pointer(&mut self, at: Point, extend: bool) -> Update {
202        self.keymap.reset();
203        let hit = self.cache.hit_test(&self.editor, &mut self.typesetter, self.mode, &self.boxes, at);
204        match hit {
205            Ok(Some(path)) if extend => self.run(vec![Command::ExtendTo(path)]),
206            Ok(Some(path)) => self.run(vec![Command::MoveTo(path)]),
207            _ => Update::default(),
208        }
209    }
210
211    /// Run one host command, such as a toolbar button or a menu row, as its own undo step.
212    pub fn command(&mut self, cmd: Command) -> Update {
213        self.keymap.reset();
214        match &cmd {
215            Command::InsertHostBox(token) => {
216                let _ = self.tokens.reserve(*token);
217            }
218            Command::InsertDocument(doc) => self.tokens.reserve_document(doc),
219            _ => {}
220        }
221        self.run(vec![cmd])
222    }
223
224    /// Insert what a palette word inserts, as one undo step like typing it.
225    pub fn commit_word(&mut self, word: &str) -> Update {
226        let Some(commands) = self.keymap.commands_for_word(word) else {
227            return Update::default();
228        };
229        self.keymap.reset();
230        self.run(commands)
231    }
232
233    /// Cross the host box a motion stopped at, when the host does not take the caret into it.
234    pub fn step_over_host_box(&mut self, entry: HostBoxEntry) -> Update {
235        let dir = match entry.side {
236            Side::Before => Dir::Right,
237            Side::After => Dir::Left,
238        };
239        self.editor.set_host_box_policy(HostBoxPolicy::Skip);
240        let update = self.command(Command::Move(dir));
241        self.editor.set_host_box_policy(self.policy);
242        update
243    }
244
245    /// Undo the last step.
246    pub fn undo(&mut self) -> Update {
247        self.keymap.reset();
248        let Some(target) = self.history.undo(self.editor.snapshot()) else {
249            return Update::default();
250        };
251        self.restore(&target)
252    }
253
254    /// Redo the last undone step.
255    pub fn redo(&mut self) -> Update {
256        self.keymap.reset();
257        let Some(target) = self.history.redo(self.editor.snapshot()) else {
258            return Update::default();
259        };
260        self.restore(&target)
261    }
262
263    fn restore(&mut self, target: &mathtex_editor_core::Snapshot) -> Update {
264        // History snapshots come from this editor, so they always restore.
265        let changed = self.editor.restore(target).is_ok();
266        Update { changed, needs_redraw: changed, ..Update::default() }
267    }
268
269    /// Copy the selection to the internal clipboard and return it for the system clipboard.
270    pub fn copy(&mut self) -> Option<ClipboardData> {
271        let doc = self.editor.selection_document()?;
272        let tex = self.editor.selection_tex()?;
273        let json = serde_json::to_string(&doc).ok()?;
274        self.clipboard = Some(doc);
275        Some(ClipboardData { json, tex })
276    }
277
278    /// Copy the selection, then delete it as one undo step.
279    pub fn cut(&mut self) -> Option<(ClipboardData, Update)> {
280        let data = self.copy()?;
281        let update = self.command(Command::DeleteBackward);
282        Some((data, update))
283    }
284
285    /// The internal clipboard.
286    pub fn clipboard(&self) -> Option<&Document> {
287        self.clipboard.as_ref()
288    }
289
290    /// Paste document JSON, giving each host box a fresh token and the size its old token had.
291    pub fn paste_json(&mut self, json: &str) -> Result<Update, PasteError> {
292        let doc: Document = serde_json::from_str(json).map_err(|e| PasteError::Json(e.to_string()))?;
293        if doc.is_empty() {
294            return Err(PasteError::Empty);
295        }
296        self.paste_document(doc).map_err(PasteError::Tokens)
297    }
298
299    /// Paste the internal clipboard, giving each host box a fresh token.
300    pub fn paste_internal(&mut self) -> Result<Update, TokenError> {
301        match self.clipboard.clone() {
302            Some(doc) => self.paste_document(doc),
303            None => Ok(Update::default()),
304        }
305    }
306
307    /// Paste plain text through the keymap, as one undo step.
308    pub fn paste_text(&mut self, s: &str) -> Update {
309        self.text(s)
310    }
311
312    fn paste_document(&mut self, mut doc: Document) -> Result<Update, TokenError> {
313        let reminted = self.tokens.remint(&mut doc)?;
314        for &(old, new) in &reminted {
315            self.boxes.copy_size(old, new);
316        }
317        self.keymap.reset();
318        let mut update = self.run(vec![Command::InsertDocument(doc)]);
319        if update.changed {
320            update.reminted = reminted;
321        }
322        Ok(update)
323    }
324
325    /// Mint a token and insert a host box carrying it.
326    pub fn insert_host_box(&mut self) -> Result<(u32, Update), TokenError> {
327        let token = self.tokens.mint()?;
328        Ok((token, self.command(Command::InsertHostBox(token))))
329    }
330
331    /// Mint a token for a host box the host inserts itself.
332    pub fn mint_host_token(&mut self) -> Result<u32, TokenError> {
333        self.tokens.mint()
334    }
335
336    /// Lay out `token` as an empty box of this size, for hosts that draw the box content themselves.
337    pub fn set_host_box_size(&mut self, token: u32, width: Length, height: Length, depth: Length) -> Result<(), TokenError> {
338        self.tokens.reserve(token)?;
339        self.boxes.set_size(token, width, height, depth);
340        self.cache.invalidate();
341        Ok(())
342    }
343
344    /// Answer `\hostbox` for tokens without a size through `provider`.
345    pub fn set_host_boxes(&mut self, provider: Box<dyn HostBoxes>) {
346        self.boxes.set_provider(Some(provider));
347        self.cache.invalidate();
348    }
349
350    /// Typeset again on the next view, after the provider's boxes changed.
351    pub fn host_boxes_changed(&mut self) {
352        self.cache.invalidate();
353    }
354
355    /// Tokens the document, the history, or the clipboard reference, and drop the sizes of every other token.
356    pub fn live_host_tokens(&mut self) -> BTreeSet<u32> {
357        let live = TokenRegistry::live(&self.editor, &self.history, self.clipboard.as_ref());
358        self.boxes.retain(&live);
359        live
360    }
361
362    /// The typeset document with its caret and selection geometry, typeset again only after a change.
363    pub fn view(&mut self) -> Result<&View, TypesetError> {
364        self.cache.view(&self.editor, &mut self.typesetter, self.mode, &self.boxes)
365    }
366
367    /// A standalone view of what a palette word inserts, `None` for an unknown word.
368    pub fn preview_word(&mut self, word: &str) -> Option<Result<View, TypesetError>> {
369        let commands = self.keymap.commands_for_word(word)?;
370        let mut scratch = Editor::new();
371        for cmd in commands {
372            let _ = scratch.exec(cmd);
373        }
374        let mut cache = RenderCache::new();
375        Some(cache.view(&scratch, &mut self.typesetter, self.mode, &self.boxes).cloned())
376    }
377
378    /// Run commands in order as one undo step.
379    fn run(&mut self, commands: Vec<Command>) -> Update {
380        if commands.is_empty() {
381            return Update::default();
382        }
383        let before = self.editor.snapshot();
384        let seen = self.visible();
385        let mut update = Update::default();
386        for cmd in commands {
387            let out = self.editor.exec(cmd);
388            update.changed |= out.changed;
389            update.close |= out.close;
390            update.exit = update.exit.or(out.exit);
391            update.entered_host_box = update.entered_host_box.or(out.entered_host_box);
392        }
393        if update.changed {
394            self.history.record(before);
395        }
396        update.needs_redraw = update.changed || self.visible() != seen;
397        update
398    }
399
400    fn visible(&self) -> Visible {
401        Visible {
402            revision: self.editor.revision(),
403            cursor: self.editor.cursor(),
404            selection: self.editor.selection(),
405            menu: self.editor.menu(),
406        }
407    }
408}