mod boxes;
mod cache;
mod tokens;
mod undo;
#[cfg(test)]
mod tests;
use std::collections::BTreeSet;
use std::fmt;
use mathtex_editor_core::{
CaretPath, Command, Dir, Document, Editor, ExitDir, HostBoxEntry, HostBoxPolicy, MenuView, Point, Repair,
Selection, Side,
};
use mathtex_editor_keymap::{KeyInput, Keymap};
use mathtex_engine::font::FontLoader;
use mathtex_engine::{HostBoxes, MathMode, TypesetError, Typesetter};
use mathtex_ir::Length;
use crate::boxes::SessionBoxes;
pub use crate::cache::{RenderCache, View};
pub use crate::tokens::{TokenError, TokenRegistry};
pub use crate::undo::{DEFAULT_UNDO_LIMIT, UndoStack};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Update {
pub changed: bool,
pub exit: Option<ExitDir>,
pub close: bool,
pub entered_host_box: Option<HostBoxEntry>,
pub needs_redraw: bool,
pub reminted: Vec<(u32, u32)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardData {
pub json: String,
pub tex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PasteError {
Json(String),
Empty,
Tokens(TokenError),
}
impl fmt::Display for PasteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PasteError::Json(e) => write!(f, "not a document: {e}"),
PasteError::Empty => write!(f, "the document is empty"),
PasteError::Tokens(e) => e.fmt(f),
}
}
}
impl std::error::Error for PasteError {}
#[derive(PartialEq)]
struct Visible {
revision: u64,
cursor: CaretPath,
selection: Option<Selection>,
menu: Option<MenuView>,
}
pub struct Session<L: FontLoader> {
editor: Editor,
keymap: Keymap,
typesetter: Typesetter<L>,
mode: MathMode,
policy: HostBoxPolicy,
cache: RenderCache,
history: UndoStack,
tokens: TokenRegistry,
clipboard: Option<Document>,
boxes: SessionBoxes,
}
impl<L: FontLoader> fmt::Debug for Session<L> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("revision", &self.editor.revision())
.field("typesetter", &self.typesetter)
.field("mode", &self.mode)
.finish_non_exhaustive()
}
}
impl<L: FontLoader> Session<L> {
pub fn new(typesetter: Typesetter<L>) -> Self {
Self {
editor: Editor::new(),
keymap: Keymap::new(),
typesetter,
mode: MathMode::Display,
policy: HostBoxPolicy::Skip,
cache: RenderCache::new(),
history: UndoStack::default(),
tokens: TokenRegistry::new(),
clipboard: None,
boxes: SessionBoxes::default(),
}
}
pub fn load(&mut self, mut doc: Document) -> Vec<Repair> {
let repairs = doc.repair();
if let Ok(mut editor) = Editor::from_document(&doc) {
editor.set_host_box_policy(self.policy);
self.editor = editor;
}
self.tokens.reserve_document(&doc);
self.keymap.reset();
self.history.clear();
self.cache.invalidate();
repairs
}
pub fn editor(&self) -> &Editor {
&self.editor
}
pub fn keymap(&self) -> &Keymap {
&self.keymap
}
pub fn keymap_mut(&mut self) -> &mut Keymap {
&mut self.keymap
}
pub fn typesetter(&self) -> &Typesetter<L> {
&self.typesetter
}
pub fn history(&self) -> &UndoStack {
&self.history
}
pub fn set_undo_limit(&mut self, limit: usize) {
self.history.set_limit(limit);
}
pub fn set_math_mode(&mut self, mode: MathMode) {
self.mode = mode;
self.cache.invalidate();
}
pub fn set_host_box_policy(&mut self, policy: HostBoxPolicy) {
self.policy = policy;
self.editor.set_host_box_policy(policy);
}
pub fn key(&mut self, input: &KeyInput) -> Update {
let ctx = self.editor.input_context();
let commands = self.keymap.map_key(input, &ctx);
self.run(commands)
}
pub fn text(&mut self, s: &str) -> Update {
let ctx = self.editor.input_context();
let commands = self.keymap.map_text(s, &ctx);
self.run(commands)
}
pub fn pointer(&mut self, at: Point, extend: bool) -> Update {
self.keymap.reset();
let hit = self.cache.hit_test(&self.editor, &mut self.typesetter, self.mode, &self.boxes, at);
match hit {
Ok(Some(path)) if extend => self.run(vec![Command::ExtendTo(path)]),
Ok(Some(path)) => self.run(vec![Command::MoveTo(path)]),
_ => Update::default(),
}
}
pub fn command(&mut self, cmd: Command) -> Update {
self.keymap.reset();
match &cmd {
Command::InsertHostBox(token) => {
let _ = self.tokens.reserve(*token);
}
Command::InsertDocument(doc) => self.tokens.reserve_document(doc),
_ => {}
}
self.run(vec![cmd])
}
pub fn commit_word(&mut self, word: &str) -> Update {
let Some(commands) = self.keymap.commands_for_word(word) else {
return Update::default();
};
self.keymap.reset();
self.run(commands)
}
pub fn step_over_host_box(&mut self, entry: HostBoxEntry) -> Update {
let dir = match entry.side {
Side::Before => Dir::Right,
Side::After => Dir::Left,
};
self.editor.set_host_box_policy(HostBoxPolicy::Skip);
let update = self.command(Command::Move(dir));
self.editor.set_host_box_policy(self.policy);
update
}
pub fn undo(&mut self) -> Update {
self.keymap.reset();
let Some(target) = self.history.undo(self.editor.snapshot()) else {
return Update::default();
};
self.restore(&target)
}
pub fn redo(&mut self) -> Update {
self.keymap.reset();
let Some(target) = self.history.redo(self.editor.snapshot()) else {
return Update::default();
};
self.restore(&target)
}
fn restore(&mut self, target: &mathtex_editor_core::Snapshot) -> Update {
let changed = self.editor.restore(target).is_ok();
Update { changed, needs_redraw: changed, ..Update::default() }
}
pub fn copy(&mut self) -> Option<ClipboardData> {
let doc = self.editor.selection_document()?;
let tex = self.editor.selection_tex()?;
let json = serde_json::to_string(&doc).ok()?;
self.clipboard = Some(doc);
Some(ClipboardData { json, tex })
}
pub fn cut(&mut self) -> Option<(ClipboardData, Update)> {
let data = self.copy()?;
let update = self.command(Command::DeleteBackward);
Some((data, update))
}
pub fn clipboard(&self) -> Option<&Document> {
self.clipboard.as_ref()
}
pub fn paste_json(&mut self, json: &str) -> Result<Update, PasteError> {
let doc: Document = serde_json::from_str(json).map_err(|e| PasteError::Json(e.to_string()))?;
if doc.is_empty() {
return Err(PasteError::Empty);
}
self.paste_document(doc).map_err(PasteError::Tokens)
}
pub fn paste_internal(&mut self) -> Result<Update, TokenError> {
match self.clipboard.clone() {
Some(doc) => self.paste_document(doc),
None => Ok(Update::default()),
}
}
pub fn paste_text(&mut self, s: &str) -> Update {
self.text(s)
}
fn paste_document(&mut self, mut doc: Document) -> Result<Update, TokenError> {
let reminted = self.tokens.remint(&mut doc)?;
for &(old, new) in &reminted {
self.boxes.copy_size(old, new);
}
self.keymap.reset();
let mut update = self.run(vec![Command::InsertDocument(doc)]);
if update.changed {
update.reminted = reminted;
}
Ok(update)
}
pub fn insert_host_box(&mut self) -> Result<(u32, Update), TokenError> {
let token = self.tokens.mint()?;
Ok((token, self.command(Command::InsertHostBox(token))))
}
pub fn mint_host_token(&mut self) -> Result<u32, TokenError> {
self.tokens.mint()
}
pub fn set_host_box_size(&mut self, token: u32, width: Length, height: Length, depth: Length) -> Result<(), TokenError> {
self.tokens.reserve(token)?;
self.boxes.set_size(token, width, height, depth);
self.cache.invalidate();
Ok(())
}
pub fn set_host_boxes(&mut self, provider: Box<dyn HostBoxes>) {
self.boxes.set_provider(Some(provider));
self.cache.invalidate();
}
pub fn host_boxes_changed(&mut self) {
self.cache.invalidate();
}
pub fn live_host_tokens(&mut self) -> BTreeSet<u32> {
let live = TokenRegistry::live(&self.editor, &self.history, self.clipboard.as_ref());
self.boxes.retain(&live);
live
}
pub fn view(&mut self) -> Result<&View, TypesetError> {
self.cache.view(&self.editor, &mut self.typesetter, self.mode, &self.boxes)
}
pub fn preview_word(&mut self, word: &str) -> Option<Result<View, TypesetError>> {
let commands = self.keymap.commands_for_word(word)?;
let mut scratch = Editor::new();
for cmd in commands {
let _ = scratch.exec(cmd);
}
let mut cache = RenderCache::new();
Some(cache.view(&scratch, &mut self.typesetter, self.mode, &self.boxes).cloned())
}
fn run(&mut self, commands: Vec<Command>) -> Update {
if commands.is_empty() {
return Update::default();
}
let before = self.editor.snapshot();
let seen = self.visible();
let mut update = Update::default();
for cmd in commands {
let out = self.editor.exec(cmd);
update.changed |= out.changed;
update.close |= out.close;
update.exit = update.exit.or(out.exit);
update.entered_host_box = update.entered_host_box.or(out.entered_host_box);
}
if update.changed {
self.history.record(before);
}
update.needs_redraw = update.changed || self.visible() != seen;
update
}
fn visible(&self) -> Visible {
Visible {
revision: self.editor.revision(),
cursor: self.editor.cursor(),
selection: self.editor.selection(),
menu: self.editor.menu(),
}
}
}