mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Merge pull request #4192 from dotCooCoo/feat/typed-list-detection
Type: typing "1. ", "A) " or "* " at the start of a paragraph starts a list
This commit is contained in:
@@ -675,6 +675,19 @@ impl ParaList {
|
||||
..first.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// The list that the Type tool starts when `typed` and a space are typed at the start of a
|
||||
/// paragraph: "1." or "1)" starts a numbered list in 1, 2, 3, "A." or "A)" starts one in A, B,
|
||||
/// C, and "*", "#", ">" or "+" starts a bulleted list with the filled dot. Any other text starts
|
||||
/// none.
|
||||
pub fn typed(typed: &str) -> Option<ParaList> {
|
||||
match typed {
|
||||
"1." | "1)" => Some(Self::numbered(NumberStyle::Arabic)),
|
||||
"A." | "A)" => Some(Self::numbered(NumberStyle::UpperLetters)),
|
||||
"*" | "#" | ">" | "+" => Some(Self::bulleted(Bullet::Dot)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The left and first-line indents (pt) a list gives a paragraph whose indents are both 0: the
|
||||
@@ -1888,6 +1901,17 @@ impl TextObject {
|
||||
let index = |byte: usize| text.as_bytes().get(..byte).map_or(0, |s| s.iter().filter(|&&c| c == b'\n').count());
|
||||
index(a)..index(b) + 1
|
||||
}
|
||||
/// The list that the characters before byte `at` start ([`ParaList::typed`]) with the byte
|
||||
/// where their paragraph starts: when the plain text from the start of the paragraph up to
|
||||
/// `at` is a list's start followed by a space, and the paragraph is in no list. None otherwise.
|
||||
pub fn typed_list_at(&self, at: usize) -> Option<(usize, ParaList)> {
|
||||
let text = self.plain_text();
|
||||
let before = text.get(..at)?;
|
||||
let start = before.rfind('\n').map_or(0, |i| i + 1);
|
||||
let list = ParaList::typed(before[start..].strip_suffix(' ')?)?;
|
||||
let index = before.bytes().filter(|&b| b == b'\n').count();
|
||||
self.para_at(index).list.is_none().then_some((start, list))
|
||||
}
|
||||
/// Apply `f` to the attributes of paragraphs `range` (indices; None = every paragraph).
|
||||
pub fn edit_paras(&mut self, range: Option<std::ops::Range<usize>>, mut f: impl FnMut(&mut ParaStyle)) {
|
||||
match range {
|
||||
@@ -2328,6 +2352,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Typed list detection: "1.", "1)", "A." and "A)" and a space at the start of a paragraph
|
||||
/// start a numbered list, and "*", "#", ">" and "+" and a space start a bulleted list with the
|
||||
/// filled dot. Other text, the same characters later in a paragraph or without the space, and
|
||||
/// a paragraph already in a list start nothing.
|
||||
#[test]
|
||||
fn typed_list_starts_are_found_at_a_paragraphs_start() {
|
||||
let (arabic, letters, dot) =
|
||||
(ParaList::numbered(NumberStyle::Arabic), ParaList::numbered(NumberStyle::UpperLetters), ParaList::bulleted(Bullet::Dot));
|
||||
let typed = ["1.", "1)", "A.", "A)", "*", "#", ">", "+"].map(ParaList::typed);
|
||||
assert_eq!(typed, [arabic, arabic, letters, letters, dot, dot, dot, dot].map(Some));
|
||||
for other in ["", "2.", "a.", "a)", "I.", "1", "A", "1:", "-", "•", "1.)", " 1.", "*1", "1."] {
|
||||
assert_eq!(ParaList::typed(other), None, "{other:?}");
|
||||
}
|
||||
let mut t = TextObject::point(Point::ZERO, "Fruit\n1. Apples\nA) x * y\n+ ", CharStyle::default());
|
||||
assert_eq!(t.typed_list_at(9), Some((6, arabic)));
|
||||
assert_eq!(t.typed_list_at(19), Some((16, letters)));
|
||||
assert_eq!(t.typed_list_at(27), Some((25, dot)));
|
||||
// Not at the paragraph's start, not right after the space, or past the end.
|
||||
for at in [0, 5, 8, 10, 15, 23, 25, 26, 99] {
|
||||
assert_eq!(t.typed_list_at(at), None, "{at}");
|
||||
}
|
||||
let mut styles = t.paragraph_styles();
|
||||
styles[1].list = Some(ParaList::bulleted(Bullet::Square));
|
||||
t.set_paragraph_styles(styles);
|
||||
assert_eq!(t.typed_list_at(9), None, "a list paragraph stays as it is");
|
||||
assert_eq!(t.typed_list_at(19), Some((16, letters)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_controls_reject_collapsed_and_nonfinite_transforms() {
|
||||
let mut t = TextObject::point(Point::ZERO, "Text", CharStyle::default());
|
||||
|
||||
@@ -22,6 +22,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
edit_range
|
||||
),
|
||||
cmd!(
|
||||
"text.startList",
|
||||
"Bullets and Numbering",
|
||||
[],
|
||||
None,
|
||||
"{id, at?: byte (default: the end of the text)} start a list from typed characters, what the Type tool runs when a space is typed on its own: when the text from the start of the paragraph that `at` is in up to `at` is \"1. \" or \"1) \" (a numbered list in 1, 2, 3), \"A. \" or \"A) \" (a numbered list in A, B, C), or \"* \", \"# \", \"> \" or \"+ \" (a bulleted list with the filled dot), and the paragraph is in no list, those characters are removed and the paragraph joins that list as one undo step (left and first-line indents of 0 become 18 and -18, as text.setFormat sets them); otherwise nothing changes → {id, started: bool, caret: byte (the paragraph's start when started, else `at`)}",
|
||||
has_doc,
|
||||
start_list
|
||||
),
|
||||
cmd!(
|
||||
"text.setRangeStyle",
|
||||
"Character",
|
||||
@@ -220,6 +229,36 @@ fn edit_range(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"id": id.0, "caret": caret}))
|
||||
}
|
||||
|
||||
fn start_list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.startList";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
check_numbers(p, &["at"], C)?;
|
||||
let t = text_ref(s, id)?;
|
||||
let len = edit::runs_len(&t.runs);
|
||||
let at = byte_param(p, "at").unwrap_or(len).min(len);
|
||||
let Some((start, list)) = t.typed_list_at(at) else {
|
||||
return Ok(json!({"id": id.0, "started": false, "caret": at}));
|
||||
};
|
||||
s.edit("Bullets and Numbering", |d, _| {
|
||||
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
|
||||
t.splice_paras(start, at, "");
|
||||
edit::replace_range(&mut t.runs, start, at, "");
|
||||
t.ruby_after_replace(start, at, 0);
|
||||
let i = t.paragraphs_in(start, start).start;
|
||||
t.edit_paras(Some(i..i + 1), |para| {
|
||||
para.list = Some(list);
|
||||
// A paragraph with no left or first-line indent gets the hanging indent.
|
||||
if para.left_indent == 0.0 && para.first_line_indent == 0.0 {
|
||||
para.left_indent = vectorcraft_doc::text::LIST_HANGING_INDENT;
|
||||
para.first_line_indent = -vectorcraft_doc::text::LIST_HANGING_INDENT;
|
||||
}
|
||||
});
|
||||
refresh_bounds(t);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"id": id.0, "started": true, "caret": start}))
|
||||
}
|
||||
|
||||
fn get_range(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("text.getRange", "missing `id`"))?;
|
||||
let t = text_ref(s, id)?;
|
||||
|
||||
@@ -545,6 +545,164 @@ fn undo_while_typing_takes_back_only_the_typing_and_redo_returns_it() {
|
||||
assert_eq!(obj(&s, id).plain_text(), "abc!");
|
||||
}
|
||||
|
||||
fn list_markers(s: &Session, id: NodeId) -> Vec<String> {
|
||||
vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &obj(s, id)).markers.iter().map(|m| m.text.clone()).collect()
|
||||
}
|
||||
|
||||
/// Typed list detection: at the start of a paragraph, "1.", "1)", "A." or "A)" and a space start
|
||||
/// a numbered list, and "*", "#", ">" or "+" and a space start a bulleted list with the filled dot.
|
||||
/// The typed characters go, the paragraph takes the hanging indent, and the text typed next is the
|
||||
/// list item's. One Undo puts the typed characters back with no list, and typing goes on after
|
||||
/// them, also after a Backspace that deleted nothing.
|
||||
#[test]
|
||||
fn typing_a_list_start_and_a_space_starts_a_list() {
|
||||
use vectorcraft_doc::text::{Bullet, NumberStyle, ParaList};
|
||||
let v = ViewInfo::default();
|
||||
let (arabic, letters, dot) =
|
||||
(ParaList::numbered(NumberStyle::Arabic), ParaList::numbered(NumberStyle::UpperLetters), ParaList::bulleted(Bullet::Dot));
|
||||
let cases = [("1.", arabic, "1."), ("1)", arabic, "1."), ("A.", letters, "A."), ("A)", letters, "A.")];
|
||||
let bullets = ["*", "#", ">", "+"].map(|b| (b, dot, "\u{2022}"));
|
||||
for (typed, list, marker) in cases.into_iter().chain(bullets) {
|
||||
let mut s = session();
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 200.0, 200.0);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
for c in typed.chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
s.tool_text(" ", v).unwrap();
|
||||
let t = obj(&s, id);
|
||||
assert_eq!((t.plain_text().as_str(), t.para_at(0).list), ("", Some(list)), "{typed}");
|
||||
assert_eq!((t.para_at(0).left_indent, t.para_at(0).first_line_indent), (18.0, -18.0), "{typed}");
|
||||
assert_eq!(undo_labels(&s).last().map(String::as_str), Some("Bullets and Numbering"), "{typed}");
|
||||
s.tool_text("Apples", v).unwrap();
|
||||
assert_eq!((obj(&s, id).plain_text().as_str(), obj(&s, id).para_at(0).list), ("Apples", Some(list)), "{typed}");
|
||||
assert_eq!(list_markers(&s, id), [marker], "{typed}");
|
||||
key(&mut s, ToolKey::Enter, Mods::default());
|
||||
s.tool_text("Pears", v).unwrap();
|
||||
assert_eq!(obj(&s, id).para_at(1).list, Some(list), "Return continues the list: {typed}");
|
||||
// One Undo after the space puts the characters back with no list; typing goes on after them.
|
||||
let mut s = session();
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 200.0, 200.0);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
for c in typed.chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
s.tool_text(" ", v).unwrap();
|
||||
// A Backspace at the start of the empty item deletes nothing and leaves that as it is.
|
||||
key(&mut s, ToolKey::Backspace, Mods::default());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let t = obj(&s, id);
|
||||
assert_eq!((t.plain_text(), t.para_at(0).list), (format!("{typed} "), None), "{typed}");
|
||||
assert_eq!((t.para_at(0).left_indent, t.para_at(0).first_line_indent), (0.0, 0.0), "{typed}");
|
||||
s.tool_text("x", v).unwrap();
|
||||
assert_eq!((obj(&s, id).plain_text(), obj(&s, id).para_at(0).list), (format!("{typed} x"), None), "{typed}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(obj(&s, id).plain_text(), "", "the typing before the list is a step of its own: {typed}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A list typed before a paragraph's text: the caret stays before the text, goes after the typed
|
||||
/// characters when Undo puts them back, also after undoing what was typed after the list, and
|
||||
/// before the text again after Redo, also when the text starts with the same characters.
|
||||
#[test]
|
||||
fn the_caret_follows_undo_and_redo_of_a_typed_list() {
|
||||
let v = ViewInfo::default();
|
||||
for start in ["Apples", "* Apples"] {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, start);
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 101.0, 95.0);
|
||||
s.set_tool_option("select", &json!({"start": 0, "end": 0}));
|
||||
for c in "* ".chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
let state = |s: &Session| (obj(s, id).plain_text(), s.tool_options()["caret"].clone());
|
||||
assert_eq!(state(&s), (start.to_string(), json!(0)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(state(&s), (format!("* {start}"), json!(2)));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(state(&s), (start.to_string(), json!(0)));
|
||||
s.tool_text("x", v).unwrap();
|
||||
assert_eq!(obj(&s, id).plain_text(), format!("x{start}"));
|
||||
// Undo of the typing and then of the list: typing goes on after the characters again.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.tool_text("y", v).unwrap();
|
||||
assert_eq!(obj(&s, id).plain_text(), format!("* y{start}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The same characters later in a paragraph, without the space, typed in one piece with the space
|
||||
/// (a paste), or at the start of a paragraph that is already in a list start nothing.
|
||||
#[test]
|
||||
fn list_starts_typed_elsewhere_or_pasted_start_no_list() {
|
||||
use vectorcraft_doc::text::{NumberStyle, ParaList};
|
||||
let v = ViewInfo::default();
|
||||
let mut s = session();
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 200.0, 200.0);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
for c in "Fruit 1. * A) x".chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
key(&mut s, ToolKey::Enter, Mods::default());
|
||||
s.tool_text("1. Apples", v).unwrap();
|
||||
key(&mut s, ToolKey::Enter, Mods::default());
|
||||
for c in "1.Pears".chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
key(&mut s, ToolKey::Enter, Mods::default());
|
||||
s.tool_text("* ", v).unwrap();
|
||||
let t = obj(&s, id);
|
||||
assert_eq!(t.plain_text(), "Fruit 1. * A) x\n1. Apples\n1.Pears\n* ");
|
||||
assert_eq!((0..4).map(|i| t.para_at(i).list).collect::<Vec<_>>(), [None, None, None, None]);
|
||||
assert!(!undo_labels(&s).iter().any(|l| l == "Bullets and Numbering"));
|
||||
// A paragraph in a list keeps its list, and the typed characters stay as text.
|
||||
let id = text(&mut s, "Item");
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "list": "number"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
click(&mut s, 101.0, 95.0);
|
||||
s.set_tool_option("select", &json!({"start": 0, "end": 0}));
|
||||
for c in "* ".chars() {
|
||||
s.tool_text(&c.to_string(), v).unwrap();
|
||||
}
|
||||
let t = obj(&s, id);
|
||||
assert_eq!((t.plain_text().as_str(), t.para_at(0).list), ("* Item", Some(ParaList::numbered(NumberStyle::Arabic))));
|
||||
}
|
||||
|
||||
/// `text.startList`, which the Type tool runs, works from scripts too: by default it reads the
|
||||
/// text up to the end, and it changes nothing (and records no step) when the text there starts no
|
||||
/// list.
|
||||
#[test]
|
||||
fn start_list_command_removes_the_typed_characters_and_starts_the_list() {
|
||||
use vectorcraft_doc::text::{Bullet, NumberStyle, ParaList};
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "Fruit\n1) Apples");
|
||||
let r = s.execute("text.startList", &json!({"id": id.0, "at": 9})).unwrap();
|
||||
assert_eq!(r, json!({"id": id.0, "started": true, "caret": 6}));
|
||||
let t = obj(&s, id);
|
||||
assert_eq!(
|
||||
(t.plain_text().as_str(), t.para_at(0).list, t.para_at(1).list),
|
||||
("Fruit\nApples", None, Some(ParaList::numbered(NumberStyle::Arabic)))
|
||||
);
|
||||
assert_eq!(list_markers(&s, id), ["1."]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((obj(&s, id).plain_text().as_str(), obj(&s, id).para_at(1).list), ("Fruit\n1) Apples", None));
|
||||
let id = text(&mut s, "x\n# ");
|
||||
let r = s.execute("text.startList", &json!({"id": id.0})).unwrap();
|
||||
assert_eq!((r["started"].clone(), obj(&s, id).plain_text()), (json!(true), "x\n".to_string()));
|
||||
assert_eq!(obj(&s, id).para_at(1).list, Some(ParaList::bulleted(Bullet::Dot)));
|
||||
let steps = undo_labels(&s).len();
|
||||
let id = text(&mut s, "1.x");
|
||||
assert_eq!(s.execute("text.startList", &json!({"id": id.0, "at": 3})).unwrap()["started"], json!(false));
|
||||
assert_eq!(undo_labels(&s).len(), steps + 1, "only text.create's step");
|
||||
assert!(s.execute("text.startList", &json!({"id": id.0, "at": "3"})).is_err(), "`at` is a number");
|
||||
assert!(s.execute("text.startList", &json!({"at": 3})).is_err(), "id required");
|
||||
}
|
||||
|
||||
/// #1881: a Cmd/Ctrl drag while typing (the Selection tool lent to the Type tool) moves the type
|
||||
/// and keeps what was typed, and typing goes on after it.
|
||||
#[test]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+14
-1
@@ -1125,7 +1125,7 @@ naming the key, and change nothing: `radius` in `shape.rectangle`; `sides` and `
|
||||
and `radial` in `shape.polarGrid`; `size` and the `width`, `height` and `fitMinPercent` of `area` in `text.create`;
|
||||
`size` and `fitMinPercent` in `text.createInPath`; `start`, `end`, `size`, `tracking`,
|
||||
`baselineShift`, `hScale`, `vScale`, `rotation` and `strokeWidth` in `text.setRangeStyle`; `start` and `end` in
|
||||
`text.editRange` and `text.getRange`; `start`, `hScale`, `vScale` and `rotation` in `text.touchType`; `by`, `start` and
|
||||
`text.editRange` and `text.getRange`; `at` in `text.startList`; `start`, `hScale`, `vScale` and `rotation` in `text.touchType`; `by`, `start` and
|
||||
`end` in `type.step`; `by` in `type.size.increase` and `type.size.decrease`; `size` in `type.ruby`; `width`, `height`
|
||||
and `fitMinPercent` in `text.areaOptions`; `x` and `y` in `symbol.place`; `radius`, `density` and `scale` in
|
||||
`symbol.spray`; `radius` and `intensity` in `symbol.adjust`; `to` in `swatch.move`; `weight` and `opacity` in
|
||||
@@ -4468,6 +4468,19 @@ Type › Bullets and Numbering › Apply Bullets and the canvas context menu's B
|
||||
same menus runs `type.applyNumbers {number?}`. Without `number` it runs `text.setFormat {list: "number"}`, and with it
|
||||
it runs `text.setFormat {number}`.
|
||||
|
||||
Typing starts a list too. When the Type tool types a space on its own (one key press, or `type_text {text: " "}`) right
|
||||
after `1.` or `1)`, `A.` or `A)`, or one of `*`, `#`, `>` and `+` at the start of a paragraph that is in no list, the
|
||||
typed characters and the space are removed and the paragraph becomes a numbered list in 1, 2, 3 or A, B, C, or a
|
||||
bulleted list with the filled dot. A list started with `1)` or `A)` also writes its numbers with a period. A paragraph
|
||||
whose left and first-line indents are both 0 gets 18 pt and -18 pt. The tool runs `text.startList {id, at}`, with `at`
|
||||
the byte after the space, as an undo step of its own after the typing: one Undo puts the characters back as text with no
|
||||
list, and typing goes on after them; Redo removes them again, and typing goes on before the paragraph's text. The same
|
||||
characters later in a paragraph start nothing, and so do list characters that arrive in one piece with the space: a
|
||||
paste, an IME commit, or `type_text` of a longer string. From a script, `text.startList {id, at?}` (`at` defaults to the end of the text) does
|
||||
the same with the text before `at` and returns `started` and `caret`. When the list starts, `caret` is the byte where
|
||||
the paragraph's text now starts; when the text before `at` starts no list, nothing changes, `started` is false and
|
||||
`caret` is `at`.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"bullet":"square"}}}
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"number":"upperRoman"}}}
|
||||
|
||||
@@ -774,8 +774,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
|
||||
PDF export with type as text writes the hyphen that ends a line broken inside a word as a hyphen character where the line draws it, and the file opens with the hyphen in the line's text; kashidas are drawn as paths.
|
||||
The Paragraph panel's Insert Kashidas (with the Indic options; `text.setFormat {kashida}`) lengthens each Arabic word of a justified horizontal area type line with a kashida, a tatweel between its last pair of joined letters, before the word spaces widen: Short, Medium and Long kashidas are at most a quarter, half and a whole em long, Stylistic ones are whole tatweels up to an em, and None (the default) leaves the line as before; paragraph styles and the native file keep the setting.
|
||||
The Paragraph panel's Bullets button, Type › Bullets and Numbering › Apply Bullets and the context menu's Apply Bullets (`text.setFormat {list, bullet}`, `type.applyBullets`) make paragraphs a bulleted list with the documented presets (none, filled dot, dash, hollow circle, filled square; [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): the bullet starts each paragraph's first line in its first character's font, size and color, the first line's text starts at the next tab stop after the bullet (the left indent counts as a stop), the lines it wraps to start at the left indent, a paragraph with no indents gets 18 pt and -18 pt, and Return continues the list; Reset Panel turns it off, and paragraph styles and the native file keep it. The Bullets and Numbering dialog, levels, list detection while typing and Convert to Text are not built yet.
|
||||
The Paragraph panel's Bullets button, Type › Bullets and Numbering › Apply Bullets and the context menu's Apply Bullets (`text.setFormat {list, bullet}`, `type.applyBullets`) make paragraphs a bulleted list with the documented presets (none, filled dot, dash, hollow circle, filled square; [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): the bullet starts each paragraph's first line in its first character's font, size and color, the first line's text starts at the next tab stop after the bullet (the left indent counts as a stop), the lines it wraps to start at the left indent, a paragraph with no indents gets 18 pt and -18 pt, and Return continues the list; Reset Panel turns it off, and paragraph styles and the native file keep it. The Bullets and Numbering dialog, levels and Convert to Text are not built yet.
|
||||
The Paragraph panel's Numbered List button, Type › Bullets and Numbering › Apply Numbers and the context menu's Apply Numbers (`text.setFormat {list, number}`, `type.applyNumbers`) make paragraphs a numbered list with the documented presets (1, A, a, I, i, 01, 001, and None, which turns the list off; [Bullets and numbering](https://web.archive.org/web/20250807202418/https://helpx.adobe.com/illustrator/using/bullets-numbering.html), [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): each paragraph starts with its number and a period, counted through the list paragraphs of the story and its threaded frames, with the text and indents as in a bulleted list; letters go on from Z to AA, and roman numerals go on from III to IV, an empty paragraph is not numbered, and a paragraph in no list starts the count again at 1.
|
||||
The Type tool starts a list from the characters [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html) names (`text.startList`): a space typed on its own after `1.`, `1)`, `A.` or `A)` at the start of a paragraph in no list makes the paragraph a numbered list in 1, 2, 3 or A, B, C, and a space typed after `*`, `#`, `>` or `+` makes it a bulleted list with the filled dot. The typed characters are removed, and one Undo puts them back as text with no list. The same characters later in a paragraph, and pasted list characters, start nothing.
|
||||
The Character panel menu's Character Direction (with the Indic options; `text.setFormat {charDirection}`, `text.setRangeStyle {charDirection}`) sets the selected characters left to right or right to left inside their paragraph, as a Unicode bidi embedding: a date set Left to Right keeps its order in right-to-left text, and a word set Right to Left keeps the punctuation after it on its left. Default (the default) leaves the characters to the bidi algorithm; character styles and the native file keep the setting.
|
||||
In vertical type, the Tabs panel names its alignment buttons Top-Justified Tab, Center-Justified Tab, Bottom-Justified Tab and Decimal-Justified Tab, in their tool tips and for screen readers, and labels the position field Y. In horizontal type the buttons keep the names Left-Justified Tab and Right-Justified Tab, and the field keeps the label X.
|
||||
The Tabs panel's ruler is numbered in the General unit with the canvas rulers' label steps and a tick every tenth of a step. With the panel menu's Snap to Unit on, or Shift held, a tab stop placed by a click on the ruler or dragged on it lands on the nearest tick (`text.tabs.set`).
|
||||
|
||||
Reference in New Issue
Block a user