mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 06:30:00 -06:00
M7: per-paragraph paragraph attributes
Each paragraph has its own alignment, indents, spacing, hyphenation, tab stops and paragraph style (TextObject.paras; para stays paragraph 0's, so old files and readers work). Layout, editing (split/merge), threads, the Paragraph/Tabs/Paragraph Styles panels, ranged text.setStyle/setFormat/ tabs/paraStyle.apply, SVG export/import and the fuzzers follow.
This commit is contained in:
@@ -44,6 +44,10 @@ enum Mode {
|
||||
struct Typing {
|
||||
base: Vec<TextRun>,
|
||||
cur: Vec<TextRun>,
|
||||
/// The first byte any edit of the session touched (the text before it is unchanged): the
|
||||
/// previewed span starts no later, so Return at a paragraph's end splits that paragraph (its
|
||||
/// style continues), not the next one.
|
||||
lo: usize,
|
||||
}
|
||||
|
||||
/// IME marked text inside the typing session, not yet committed.
|
||||
@@ -174,9 +178,10 @@ impl TypeTool {
|
||||
if self.typing.is_none() {
|
||||
out.push(Action::Begin("Typing".into()));
|
||||
}
|
||||
let ty = self.typing.get_or_insert_with(|| Typing { base: t.runs.clone(), cur: t.runs.clone() });
|
||||
let ty = self.typing.get_or_insert_with(|| Typing { base: t.runs.clone(), cur: t.runs.clone(), lo: usize::MAX });
|
||||
let len = edit::runs_len(&ty.cur);
|
||||
let (a, b) = (self.caret.min(self.anchor).min(len), self.caret.max(self.anchor).min(len));
|
||||
ty.lo = ty.lo.min(a);
|
||||
let caret = match &styled {
|
||||
Some(r) => edit::replace_range_styled(&mut ty.cur, a, b, r),
|
||||
None => edit::replace_range(&mut ty.cur, a, b, insert),
|
||||
@@ -187,7 +192,7 @@ impl TypeTool {
|
||||
// One preview for the whole session: the changed span of base → cur, with its runs.
|
||||
let base: String = ty.base.iter().map(|r| r.text.as_str()).collect();
|
||||
let cur: String = ty.cur.iter().map(|r| r.text.as_str()).collect();
|
||||
let (p, s) = common_affixes(&base, &cur);
|
||||
let (p, s) = common_affixes(&base, &cur, ty.lo);
|
||||
let runs = edit::slice_runs(&ty.cur, p, cur.len() - s);
|
||||
let runs = if runs.is_empty() { json!([]) } else { serde_json::to_value(&runs).unwrap_or(json!([])) };
|
||||
out.push(Action::Preview("text.editRange".into(), json!({"id": id.0, "start": p, "end": base.len() - s, "runs": runs})));
|
||||
@@ -332,11 +337,12 @@ fn char_range_to_bytes(s: &str, r: Range<usize>) -> Option<Range<usize>> {
|
||||
(a <= b).then_some(a..b)
|
||||
}
|
||||
|
||||
/// Lengths of the common prefix and suffix of `a` and `b` (on char boundaries, not overlapping).
|
||||
fn common_affixes(a: &str, b: &str) -> (usize, usize) {
|
||||
/// Lengths of the common prefix (at most `max_prefix`) and suffix of `a` and `b` (on char
|
||||
/// boundaries, not overlapping).
|
||||
fn common_affixes(a: &str, b: &str, max_prefix: usize) -> (usize, usize) {
|
||||
let mut p = 0;
|
||||
for ((i, x), y) in a.char_indices().zip(b.chars()) {
|
||||
if x != y {
|
||||
if x != y || i + x.len_utf8() > max_prefix {
|
||||
break;
|
||||
}
|
||||
p = i + x.len_utf8();
|
||||
|
||||
@@ -221,10 +221,13 @@ fn select_all_and_styled_paste() {
|
||||
|
||||
#[test]
|
||||
fn common_affixes_are_char_safe() {
|
||||
assert_eq!(common_affixes("hello", "help"), (3, 0));
|
||||
assert_eq!(common_affixes("aXb", "ab"), (1, 1));
|
||||
assert_eq!(common_affixes("aaa", "aaaa"), (3, 0));
|
||||
assert_eq!(common_affixes("é1", "è1"), (0, 1));
|
||||
assert_eq!(common_affixes("hello", "help", usize::MAX), (3, 0));
|
||||
assert_eq!(common_affixes("aXb", "ab", usize::MAX), (1, 1));
|
||||
assert_eq!(common_affixes("aaa", "aaaa", usize::MAX), (3, 0));
|
||||
assert_eq!(common_affixes("é1", "è1", usize::MAX), (0, 1));
|
||||
// Return at the end of a paragraph: the span starts where the edit did (before the old break).
|
||||
assert_eq!(common_affixes("abc\ndef", "abc\n\ndef", 3), (3, 4));
|
||||
assert_eq!(common_affixes("abc\ndef", "abc\n\ndef", 4), (4, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user