mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 22:06:49 -06:00
Text: Backspace removes Thai and other marks one at a time (#1220)
Co-Authored-By: GPT-6 Luna <noreply@openai.com>
This commit is contained in:
co-authored by
GPT-6 Luna
parent
3476f4b1e7
commit
8989e7b253
@@ -60,11 +60,14 @@ fn arrows_and_deletes_step_over_whole_clusters() {
|
||||
key(&mut s, "Right", true, false, false).unwrap();
|
||||
assert_eq!(caret(&s), (1, 6));
|
||||
key(&mut s, "Left", false, false, false).unwrap();
|
||||
// Delete and Backspace remove a whole cluster.
|
||||
// Delete removes a whole cluster; Backspace takes a letter's marks one at a time (#1220) and
|
||||
// an emoji cluster whole.
|
||||
key(&mut s, "Delete", false, false, false).unwrap();
|
||||
assert_eq!(chars(&s, t), "ab\u{1F1EF}\u{1F1F5}e\u{0301}");
|
||||
key(&mut s, "Right", false, false, true).unwrap(); // to the end
|
||||
key(&mut s, "Backspace", false, false, false).unwrap();
|
||||
assert_eq!(chars(&s, t), "ab\u{1F1EF}\u{1F1F5}e");
|
||||
key(&mut s, "Backspace", false, false, false).unwrap();
|
||||
assert_eq!(chars(&s, t), "ab\u{1F1EF}\u{1F1F5}");
|
||||
key(&mut s, "Backspace", false, false, false).unwrap();
|
||||
assert_eq!(chars(&s, t), "ab");
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn key(s: &mut Session, key: &str, shift: bool, word: bool, line: bool) -> R
|
||||
} else if word {
|
||||
word_left(&chars, a)
|
||||
} else {
|
||||
figcraft_text::grapheme_step(&t.characters, a, false)
|
||||
figcraft_text::backspace_step(&t.characters, a)
|
||||
};
|
||||
(from, a)
|
||||
} else {
|
||||
@@ -574,6 +574,16 @@ mod tests {
|
||||
end(&mut s).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_removes_only_the_final_thai_mark() {
|
||||
let (mut s, id) = setup("น้ำ");
|
||||
begin(&mut s, id, Some(3), false).unwrap();
|
||||
|
||||
key(&mut s, "Backspace", false, false, false).unwrap();
|
||||
|
||||
assert_eq!(data(&s, id).characters, "น้");
|
||||
}
|
||||
|
||||
fn ev(kind: PointerKind, x: f64, y: f64, shift: bool) -> PointerEvent {
|
||||
let mut e = PointerEvent::at(kind, x, y, 1.0);
|
||||
e.shift = shift;
|
||||
|
||||
@@ -361,6 +361,18 @@ fn grapheme_clusters() {
|
||||
assert!(grapheme_starts("plain ascii").iter().all(|b| *b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_removes_marks_but_keeps_other_graphemes_atomic() {
|
||||
assert_eq!(crate::backspace_step("น้ำ", 3), 2); // Thai spacing mark
|
||||
assert_eq!(crate::backspace_step("e\u{0301}", 2), 1); // Latin combining mark
|
||||
assert_eq!(crate::backspace_step("👨👩👧", 5), 0); // Emoji ZWJ sequence
|
||||
assert_eq!(crate::backspace_step("🇯🇵", 2), 0); // Flag
|
||||
assert_eq!(crate::backspace_step("x", 1), 0); // Ordinary character
|
||||
assert_eq!(crate::backspace_step("\u{1112}\u{1161}\u{11AB}", 3), 0); // Hangul syllable as conjoining jamo
|
||||
assert_eq!(crate::backspace_step("a\u{200D}", 2), 0); // A letter ending in ZWJ
|
||||
assert_eq!(crate::backspace_step("1\u{FE0F}\u{20E3}", 3), 0); // Keycap
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cluster_is_shaped_with_one_face() {
|
||||
if face().is_none() {
|
||||
|
||||
@@ -89,7 +89,7 @@ pub(crate) fn grapheme_starts(text: &str) -> Vec<bool> {
|
||||
}
|
||||
|
||||
/// The char index of the grapheme cluster boundary after (`forward`) or before char `index` in
|
||||
/// `text`: where an arrow key, Delete or Backspace moves the caret to.
|
||||
/// `text`: where an arrow key or Delete moves the caret to.
|
||||
pub fn grapheme_step(text: &str, index: usize, forward: bool) -> usize {
|
||||
let mut at = 0;
|
||||
for g in text.graphemes(true) {
|
||||
@@ -102,6 +102,29 @@ pub fn grapheme_step(text: &str, index: usize, forward: bool) -> usize {
|
||||
at.min(index)
|
||||
}
|
||||
|
||||
/// The start of the text deleted by Backspace at char `index`.
|
||||
///
|
||||
/// A letter's trailing mark (Grapheme_Cluster_Break Extend or SpacingMark, such as Thai MAI THO or
|
||||
/// SARA AM) is deleted on its own; other grapheme clusters remain atomic, so emoji sequences, flags,
|
||||
/// keycaps and Hangul syllables written as conjoining jamo stay intact.
|
||||
pub fn backspace_step(text: &str, index: usize) -> usize {
|
||||
let end = text.chars().count().min(index);
|
||||
let start = grapheme_step(text, end, false);
|
||||
if start == end {
|
||||
return end;
|
||||
}
|
||||
|
||||
let cluster = text.chars().skip(start).take(end - start).collect::<Vec<_>>();
|
||||
let letter_cluster = cluster.len() > 1 && cluster.first().is_some_and(|c| c.is_alphabetic());
|
||||
if letter_cluster && cluster.last().is_some_and(|&c| is_trailing_mark(c)) { end - 1 } else { start }
|
||||
}
|
||||
|
||||
/// Whether `c` is Extend or SpacingMark: it joins a plain letter's cluster. ZWJ also joins one but
|
||||
/// is a joiner, not a mark.
|
||||
fn is_trailing_mark(c: char) -> bool {
|
||||
c != '\u{200D}' && format!("a{c}").graphemes(true).count() == 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -13,7 +13,7 @@ pub mod fontdb;
|
||||
mod layout;
|
||||
mod onpath;
|
||||
|
||||
pub use breaks::grapheme_step;
|
||||
pub use breaks::{backspace_step, grapheme_step};
|
||||
pub use color::{ColorBitmap, ColorInk, ColorLayer};
|
||||
pub use fontdb::{
|
||||
Axis, CRAFT_FONTS, Coords, Face, FontDb, NamedInstance, Rescan, axis_label, db, face_with_char, family_names, family_styles, find_face,
|
||||
|
||||
Reference in New Issue
Block a user