Files
wordcraft/crates/engine/src/session/layout.rs
T

211 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::doc_state::{Selection, TypedWord, typed_range};
use super::locale::SYSTEM_LOCALE;
use super::snapshot_types::LayoutEntry;
use super::view::DRAFT_WIDTH;
use super::*;
impl Session {
/// The current layout (recomputed when the document or view changed).
pub fn layout(&mut self) -> Arc<DocLayout> {
let typing = self.typing_word();
self.layout_with(typing)
}
/// [`Session::layout`] with `typing` as the word being typed.
fn layout_with(&mut self, typing: Option<wordcraft_layout::TypingWord>) -> Arc<DocLayout> {
let mode = if self.view.read_mode { ViewMode::Read } else { self.view.mode };
let width = match mode {
ViewMode::Read => self.view.read_width,
ViewMode::Web => self.view.web_width,
_ => DRAFT_WIDTH,
};
let height = if self.view.read_mode { self.view.read_height } else { 0.0 };
let spacing = if self.view.read_mode { self.view.read_line_spacing.map(f32::to_bits) } else { None };
// Deletions in balloons depend on the view (the Comments pane, Read Mode, side by side), so
// a view change that moves them in or out of the text lays it out again.
let hide_deleted = !self.view.show_markup;
let deleted = (hide_deleted, self.prefs.markup.hidden_deletions(), self.revisions_in_balloons());
let outline = (mode == ViewMode::Outline).then(|| self.view.outline.view());
let shown = (self.view.marks || self.prefs.marks.hidden, self.view.field_codes);
let (rev, proofing, numeral) = (self.rev, self.view.proofing, self.view.numeral);
let fits = |e: &Option<LayoutEntry>| {
e.as_ref().is_some_and(|(r, w, h, m, sp, _, pf, o, hd, n, t, sh)| {
*r == rev
&& (*w == width || mode == ViewMode::Print)
&& *h == height
&& *m == mode
&& *sp == spacing
&& *pf == proofing
&& *n == numeral
&& *hd == deleted
&& *o == outline
&& *t == typing
&& *sh == shown
})
};
if !fits(&self.layout[0]) && fits(&self.layout[1]) {
self.layout.swap(0, 1);
}
if fits(&self.layout[0])
&& let Some(e) = &self.layout[0]
{
return e.5.clone();
}
let (fn_name, fn_path) = self.resolved_filenames();
let (filename, filepath): (Option<Arc<str>>, Option<Arc<str>>) = (Some(Arc::from(fn_name.as_str())), Some(Arc::from(fn_path.as_str())));
let opts = LayoutOptions {
view: mode,
web_width: width,
read_height: height,
read_line_spacing: spacing.map(f32::from_bits),
show_hidden: shown.0,
hide_deleted,
hide_marked: deleted.1,
deletions_in_balloons: deleted.2,
proofing: self.view.proofing,
numeral: self.view.numeral.resolve(SYSTEM_LOCALE.get().map(String::as_str)),
comment_balloons: false,
field_codes: shown.1,
outline: outline.clone(),
proof: wordcraft_layout::ProofOptions { grammar: self.prefs.mark_grammar, spelling: self.prefs.spelling() },
typing_word: typing,
filename: filename.clone(),
filepath: filepath.clone(),
};
let l = Arc::new(wordcraft_layout::layout(&self.doc, &mut self.cache, &opts));
let [newest, _] = std::mem::take(&mut self.layout);
self.layout = [
Some((self.rev, width, height, mode, spacing, l.clone(), self.view.proofing, outline, deleted, self.view.numeral, typing, shown)),
newest,
];
l
}
/// The recorded word while the caret and the word are as typing left them; else drops it.
fn typing_word(&mut self) -> Option<wordcraft_layout::TypingWord> {
let t = self.typing.as_ref()?;
let p = if self.sel.is_collapsed() && self.sel.focus == t.at { self.doc.para_at(&t.at) } else { None };
match p {
Some(p) if typed_range(&p.text, t.at.off) == Some((t.start, t.end)) => {
Some(wordcraft_layout::TypingWord { rev: p.rev, start: t.start, end: t.end })
}
_ => {
self.typing = None;
None
}
}
}
/// After a successful command: a `text.*` edit at the caret records the word being typed;
/// any other caret move ends it.
pub(super) fn note_typing(&mut self, id: &str, before: Option<&Document>, sel_before: &Selection) {
let at = &self.sel.focus;
let edited = || before.is_some_and(|d| d.para_at(at).map(|p| &p.text) != self.doc.para_at(at).map(|p| &p.text));
if id.starts_with("text.") && edited() {
self.typing = self.word_being_typed();
} else if self.sel != *sel_before {
self.typing = None;
}
}
/// The word the collapsed caret ends or sits in, as [`TypedWord`].
fn word_being_typed(&self) -> Option<TypedWord> {
if !self.sel.is_collapsed() {
return None;
}
let at = self.sel.focus.clone();
let (start, end) = typed_range(&self.doc.para_at(&at)?.text, at.off)?;
Some(TypedWord { at, start, end })
}
/// The page as command `id` would leave it, for a live preview (Insert › Table's grid, #3931):
/// the command runs on a copy of the document, with this caret and view, and the copy is laid
/// out. The copy comes back with its layout, which is drawn from it: the layout's paragraph
/// paths name the copy's paragraphs, not this document's, and so is the copy's selection,
/// which is where the caret would be. The document, the selection, the undo history and Repeat
/// are untouched. `None` when the command fails or isn't allowed here.
pub fn preview_layout(&mut self, id: &str, params: &Value) -> Option<(Document, Selection, Arc<DocLayout>)> {
// The word being typed keeps going without a squiggle on the preview: the command moves
// the copy's caret off it and may split its paragraph, so it is found again by its place.
let typing = self.typing_word();
let at = self.sel.focus.clone();
let mut copy = Session::with_registry(self.doc.clone(), self.registry.clone());
copy.sel = self.sel.clone();
copy.column = self.column.clone();
copy.widened = self.widened.clone();
copy.view = self.view.clone();
copy.prefs = self.prefs.clone();
copy.path = self.path.clone();
copy.file_name = self.file_name.clone();
copy.author = self.author.clone();
copy.last_highlight = self.last_highlight;
copy.system_region = self.system_region.clone();
copy.editing_lang = self.editing_lang.clone();
// The copy shares the paragraph layouts, so only what the command changed is laid out.
std::mem::swap(&mut copy.cache, &mut self.cache);
let layout = copy.run(id, params).is_ok().then(|| {
let typing = typing.and_then(|t| {
let p = copy.doc.para_at(&at)?;
(t.end <= p.len()).then_some(wordcraft_layout::TypingWord { rev: p.rev, ..t })
});
copy.layout_with(typing)
});
std::mem::swap(&mut copy.cache, &mut self.cache);
layout.map(|l| (copy.doc, copy.sel, l))
}
/// A layout for output (PDF, images, print): no proofing marks, print view.
pub fn export_layout(&self) -> Arc<DocLayout> {
self.export_layout_with(false)
}
/// [`Session::export_layout`]; with `markup` (Print Markup) each page also has the markup
/// area with the comments in balloons.
pub fn export_layout_with(&self, markup: bool) -> Arc<DocLayout> {
let (fn_name, fn_path) = self.resolved_filenames();
let opts = LayoutOptions {
view: ViewMode::Print,
comment_balloons: markup && self.prefs.markup.comments,
numeral: self.view.numeral.resolve(SYSTEM_LOCALE.get().map(String::as_str)),
filename: Some(Arc::from(fn_name.as_str())),
filepath: Some(Arc::from(fn_path.as_str())),
..Default::default()
};
Arc::new(wordcraft_layout::layout(&self.doc, &mut LayoutCache::new(), &opts))
}
/// Invalidate the cached layout (fonts changed etc.).
pub fn relayout(&mut self) {
self.layout = Default::default();
self.cache.clear();
}
/// Set the language new documents are written in ([`Session::editing_lang`]; `None` or a
/// malformed tag: US English). A document nobody has touched yet — untitled, unchanged, no
/// undo steps, no text — switches too, so the blank document a session starts with follows
/// the saved proofing default, or the interface language when none is saved. Nothing else
/// changes and no undo step is added.
pub fn set_editing_language(&mut self, tag: Option<&str>) {
let tag = self
.prefs
.default_language
.as_deref()
.and_then(wordcraft_proof::lang::normalize_tag)
.or_else(|| tag.and_then(wordcraft_proof::lang::normalize_tag));
if self.editing_lang == tag {
return;
}
self.editing_lang = tag;
let untouched = self.path.is_none() && !self.dirty && self.history.is_empty() && self.redo.is_empty();
if untouched && self.doc.plain_text(StoryRef::Body).trim().is_empty() && self.doc.paragraph_count() <= 1 {
self.doc.styles.default_chr.lang = Some(self.editing_lang.clone().unwrap_or_else(|| "en-US".into()));
self.rev = self.rev.wrapping_add(1);
// Not an edit: the document is as untouched as it was.
if self.made_rev == self.rev.wrapping_sub(1) {
self.made_rev = self.rev;
}
self.relayout();
}
}
}