mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: the caret steps over whole clusters (Thai marks, accents, emoji)
The Type tool's Left and Right arrows and Delete moved one code point at a time, so the caret could stop between a Thai consonant and its vowel or tone marks (or inside an accented letter or emoji sequence), where typing or deleting splits the cluster. They now step over extended grapheme clusters. Backspace still takes back one character at a time, as Thai typists expect for a mistyped tone mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
436d7ecdd3
commit
3fb51869ac
Generated
+1
@@ -4813,6 +4813,7 @@ dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-general-category",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
"windows-registry",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -21,6 +21,9 @@ subsetter = { version = "0.2", default-features = false, features = ["variable-f
|
||||
unicode-bidi = "0.3.18"
|
||||
unicode-general-category = "1"
|
||||
unicode-script = "0.5"
|
||||
# Grapheme clusters (a Thai consonant with its vowel and tone marks, an accented letter, an emoji
|
||||
# sequence) for caret movement; MIT OR Apache-2.0, already in the build.
|
||||
unicode-segmentation = "1.13"
|
||||
# Builds a small variable font for tests (`test_fonts`; no font file is committed). MIT OR
|
||||
# Apache-2.0, already in the build through subsetter.
|
||||
write-fonts = { version = "0.48", optional = true }
|
||||
|
||||
@@ -190,6 +190,21 @@ pub fn next_char(s: &str, i: usize) -> usize {
|
||||
s[i..].chars().next().map(|c| i + c.len_utf8()).unwrap_or(s.len())
|
||||
}
|
||||
|
||||
/// The start of the grapheme cluster before `i` (Left arrow): a Thai consonant with its vowel and
|
||||
/// tone marks, an accented letter or an emoji sequence is passed over whole.
|
||||
pub fn prev_cluster(s: &str, i: usize) -> usize {
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
let i = floor_char(s, i);
|
||||
GraphemeCursor::new(i, s.len(), true).prev_boundary(s, 0).ok().flatten().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The end of the grapheme cluster at `i` (Right arrow, Delete): see [`prev_cluster`].
|
||||
pub fn next_cluster(s: &str, i: usize) -> usize {
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
let i = floor_char(s, i);
|
||||
GraphemeCursor::new(i, s.len(), true).next_boundary(s, 0).ok().flatten().unwrap_or(s.len())
|
||||
}
|
||||
|
||||
fn is_word_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '\'' || c == '’'
|
||||
}
|
||||
|
||||
@@ -791,3 +791,17 @@ fn proportional_metrics_set_full_width_glyphs_on_their_proportional_widths() {
|
||||
let moved = x0(&half)[1] - x0(&none)[1];
|
||||
assert!((moved - 10.0).abs() < 0.01, "〙's empty half goes past the line's end: あ moved {moved}");
|
||||
}
|
||||
|
||||
/// The arrows pass a Thai consonant with its vowel and tone marks whole (น้ำ: no, mai tho, sara am),
|
||||
/// as an accented letter written with a combining mark; plain text moves a character at a time.
|
||||
#[test]
|
||||
fn caret_moves_over_whole_clusters() {
|
||||
let s = "กน\u{0E49}\u{0E33}e\u{0301}x";
|
||||
let stops: Vec<usize> = std::iter::successors(Some(0), |&i| (i < s.len()).then(|| edit::next_cluster(s, i))).collect();
|
||||
assert_eq!(stops, [0, 3, 12, 15, 16]);
|
||||
let back: Vec<usize> = std::iter::successors(Some(s.len()), |&i| (i > 0).then(|| edit::prev_cluster(s, i))).collect();
|
||||
assert_eq!(back, [16, 15, 12, 3, 0]);
|
||||
// Inside a cluster (a caret left there by a click), the arrows go to its edges.
|
||||
assert_eq!((edit::prev_cluster(s, 6), edit::next_cluster(s, 6)), (3, 12));
|
||||
assert_eq!((edit::next_cluster("", 0), edit::prev_cluster("ab", 99)), (0, 1));
|
||||
}
|
||||
|
||||
@@ -530,8 +530,10 @@ impl Tool for TypeTool {
|
||||
self.anchor = if word { edit::prev_word(&text, self.caret) } else { edit::prev_char(&text, self.caret) };
|
||||
self.replace(cx, "", None)
|
||||
}
|
||||
// Backspace takes back one character (a mistyped Thai tone mark, say); Delete, the
|
||||
// Left and Right arrows a whole cluster.
|
||||
ToolKey::Delete if self.caret < len => {
|
||||
self.anchor = if word { edit::next_word(&text, self.caret) } else { edit::next_char(&text, self.caret) };
|
||||
self.anchor = if word { edit::next_word(&text, self.caret) } else { edit::next_cluster(&text, self.caret) };
|
||||
self.replace(cx, "", None)
|
||||
}
|
||||
ToolKey::Left => {
|
||||
@@ -549,7 +551,7 @@ impl Tool for TypeTool {
|
||||
if lay.glyphs.iter().any(|g| g.rtl) {
|
||||
vectorcraft_text::caret_horizontal(&lay, self.caret, false)
|
||||
} else {
|
||||
edit::prev_char(&text, self.caret)
|
||||
edit::prev_cluster(&text, self.caret)
|
||||
}
|
||||
};
|
||||
self.move_to(to, mods.shift)
|
||||
@@ -569,7 +571,7 @@ impl Tool for TypeTool {
|
||||
if lay.glyphs.iter().any(|g| g.rtl) {
|
||||
vectorcraft_text::caret_horizontal(&lay, self.caret, true)
|
||||
} else {
|
||||
edit::next_char(&text, self.caret)
|
||||
edit::next_cluster(&text, self.caret)
|
||||
}
|
||||
};
|
||||
self.move_to(to, mods.shift)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Type parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the caret steps over whole clusters such as Thai consonants with their marks; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
|
||||
@@ -37,6 +38,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` |
|
||||
| Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary |
|
||||
| Smart Punctuation, Change Case, Find & Replace | done | |
|
||||
| Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | |
|
||||
| Touch Type tool | missing | listed in the toolbar, not implemented |
|
||||
| Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model |
|
||||
| Snap to Glyph | missing | menu stub |
|
||||
@@ -52,6 +54,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | The Type tool's arrows and Delete move by grapheme cluster, never between a Thai consonant and its vowel or tone marks; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Thai (Lao, Khmer, Myanmar) missing from the font in use falls back to an installed font made for the script instead of whichever face covering it loaded first; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Proportional Metrics done: `palt` in horizontal type, `vpal` in vertical type (#966, #1082); ruby and manual tate-chu-yoko still missing |
|
||||
| 2026-10-10 | minor | #1034: transformed character controls and regression coverage; preserving group font sizes remains a separate enhancement. Scores/hours unchanged at this rounding. |
|
||||
|
||||
Reference in New Issue
Block a user