mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 06:30:00 -06:00
Type: typing "1. ", "A) " or "* " at the start of a paragraph starts a list
The reference app detects lists typed as 1., 1), A. or A), and makes custom bullets from *, #, > or + followed by a space. In VectorCraft the characters stayed as text. When the Type tool types a space on its own after one of these at the start of a paragraph in no list, the typing so far becomes one undo step. The new command text.startList then removes the typed characters and the space and makes the paragraph a numbered list in 1, 2, 3 or A, B, C, or a bulleted list with the filled dot. A paragraph with no indents gets the 18 pt hanging indent. One Undo puts the characters back as text with no list, and the caret goes on after them; Redo removes them again. The same characters later in a paragraph, list characters pasted in one piece with the space and a paragraph already in a list start nothing. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
//! Styling a selected range goes through `text.setRangeStyle` (the Character panel reads the
|
||||
//! selection from [`Tool::options`]); Alt+arrows step it by the Preferences › Type increments
|
||||
//! (`type.step`). New type starts with placeholder text, selected, when Fill New Type Objects With
|
||||
//! Placeholder Text is on.
|
||||
//! Placeholder Text is on. A space typed on its own after "1.", "1)", "A.", "A)", "*", "#", ">" or
|
||||
//! "+" at the start of a paragraph in no list makes that paragraph a list (`text.startList`).
|
||||
//!
|
||||
//! IME: the marked text of a composition is part of the typing session (so it lays out in place),
|
||||
//! underlined, until the IME commits it (it then goes through [`Tool::text_input`]) or clears it.
|
||||
@@ -60,6 +61,18 @@ struct Preedit {
|
||||
active: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
/// A list that typed characters started ([`TypeTool::start_typed_list`]).
|
||||
struct TypedList {
|
||||
/// The type object.
|
||||
id: NodeId,
|
||||
/// The bytes of the typed characters and the space: from the paragraph's start to the caret.
|
||||
start: usize,
|
||||
end: usize,
|
||||
/// The plain text with the typed characters, before the list started.
|
||||
text: String,
|
||||
list: vectorcraft_doc::text::ParaList,
|
||||
}
|
||||
|
||||
/// The edited text's layout, keyed by the font database's generation and the text.
|
||||
type CachedLayout = ((u64, TextObject), Arc<TextLayout>);
|
||||
|
||||
@@ -118,6 +131,9 @@ pub struct TypeTool {
|
||||
clicks: (Option<Point>, u8),
|
||||
/// Styled copy of the last copied range (pasting the same plain text keeps its styles).
|
||||
clipboard: Vec<TextRun>,
|
||||
/// The list typed characters last started (`text.startList`), so that after Undo puts them back
|
||||
/// the caret goes on after them, and after Redo it goes back.
|
||||
listed: Option<TypedList>,
|
||||
cache: Mutex<Option<CachedLayout>>,
|
||||
}
|
||||
|
||||
@@ -301,6 +317,7 @@ impl TypeTool {
|
||||
if !extend {
|
||||
self.anchor = byte;
|
||||
}
|
||||
self.listed = None;
|
||||
out
|
||||
}
|
||||
|
||||
@@ -308,6 +325,7 @@ impl TypeTool {
|
||||
self.edit_session = next_edit_session();
|
||||
self.editing = Some(id);
|
||||
self.fresh = None;
|
||||
self.listed = None;
|
||||
self.caret = caret;
|
||||
self.anchor = caret;
|
||||
self.goal_x = None;
|
||||
@@ -316,6 +334,44 @@ impl TypeTool {
|
||||
self.typing_ended = false;
|
||||
}
|
||||
|
||||
/// Typed list detection, after a space typed on its own at the caret: when the characters
|
||||
/// from the start of the caret's paragraph up to it start a list ([`TextObject::typed_list_at`]),
|
||||
/// the typing session ends as one undo step with those characters, and `text.startList`
|
||||
/// removes them and makes the paragraph a list as the next one, so one Undo puts them back.
|
||||
fn start_typed_list(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
let (Some(id), Some(ty)) = (self.editing, &self.typing) else { return vec![] };
|
||||
let Some(mut t) = Self::text(cx, id).cloned() else { return vec![] };
|
||||
// The document does not have the space yet (its preview is applied after this input).
|
||||
t.runs = ty.cur.clone();
|
||||
let at = self.caret;
|
||||
let Some((start, list)) = t.typed_list_at(at) else { return vec![] };
|
||||
let mut out = self.commit();
|
||||
out.push(Action::Exec("text.startList".into(), json!({"id": id.0, "at": at})));
|
||||
self.caret = start;
|
||||
self.anchor = start;
|
||||
self.listed = Some(TypedList { id, start, end: at, text: t.plain_text(), list });
|
||||
out
|
||||
}
|
||||
|
||||
/// After Undo of a list that typed characters started (and of what was typed after it), the
|
||||
/// text is as it was typed: the caret goes on after the characters. After Redo the text is as
|
||||
/// the list left it: the caret goes back to the paragraph's start.
|
||||
fn after_typed_list(&mut self, cx: &ToolContext) {
|
||||
let Some(l) = &self.listed else { return };
|
||||
if self.editing != Some(l.id) || self.typing.is_some() || self.caret != self.anchor {
|
||||
return;
|
||||
}
|
||||
let Some(t) = Self::text(cx, l.id) else { return };
|
||||
let (text, list) = (t.plain_text(), t.para_at(t.paragraphs_in(l.start, l.start).start).list);
|
||||
let listed_text = l.text.get(..l.start).zip(l.text.get(l.end..)).map(|(a, b)| format!("{a}{b}"));
|
||||
if text == l.text && list.is_none() {
|
||||
self.caret = l.end;
|
||||
} else if self.caret == l.end && Some(&text) == listed_text.as_ref() && list == Some(l.list) {
|
||||
self.caret = l.start;
|
||||
}
|
||||
self.anchor = self.caret;
|
||||
}
|
||||
|
||||
/// Byte under `p` in the edited text if `p` is inside it (frame, path band or layout bounds).
|
||||
fn hit_edited(&self, cx: &ToolContext, p: Point) -> Option<usize> {
|
||||
let t = self.current(cx)?;
|
||||
@@ -904,6 +960,8 @@ impl Tool for TypeTool {
|
||||
}
|
||||
// An IME commit replaces its marked text (an empty commit just removes it).
|
||||
let committing = self.preedit.is_some();
|
||||
// A space typed on its own at a caret may start a list.
|
||||
let list_space = !committing && s == " " && self.caret == self.anchor;
|
||||
if committing && s.is_empty() {
|
||||
return self.ime_preedit(cx, "", None);
|
||||
}
|
||||
@@ -919,7 +977,11 @@ impl Tool for TypeTool {
|
||||
let runs = self.clipboard.clone();
|
||||
return self.replace(cx, "", Some(runs));
|
||||
}
|
||||
self.replace(cx, &s, None)
|
||||
let mut out = self.replace(cx, &s, None);
|
||||
if list_space {
|
||||
out.extend(self.start_typed_list(cx));
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Would [`Tool::text_input`] change `s` before inserting it? Control characters are dropped,
|
||||
/// `\r` becomes `\n`, Typographer's Quotes rewrites a straight quote, and copied text keeps
|
||||
@@ -1237,8 +1299,11 @@ impl Tool for TypeTool {
|
||||
self.finish(cx)
|
||||
}
|
||||
/// Editing ends when a command takes the text out of the selection (Deselect, another object
|
||||
/// picked in the Layers panel) or removes it.
|
||||
/// picked in the Layers panel) or removes it. After an Undo that takes back a list typed
|
||||
/// characters started, the caret goes on after the characters, and after its Redo the caret
|
||||
/// goes before the paragraph's text.
|
||||
fn after_command(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.after_typed_list(cx);
|
||||
let Some(id) = self.editing else { return vec![] };
|
||||
let mut cur = Some(id).filter(|i| cx.doc.node(*i).is_some());
|
||||
while let Some(c) = cur {
|
||||
|
||||
Reference in New Issue
Block a user