feat(unicode,datastruct): port Unicode tables and data structures
ghostty-unicode: Ghostty computes codepoint properties with the uucode Zig package (v0.2.0, Unicode 17.0.0). There is no Rust equivalent, so a trimmed UCD set is vendored in ucd/ and build.rs re-implements uucode's derivations: cell width rules incl. Ghostty's overrides for emoji modifiers and Prepend, grapheme break classes with the compile-time grapheme_break state table, the is_symbol set, and emoji presentation. Lookups are three array indexes per codepoint. Validated against the official GraphemeBreakTest.txt. ghostty-datastruct: split_tree (used for GTK splits; f16 ratios so rounding and debug diagrams match Zig), circ_buf, lru, cache_table, blocking_queue, segmented_pool, message_data, comparison, array_list_collection. intrusive_linked_list is not needed (lru is index based). lru now reports the evicted (old) key, Zig reported the new key, which appears to be a bug. Not yet covered: case_folding_full (needed by input bindings). Test Plan: - cargo test -p ghostty-unicode (16) -p ghostty-datastruct (88) - cargo clippy for both crates with -D warnings
This commit is contained in:
1 parent
e724645057
commit
3323c3f525
31 files changed
+18468
-3
No files matched your search
@@ -1 +1,184 @@
|
||||
//! ghostty-unicode: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//! Unicode properties for Ghostty: codepoint width, grapheme breaking, and
|
||||
//! the "is symbol" table used by the renderer for constraint width.
|
||||
//!
|
||||
//! Port of `src/unicode/*`. Ghostty derives these from the `uucode` package
|
||||
//! configured by `src/build/uucode_config.zig` (Unicode 17.0.0); here the
|
||||
//! same values are generated at build time (`build.rs`) from vendored UCD
|
||||
//! files. See `docs/rewrite/notes/ghostty-unicode.md`.
|
||||
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
pub mod emoji_table;
|
||||
pub mod grapheme;
|
||||
pub mod lut;
|
||||
pub mod props;
|
||||
pub mod props_table;
|
||||
pub mod symbols_table;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../build_support/ucd.rs"]
|
||||
mod props_uucode;
|
||||
|
||||
pub use grapheme::{BreakState, grapheme_break};
|
||||
pub use props::{GraphemeBreakNoControl, Properties};
|
||||
pub use props_table::table;
|
||||
|
||||
/// Unicode version of the vendored data.
|
||||
pub const UNICODE_VERSION: (u8, u8, u8) = (17, 0, 0);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unicode_props_tables_match_uucode() {
|
||||
let ucd =
|
||||
props_uucode::Ucd::load(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("ucd"));
|
||||
for cp in 0..=0x1F_FFFFu32 {
|
||||
assert_eq!(table().get(cp), ucd.get(cp), "mismatch cp=U+{cp:x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_symbols_tables_match_uucode() {
|
||||
let ucd =
|
||||
props_uucode::Ucd::load(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("ucd"));
|
||||
for cp in 0..=0x1F_FFFFu32 {
|
||||
assert_eq!(
|
||||
symbols_table::table().get(cp),
|
||||
ucd.is_symbol(cp),
|
||||
"mismatch cp=U+{cp:x}"
|
||||
);
|
||||
assert_eq!(emoji_table::table().get(cp), ucd.is_emoji_presentation(cp));
|
||||
}
|
||||
}
|
||||
|
||||
fn w(cp: u32) -> u8 {
|
||||
table().get(cp).width
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_ascii() {
|
||||
for cp in 0x20..0x7F {
|
||||
assert_eq!(w(cp), 1, "{cp:x}");
|
||||
}
|
||||
// Control characters are zero width (handled before lookup by the
|
||||
// terminal).
|
||||
for cp in (0..0x20).chain(0x7F..0xA0) {
|
||||
assert_eq!(w(cp), 0, "{cp:x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_cjk_and_fullwidth() {
|
||||
assert_eq!(w('中' as u32), 2);
|
||||
assert_eq!(w('あ' as u32), 2);
|
||||
assert_eq!(w('한' as u32), 2);
|
||||
assert_eq!(w(0xFF21), 2); // fullwidth A
|
||||
assert_eq!(w(0x3000), 2); // ideographic space
|
||||
assert_eq!(w(0xFF61), 1); // halfwidth ideographic full stop
|
||||
assert_eq!(w(0x20000), 2); // CJK ext B
|
||||
assert_eq!(w(0x3FFFD), 2);
|
||||
// Ambiguous width is narrow.
|
||||
assert_eq!(w(0x00B1), 1);
|
||||
assert_eq!(w(0x2460), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_emoji() {
|
||||
assert_eq!(w(0x1F600), 2); // grinning face
|
||||
assert_eq!(w(0x1F680), 2);
|
||||
assert_eq!(w(0x2764), 1); // heart, text default
|
||||
assert_eq!(w(0x231A), 2); // watch, emoji presentation
|
||||
assert!(table().get(0x2764).emoji_vs_base);
|
||||
assert!(!table().get(0x1F600).emoji_vs_base);
|
||||
assert!(emoji_table::table().get(0x1F600));
|
||||
assert!(!emoji_table::table().get(0x2764));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_zero_and_combining() {
|
||||
assert_eq!(w(0x0301), 0); // combining acute
|
||||
assert_eq!(w(0x20DD), 0); // enclosing circle
|
||||
assert_eq!(w(0x200B), 0); // ZWSP (default ignorable)
|
||||
assert_eq!(w(0x200D), 0); // ZWJ
|
||||
assert_eq!(w(0x200C), 0); // ZWNJ
|
||||
assert_eq!(w(0xFE0E), 0); // VS15
|
||||
assert_eq!(w(0xFE0F), 0); // VS16
|
||||
assert_eq!(w(0xE0100), 0); // VS17
|
||||
assert_eq!(w(0x2028), 0); // line separator
|
||||
assert_eq!(w(0xD800), 0); // surrogate
|
||||
assert_eq!(w(0x00AD), 1); // soft hyphen
|
||||
assert_eq!(w(0x2E3A), 2); // two-em dash
|
||||
assert_eq!(w(0x2E3B), 2); // three-em dash clamped to 2
|
||||
for cp in [0x0301, 0x200D, 0xFE0F, 0x1F3FB, 0x1160, 0x11A8] {
|
||||
assert!(table().get(cp).width_zero_in_grapheme, "{cp:x}");
|
||||
}
|
||||
assert!(!table().get('a' as u32).width_zero_in_grapheme);
|
||||
assert!(!table().get(0x1F600).width_zero_in_grapheme);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_emoji_modifier_prepend_ri_keycap() {
|
||||
// Emoji modifiers display as colour patches on their own.
|
||||
for cp in 0x1F3FB..=0x1F3FF {
|
||||
assert_eq!(w(cp), 2);
|
||||
assert!(table().get(cp).width_zero_in_grapheme);
|
||||
assert_eq!(
|
||||
table().get(cp).grapheme_break,
|
||||
GraphemeBreakNoControl::EmojiModifier
|
||||
);
|
||||
}
|
||||
// Prepend characters keep a width.
|
||||
assert_eq!(w(0x0600), 1); // Arabic number sign (prepend, Cf)
|
||||
assert_eq!(
|
||||
table().get(0x0600).grapheme_break,
|
||||
GraphemeBreakNoControl::Prepend
|
||||
);
|
||||
assert!(table().get(0x0600).width_zero_in_grapheme);
|
||||
assert_eq!(w(0x0D4E), 1); // Malayalam letter dot reph
|
||||
// Regional indicators are wide.
|
||||
assert_eq!(w(0x1F1E6), 2);
|
||||
assert_eq!(w(0x1F1FF), 2);
|
||||
assert_eq!(
|
||||
table().get(0x1F1E6).grapheme_break,
|
||||
GraphemeBreakNoControl::RegionalIndicator
|
||||
);
|
||||
// Combining enclosing keycap is zero width in the table.
|
||||
assert_eq!(w(0x20E3), 0);
|
||||
// Hangul jamo.
|
||||
assert_eq!(w(0x1100), 2); // choseong
|
||||
assert_eq!(w(0x1160), 0); // jungseong, zero (V)
|
||||
assert_eq!(w(0x11A8), 0); // jongseong (T)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_defaults() {
|
||||
// Beyond the Unicode range (props_uucode.zig `get`).
|
||||
let p = table().get(0x11_0000);
|
||||
assert_eq!(p.width, 1);
|
||||
assert!(p.width_zero_in_grapheme);
|
||||
assert_eq!(p.grapheme_break, GraphemeBreakNoControl::Other);
|
||||
assert!(!p.emoji_vs_base);
|
||||
assert_eq!(table().get(0x1F_FFFF), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols() {
|
||||
let t = symbols_table::table();
|
||||
assert!(t.get(0xE000)); // private use
|
||||
assert!(t.get(0xF0001)); // supplementary private use
|
||||
assert!(t.get(0x2190)); // arrows
|
||||
assert!(t.get(0x2713)); // dingbats
|
||||
assert!(t.get(0x1F600)); // emoticons
|
||||
assert!(t.get(0x2600)); // misc symbols
|
||||
assert!(t.get(0x2460)); // enclosed alphanumerics
|
||||
assert!(t.get(0x1F100)); // enclosed alphanumeric supplement
|
||||
assert!(t.get(0x1F300)); // misc symbols & pictographs
|
||||
assert!(t.get(0x1F680)); // transport & map
|
||||
assert!(!t.get('a' as u32));
|
||||
assert!(!t.get('中' as u32));
|
||||
assert!(!t.get(0x2500)); // box drawing
|
||||
assert!(!t.get(0x11_0000));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user