feat(font): add run iterator, shared grid set and config integration
Completes the font engine port: - shaper/run.rs ports run.zig: the RunIterator over terminal rows that splits text runs at style/face boundaries, selection and cursor, handles graphemes and ligatures; Shaper::shape_run shapes a run. - shared_grid_set.rs ports SharedGridSet.zig: grids keyed by a config derived key, handed out as Arc<SharedGrid>. - Font-related types now have one canonical definition in ghostty-config (Variation, Descriptor, CodepointMap, metric modifiers, freetype load flags, synthetic style, shaping break) which the font crate re-exports. MetricModifier::apply_i32 now saturates to -i32::MAX like Zig. - The 23 terminal-driven harfbuzz shaper tests are ported, printing via Screen until Terminal lands. Test Plan: - cargo test -p ghostty-font (157 passed, 3 ignored - also skipped in Zig); Devanagari/Chakma/Bengali tests return early without system fonts - cargo test -p ghostty-config (255 passed)
This commit is contained in:
1 parent
b64a2886ec
commit
7b408257c4
19 files changed
+3204
-554
No files matched your search
Generated
+902
-8
@@ -23,6 +23,56 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
@@ -336,6 +386,29 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df683f1d30b457964673a5541a4603208ef95ab6873d2a55871895cf310f3b56"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-sys-rs"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee548131103ad8f698c6725669647b9c757b3b66992dd6a026037596417bee21"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
@@ -364,6 +437,16 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba9e9ec16c447027685b1f897b720e18e9a8afd00bd7332c483537e38086c9f"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
@@ -384,7 +467,7 @@ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -396,6 +479,12 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.1"
|
||||
@@ -492,7 +581,7 @@ dependencies = [
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook 0.3.18",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
@@ -566,6 +655,37 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
version = "0.3.2"
|
||||
@@ -616,7 +736,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -628,6 +748,12 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.19.0"
|
||||
@@ -661,6 +787,29 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -731,6 +880,16 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
@@ -794,12 +953,32 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
@@ -819,6 +998,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
@@ -832,11 +1022,123 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d5209294ba3775f9b650bf78bfadeba4332089f2329b3e2f6ce0a4957a45bff"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e77c579bac9eff8b84c0625d5f533da6d69988c063062fe92c2e955ede8f28eb"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk4-sys",
|
||||
"gio",
|
||||
"gl",
|
||||
"glib",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-sys"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eff6238a30c1f198fb3c8d3d445fa7faf211d11d9075af0772e6e0ca32ae001"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-wayland"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99cc5266dc4e8745a4c4683b26a0c54977a452809d572c940e39c6844663c0ec"
|
||||
dependencies = [
|
||||
"gdk4",
|
||||
"gdk4-wayland-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-wayland-sys"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb55bc5225519d34fdf1412db3755af88d1388a1c2a053d92e6751db13d96e68"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-x11"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "519725adee2c62e6ae4cc39e2ec9137b04f0c894f1c673ce17ab8753850534a3"
|
||||
dependencies = [
|
||||
"gdk4",
|
||||
"gdk4-x11-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"x11-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-x11-sys"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf4849da10d077290e2bb8805278a0afd980af7928fbbb5a915821eb4b2b099"
|
||||
dependencies = [
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -905,9 +1207,31 @@ dependencies = [
|
||||
name = "ghostty-apprt-gtk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"env_logger",
|
||||
"gdk4-wayland",
|
||||
"gdk4-x11",
|
||||
"ghostty-apprt",
|
||||
"ghostty-config",
|
||||
"ghostty-core",
|
||||
"ghostty-datastruct",
|
||||
"ghostty-input",
|
||||
"ghostty-os",
|
||||
"ghostty-vt",
|
||||
"glib-build-tools",
|
||||
"glow",
|
||||
"gtk4",
|
||||
"gtk4-layer-shell",
|
||||
"half",
|
||||
"libadwaita",
|
||||
"libc",
|
||||
"libloading 0.9.0",
|
||||
"log",
|
||||
"signal-hook 0.4.5",
|
||||
"thiserror 2.0.21",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -966,8 +1290,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"freetype-sys",
|
||||
"ghostty-config",
|
||||
"ghostty-datastruct",
|
||||
"ghostty-terminal",
|
||||
"ghostty-unicode",
|
||||
"ghostty-vt",
|
||||
"harfbuzz-sys",
|
||||
"log",
|
||||
"png",
|
||||
@@ -1024,10 +1351,12 @@ dependencies = [
|
||||
name = "ghostty-terminal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ghostty-config",
|
||||
"ghostty-datastruct",
|
||||
"ghostty-unicode",
|
||||
"ghostty-vt",
|
||||
"log",
|
||||
"onig",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1057,6 +1386,105 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.22.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4620fbafdf19dba814473e19f18e75290cb3338662f6ae35f0402affd3b2b699"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28739f914c15b87a9856000bed9cbd5b3a8afbca5e982d9a299e1601422cb5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
|
||||
dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
|
||||
dependencies = [
|
||||
"khronos_api",
|
||||
"log",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.22.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4cf9e5bd8713f1990a92c422998826e0fb34824277b46bc59757373483c6694"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-build-tools"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9871f38b67853c358b8190f77b9f878eb27d933a950f1045b244c4559a9f5f0"
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9597c68fa7bdf4154a3079642cd21c4dccac0b0bdd2897d82861523bf91e7b9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99b38907e67e40dec9b60f858bcada952598719cb512a13eb13d6cdcd16f8295"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
@@ -1075,6 +1503,149 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07595c9ba696bd9819cb6a8df39f08078f3dd679508fe052dd558492c2053834"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-rs"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"graphene-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97842c7543828f98aa5583aa54fcda28aa84fa2b0f8c556142177f7bbb638057"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eee23d7425c5281ec5a2f40f7b69384aac66c15861d990d248e9d9c9368be464"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk4",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4-sys"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27900c92e47df410bbe43b51818104051e3409da43fb88547a41c0bafddfd39c"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f8226c94dd0b65ba2e8fcd8ff402d380e890f6e5a6488e652a0158e827b9bf"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"field-offset",
|
||||
"futures-channel",
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4",
|
||||
"gtk4-macros",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-layer-shell"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"gdk4",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gtk4",
|
||||
"gtk4-layer-shell-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-layer-shell-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b"
|
||||
dependencies = [
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"system-deps 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-macros"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb906c98f0b4cc9002594e337e83f40492aa6dca22c97242f13be5b046c5f338"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-sys"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd14af0137f269bdecee73404157d2a852f9651af7d68bd1023d3b8b914a14c3"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
@@ -1179,6 +1750,12 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -1203,6 +1780,43 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-core",
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
|
||||
dependencies = [
|
||||
"jiff-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
@@ -1232,9 +1846,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.2.20"
|
||||
@@ -1262,6 +1882,36 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "libadwaita"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa"
|
||||
dependencies = [
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib",
|
||||
"gtk4",
|
||||
"libadwaita-sys",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libadwaita-sys"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd"
|
||||
dependencies = [
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
@@ -1278,6 +1928,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -1604,6 +2264,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.5.3"
|
||||
@@ -1679,6 +2345,29 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2022dbbd82e1c42bd950a6f1df9b98c796e725dce5ec275e03cbf9772e4efa"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"pango-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango-sys"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca02d64761b74d56cdd4b437db6d73dc95e6c0d40f27e1c707952c0f09052948"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps 9.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1860,6 +2549,15 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.1"
|
||||
@@ -1912,6 +2610,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -2133,6 +2840,12 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -2186,6 +2899,15 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -2219,6 +2941,16 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
@@ -2227,7 +2959,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
"signal-hook 0.3.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2358,6 +3090,51 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "7.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0dae2cbca1f0ff93795713cfe0a4c02f760e3c0b8ae2ab4ec4045808ff429f"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "temp-dir"
|
||||
version = "0.1.16"
|
||||
@@ -2395,7 +3172,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook 0.3.18",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@@ -2446,7 +3223,7 @@ dependencies = [
|
||||
"pest_derive",
|
||||
"phf",
|
||||
"sha2",
|
||||
"signal-hook",
|
||||
"signal-hook 0.3.18",
|
||||
"siphasher",
|
||||
"terminfo",
|
||||
"termios",
|
||||
@@ -2549,6 +3326,21 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.8+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
@@ -2579,6 +3371,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
@@ -2693,6 +3491,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -2768,6 +3572,79 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"rustix",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.106"
|
||||
@@ -3006,6 +3883,23 @@ version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
version = "2.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "yeslogic-fontconfig-sys"
|
||||
version = "6.0.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Local ports of the pieces of the Zig font package that the config needs
|
||||
//! (`font/face.zig` `Variation`, `font/CodepointMap.zig`, and
|
||||
//! Canonical definitions of the pieces of the Zig font package that the config
|
||||
//! needs (`font/face.zig` `Variation`, `font/CodepointMap.zig`, and
|
||||
//! `font/discovery.zig` `Descriptor`). `ghostty-font` depends on this crate,
|
||||
//! so these live here and the font crate should re-use them.
|
||||
//! so these live here and the font crate re-exports them.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
|
||||
@@ -81,7 +81,8 @@ impl MetricModifier {
|
||||
#[must_use]
|
||||
pub fn apply_i32(&self, v: i32) -> i32 {
|
||||
let r = self.apply_i64(i64::from(v), false);
|
||||
i32::try_from(r).unwrap_or(if r < 0 { i32::MIN } else { i32::MAX })
|
||||
// Mirrors Zig: out-of-range yields maxInt * sign.
|
||||
i32::try_from(r).unwrap_or(if r < 0 { -i32::MAX } else { i32::MAX })
|
||||
}
|
||||
|
||||
/// Apply to a float value.
|
||||
|
||||
@@ -15,6 +15,8 @@ ghostty-unicode.workspace = true
|
||||
bitflags = "2.13.2"
|
||||
yeslogic-fontconfig-sys = "6.0.1"
|
||||
ghostty-datastruct.workspace = true
|
||||
ghostty-config.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
|
||||
# This crate wraps the freetype / harfbuzz / fontconfig C libraries through
|
||||
# their `-sys` crates; unsafe is denied by default and allowed on minimal,
|
||||
@@ -47,3 +49,6 @@ return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
|
||||
[dev-dependencies]
|
||||
ghostty-vt.workspace = true
|
||||
@@ -3,89 +3,16 @@
|
||||
//! font, the codepoint is rendered using the default font.
|
||||
//! Port of `font/CodepointMap.zig`.
|
||||
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use crate::discovery::Descriptor;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entry {
|
||||
/// Unicode codepoint range (inclusive). Asserts range.0 <= range.1.
|
||||
pub range: (u32, u32),
|
||||
|
||||
/// The discovery descriptor of the font to use for this range.
|
||||
pub descriptor: Descriptor,
|
||||
}
|
||||
|
||||
/// The list of entries.
|
||||
///
|
||||
/// Note: we just do a linear search because we expect to always have very
|
||||
/// few entries, so the overhead of a binary search is not worth it. This is
|
||||
/// possible to defeat with some pathological inputs, but there is no realistic
|
||||
/// scenario where this will be a problem except people trying to fuck around.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CodepointMap {
|
||||
pub list: Vec<Entry>,
|
||||
}
|
||||
|
||||
impl CodepointMap {
|
||||
/// Add an entry to the map.
|
||||
///
|
||||
/// For conflicting codepoints, entries added later take priority over
|
||||
/// entries added earlier.
|
||||
pub fn add(&mut self, entry: Entry) {
|
||||
assert!(entry.range.0 <= entry.range.1);
|
||||
self.list.push(entry);
|
||||
}
|
||||
|
||||
/// Get a descriptor for a codepoint.
|
||||
pub fn get(&self, cp: u32) -> Option<&Descriptor> {
|
||||
self.list
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|e| e.range.0 <= cp && cp <= e.range.1)
|
||||
.map(|e| &e.descriptor)
|
||||
}
|
||||
|
||||
/// Hash with the given hasher.
|
||||
pub fn hash_into<H: Hasher>(&self, hasher: &mut H) {
|
||||
self.list.len().hash(hasher);
|
||||
for entry in &self.list {
|
||||
entry.range.hash(hasher);
|
||||
hash_descriptor(&entry.descriptor, hasher);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a hash code that can be used to uniquely identify this
|
||||
/// action.
|
||||
pub fn hashcode(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.hash_into(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors `Descriptor.hash` of the Zig discovery module (same fields, same
|
||||
/// float handling) without depending on how the discovery module exposes it.
|
||||
fn hash_descriptor<H: Hasher>(d: &Descriptor, hasher: &mut H) {
|
||||
d.family.hash(hasher);
|
||||
d.style.hash(hasher);
|
||||
d.codepoint.hash(hasher);
|
||||
d.size.to_bits().hash(hasher);
|
||||
d.bold.hash(hasher);
|
||||
d.italic.hash(hasher);
|
||||
d.monospace.hash(hasher);
|
||||
d.variations.len().hash(hasher);
|
||||
for v in &d.variations {
|
||||
v.id.hash(hasher);
|
||||
// This is not correct, but we don't currently depend on the
|
||||
// hash value being different based on decimal values of variations.
|
||||
(v.value as i64).hash(hasher);
|
||||
}
|
||||
}
|
||||
//!
|
||||
//! The canonical definition lives in `ghostty-config` (the `font-codepoint-map`
|
||||
//! option parses straight into it); it is re-exported here.
|
||||
|
||||
pub use ghostty_config::font_types::CodepointMap;
|
||||
pub use ghostty_config::font_types::CodepointMapEntry as Entry;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ghostty_config::font_types::Descriptor;
|
||||
|
||||
fn desc(family: &str) -> Descriptor {
|
||||
Descriptor {
|
||||
@@ -96,7 +23,7 @@ mod tests {
|
||||
|
||||
fn entry(a: u32, b: u32, family: &str) -> Entry {
|
||||
Entry {
|
||||
range: (a, b),
|
||||
range: [a, b],
|
||||
descriptor: desc(family),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,5 @@
|
||||
//! Small local mirrors of the `ghostty-config` types that the font engine
|
||||
//! consumes.
|
||||
//!
|
||||
//! TODO(integration): `ghostty-config` should map its own
|
||||
//! `FreetypeLoadFlags`, `FontSyntheticStyle` and `FontShapingBreak` onto
|
||||
//! these (or this crate should re-export the config ones once the config
|
||||
//! crate exists in the dependency graph).
|
||||
//! The config types consumed by the font engine. The canonical definitions
|
||||
//! live in `ghostty-config`; they are re-exported here for convenience.
|
||||
|
||||
/// See `freetype-load-flags` in the config. Defaults match upstream.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FreetypeLoadFlags {
|
||||
pub hinting: bool,
|
||||
pub force_autohint: bool,
|
||||
pub monochrome: bool,
|
||||
pub autohint: bool,
|
||||
pub light: bool,
|
||||
}
|
||||
|
||||
impl Default for FreetypeLoadFlags {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hinting: true,
|
||||
force_autohint: false,
|
||||
monochrome: false,
|
||||
autohint: true,
|
||||
light: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See `font-synthetic-style` in the config.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FontSyntheticStyle {
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub bold_italic: bool,
|
||||
}
|
||||
|
||||
impl Default for FontSyntheticStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bold: true,
|
||||
italic: true,
|
||||
bold_italic: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See `font-shaping-break` in the config.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FontShapingBreak {
|
||||
pub cursor: bool,
|
||||
}
|
||||
|
||||
impl Default for FontShapingBreak {
|
||||
fn default() -> Self {
|
||||
Self { cursor: true }
|
||||
}
|
||||
}
|
||||
pub use ghostty_config::types_a::{FontShapingBreak, FontStyle, FontSyntheticStyle};
|
||||
pub use ghostty_config::types_b::{FreetypeLoadFlags, GraphemeWidthMethod};
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Font discovery. Port of `font/discovery.zig` (fontconfig backend only;
|
||||
//! the CoreText discoverer is macOS-only and out of scope).
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::collection::Collection;
|
||||
@@ -12,111 +11,44 @@ use crate::fontconfig as fc;
|
||||
/// Discover implementation for the compile options.
|
||||
pub type Discover = Fontconfig;
|
||||
|
||||
/// Descriptor is used to search for fonts. The only required field
|
||||
/// is "family". The rest are ignored unless they're set to a non-zero
|
||||
/// value.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Descriptor {
|
||||
/// Font family to search for. This can be a fully qualified font
|
||||
/// name such as "Fira Code", "monospace", "serif", etc.
|
||||
///
|
||||
/// On systems that use fontconfig (Linux), this can be a full
|
||||
/// fontconfig pattern, such as "Fira Code-14:bold".
|
||||
pub family: Option<String>,
|
||||
/// Descriptor is used to search for fonts (canonical definition in `ghostty-config`).
|
||||
pub use ghostty_config::font_types::Descriptor;
|
||||
|
||||
/// Specific font style to search for. This will filter the style
|
||||
/// string the font advertises. The "bold/italic" booleans later in this
|
||||
/// struct filter by the style trait the font has, not the string, so
|
||||
/// these can be used in conjunction or not.
|
||||
pub style: Option<String>,
|
||||
|
||||
/// A codepoint that this font must be able to render.
|
||||
pub codepoint: u32,
|
||||
|
||||
/// Font size in points that the font should support. For conversion
|
||||
/// to pixels, we will use 72 DPI for Mac and 96 DPI for everything else.
|
||||
/// (If pixel conversion is necessary, i.e. emoji fonts)
|
||||
pub size: f32,
|
||||
|
||||
/// True if we want to search specifically for a font that supports
|
||||
/// specific styles.
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub monospace: bool,
|
||||
|
||||
/// Variation axes to apply to the font. This also impacts searching
|
||||
/// for fonts since fonts with the ability to set these variations
|
||||
/// will be preferred, but not guaranteed.
|
||||
pub variations: Vec<Variation>,
|
||||
}
|
||||
|
||||
impl Hash for Descriptor {
|
||||
/// Hash the descriptor with the given hasher. Covers the same fields
|
||||
/// as the Zig implementation.
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.family.hash(state);
|
||||
self.style.hash(state);
|
||||
self.codepoint.hash(state);
|
||||
self.size.to_bits().hash(state);
|
||||
self.bold.hash(state);
|
||||
self.italic.hash(state);
|
||||
self.monospace.hash(state);
|
||||
self.variations.len().hash(state);
|
||||
for variation in &self.variations {
|
||||
variation.id.hash(state);
|
||||
|
||||
// This is not correct, but we don't currently depend on the
|
||||
// hash value being different based on decimal values of variations.
|
||||
(variation.value as i64).hash(state);
|
||||
}
|
||||
/// Convert to Fontconfig pattern to use for lookup. The pattern does
|
||||
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
||||
/// must still do this. (`Descriptor.toFcPattern`)
|
||||
pub fn to_fc_pattern(desc: &Descriptor) -> fc::Pattern {
|
||||
let mut pat = fc::Pattern::create();
|
||||
if let Some(family) = &desc.family {
|
||||
assert!(pat.add_string(fc::Property::Family, family));
|
||||
}
|
||||
}
|
||||
|
||||
impl Descriptor {
|
||||
/// Returns a hash code that can be used to uniquely identify this
|
||||
/// action.
|
||||
pub fn hashcode(&self) -> u64 {
|
||||
// `DefaultHasher::new()` uses fixed keys, so this is deterministic.
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
if let Some(style) = &desc.style {
|
||||
assert!(pat.add_string(fc::Property::Style, style));
|
||||
}
|
||||
if desc.codepoint > 0 {
|
||||
let mut cs = fc::CharSet::create();
|
||||
assert!(cs.add_char(desc.codepoint));
|
||||
assert!(pat.add_charset(fc::Property::Charset, &cs));
|
||||
}
|
||||
if desc.size > 0.0 {
|
||||
assert!(pat.add_integer(fc::Property::Size, desc.size.round() as i32));
|
||||
}
|
||||
if desc.bold {
|
||||
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
|
||||
}
|
||||
if desc.italic {
|
||||
assert!(pat.add_integer(fc::Property::Slant, fc::Slant::Italic as i32));
|
||||
}
|
||||
|
||||
/// Convert to Fontconfig pattern to use for lookup. The pattern does
|
||||
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
||||
/// must still do this.
|
||||
pub fn to_fc_pattern(&self) -> fc::Pattern {
|
||||
let mut pat = fc::Pattern::create();
|
||||
if let Some(family) = &self.family {
|
||||
assert!(pat.add_string(fc::Property::Family, family));
|
||||
}
|
||||
if let Some(style) = &self.style {
|
||||
assert!(pat.add_string(fc::Property::Style, style));
|
||||
}
|
||||
if self.codepoint > 0 {
|
||||
let mut cs = fc::CharSet::create();
|
||||
assert!(cs.add_char(self.codepoint));
|
||||
assert!(pat.add_charset(fc::Property::Charset, &cs));
|
||||
}
|
||||
if self.size > 0.0 {
|
||||
assert!(pat.add_integer(fc::Property::Size, self.size.round() as i32));
|
||||
}
|
||||
if self.bold {
|
||||
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
|
||||
}
|
||||
if self.italic {
|
||||
assert!(pat.add_integer(fc::Property::Slant, fc::Slant::Italic as i32));
|
||||
}
|
||||
// For fontconfig, we always add monospace in the pattern. Since
|
||||
// fontconfig sorts by closeness to the pattern, this doesn't fully
|
||||
// exclude non-monospace but helps prefer it.
|
||||
assert!(pat.add_integer(fc::Property::Spacing, fc::Spacing::Mono as i32));
|
||||
|
||||
// For fontconfig, we always add monospace in the pattern. Since
|
||||
// fontconfig sorts by closeness to the pattern, this doesn't fully
|
||||
// exclude non-monospace but helps prefer it.
|
||||
assert!(pat.add_integer(fc::Property::Spacing, fc::Spacing::Mono as i32));
|
||||
|
||||
pat
|
||||
}
|
||||
pat
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Fontconfig {
|
||||
fc_config: Arc<fc::Config>,
|
||||
}
|
||||
@@ -145,7 +77,7 @@ impl Fontconfig {
|
||||
/// be used to build up the deferred fonts.
|
||||
pub fn discover(&self, desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
|
||||
// Build our pattern that we'll search for
|
||||
let mut pat = desc.to_fc_pattern();
|
||||
let mut pat = to_fc_pattern(desc);
|
||||
assert!(self.fc_config.substitute_with_pat_pattern(&mut pat));
|
||||
pat.default_substitute();
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use crate::metrics::Metrics;
|
||||
|
||||
pub mod freetype;
|
||||
|
||||
pub use crate::config_types::FreetypeLoadFlags;
|
||||
pub use freetype::Face;
|
||||
pub use ghostty_config::types_b::FreetypeLoadFlags;
|
||||
|
||||
/// If a DPI can't be calculated, this DPI is used. This is probably
|
||||
/// wrong on modern devices so it is highly recommended you get the DPI
|
||||
@@ -118,51 +118,8 @@ impl DesiredSize {
|
||||
}
|
||||
}
|
||||
|
||||
/// A font variation setting. The best documentation for this I know of
|
||||
/// is actually the CSS font-variation-settings property on MDN:
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Variation {
|
||||
pub id: VariationId,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
/// A four byte OpenType variation axis tag (`wght`, `slnt`, ...). The
|
||||
/// numeric value is the big-endian interpretation of the 4 characters
|
||||
/// (Zig: `packed struct(u32) { d, c, b, a }`, i.e. `a` is the high byte).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct VariationId {
|
||||
pub a: u8,
|
||||
pub b: u8,
|
||||
pub c: u8,
|
||||
pub d: u8,
|
||||
}
|
||||
|
||||
impl VariationId {
|
||||
pub fn new(v: &[u8; 4]) -> VariationId {
|
||||
VariationId {
|
||||
a: v[0],
|
||||
b: v[1],
|
||||
c: v[2],
|
||||
d: v[3],
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw 32-bit tag value.
|
||||
pub fn as_u32(self) -> u32 {
|
||||
u32::from_be_bytes([self.a, self.b, self.c, self.d])
|
||||
}
|
||||
|
||||
pub fn from_u32(v: u32) -> VariationId {
|
||||
let [a, b, c, d] = v.to_be_bytes();
|
||||
VariationId { a, b, c, d }
|
||||
}
|
||||
|
||||
/// Converts the ID to a string.
|
||||
pub fn str(self) -> [u8; 4] {
|
||||
[self.a, self.b, self.c, self.d]
|
||||
}
|
||||
}
|
||||
/// Font variation types: canonical definitions live in `ghostty-config`.
|
||||
pub use ghostty_config::font_types::{Variation, VariationId};
|
||||
|
||||
/// The size and position of a glyph.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
@@ -622,26 +579,6 @@ impl Constraint {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variation_id_wght_should_be_2003265652() {
|
||||
let id = VariationId::new(b"wght");
|
||||
assert_eq!(2003265652, id.as_u32());
|
||||
assert_eq!(b"wght", &id.str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variation_id_slnt_should_be_1936486004() {
|
||||
let id = VariationId {
|
||||
a: b's',
|
||||
b: b'l',
|
||||
c: b'n',
|
||||
d: b't',
|
||||
};
|
||||
assert_eq!(1936486004, id.as_u32());
|
||||
assert_eq!(b"slnt", &id.str());
|
||||
assert_eq!(VariationId::from_u32(1936486004), id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desired_size_pixels() {
|
||||
let s = DesiredSize {
|
||||
|
||||
@@ -188,7 +188,7 @@ impl Face {
|
||||
log::debug!("variation axes font={}", result.name());
|
||||
if let Ok(axes) = result.face.mm_axes(&result.lib.inner.lib) {
|
||||
for axis in axes {
|
||||
let id = super::VariationId::from_u32(axis.tag);
|
||||
let id = super::VariationId(axis.tag);
|
||||
log::debug!(
|
||||
"variation axis: id={} min={} max={} def={}",
|
||||
String::from_utf8_lossy(&id.str()),
|
||||
@@ -321,7 +321,7 @@ impl Face {
|
||||
// variations, either.
|
||||
for (i, axis) in axes.iter().take(n).enumerate() {
|
||||
for v in vs {
|
||||
if axis.tag == v.id.as_u32() {
|
||||
if axis.tag == v.id.0 {
|
||||
coords[i] = (v.value * 65536.0) as i64;
|
||||
break;
|
||||
}
|
||||
@@ -1365,7 +1365,7 @@ mod tests {
|
||||
let o = opts(12.0, 96);
|
||||
face.set_variations(
|
||||
&[Variation {
|
||||
id: super::super::VariationId::new(b"wght"),
|
||||
id: super::super::VariationId::init(b"wght"),
|
||||
value: 800.0,
|
||||
}],
|
||||
o,
|
||||
@@ -1375,7 +1375,7 @@ mod tests {
|
||||
let mut face = init(&lib, embedded::INCONSOLATA, 12.0, 96);
|
||||
face.set_variations(
|
||||
&[Variation {
|
||||
id: super::super::VariationId::new(b"wght"),
|
||||
id: super::super::VariationId::init(b"wght"),
|
||||
value: 800.0,
|
||||
}],
|
||||
o,
|
||||
|
||||
@@ -24,6 +24,7 @@ pub mod opentype;
|
||||
pub mod shape;
|
||||
pub mod shaper;
|
||||
pub mod shared_grid;
|
||||
pub mod shared_grid_set;
|
||||
pub mod sprite;
|
||||
|
||||
pub use atlas::Atlas;
|
||||
@@ -40,6 +41,7 @@ pub use library::Library;
|
||||
pub use metrics::Metrics;
|
||||
|
||||
pub use shared_grid::SharedGrid;
|
||||
pub use shared_grid_set::SharedGridSet;
|
||||
pub use sprite::{Face as SpriteFace, Sprite};
|
||||
|
||||
/// The styles that a family can take.
|
||||
|
||||
@@ -537,127 +537,9 @@ fn add_float_to_int(int: &mut u32, float: f64) {
|
||||
/// little space as possible.
|
||||
pub type ModifierSet = HashMap<Key, Modifier>;
|
||||
|
||||
/// Error returned by [`Modifier::parse`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ModifierParseError {
|
||||
#[error("invalid format")]
|
||||
InvalidFormat,
|
||||
#[error("value required")]
|
||||
ValueRequired,
|
||||
}
|
||||
|
||||
/// A modifier to apply to a metrics value. The modifier value represents
|
||||
/// a delta, so percent is a percentage to change, not a percentage of.
|
||||
/// For example, "20%" is 20% larger, not 20% of the value. Likewise,
|
||||
/// an absolute value of "20" is 20 larger, not literally 20.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Percent(f64),
|
||||
Absolute(i32),
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
/// Parses the modifier value. If the value ends in "%" it is assumed
|
||||
/// to be a percent, otherwise the value is parsed as an integer.
|
||||
pub fn parse(input: &str) -> Result<Modifier, ModifierParseError> {
|
||||
if input.is_empty() {
|
||||
return Err(ModifierParseError::InvalidFormat);
|
||||
}
|
||||
|
||||
if let Some(num) = input.strip_suffix('%') {
|
||||
let mut percent = parse_float(num).ok_or(ModifierParseError::InvalidFormat)?;
|
||||
percent /= 100.0;
|
||||
|
||||
if percent <= -1.0 {
|
||||
return Ok(Modifier::Percent(0.0));
|
||||
}
|
||||
if percent < 0.0 {
|
||||
return Ok(Modifier::Percent(1.0 + percent));
|
||||
}
|
||||
return Ok(Modifier::Percent(1.0 + percent));
|
||||
}
|
||||
|
||||
parse_int_i32(input)
|
||||
.map(Modifier::Absolute)
|
||||
.ok_or(ModifierParseError::InvalidFormat)
|
||||
}
|
||||
|
||||
/// So it works with the config framework.
|
||||
pub fn parse_cli(input: Option<&str>) -> Result<Modifier, ModifierParseError> {
|
||||
Modifier::parse(input.ok_or(ModifierParseError::ValueRequired)?)
|
||||
}
|
||||
|
||||
/// Used by config formatter. Returns the value text of the config entry.
|
||||
pub fn format_entry(&self) -> String {
|
||||
match self {
|
||||
Modifier::Percent(v) => format!("{}%", (v - 1.0) * 100.0),
|
||||
Modifier::Absolute(v) => format!("{v}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to an unsigned 32-bit value.
|
||||
pub fn apply_u32(&self, v: u32) -> u32 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => {
|
||||
let p_clamped = p.max(0.0);
|
||||
(f64::from(v) * p_clamped).round() as u32
|
||||
}
|
||||
Modifier::Absolute(abs) => {
|
||||
let applied = i64::from(v).saturating_add(i64::from(abs));
|
||||
let clamped = applied.max(0);
|
||||
u32::try_from(clamped).unwrap_or(u32::MAX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to a signed 32-bit value.
|
||||
pub fn apply_i32(&self, v: i32) -> i32 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => {
|
||||
let p_clamped = p.max(0.0);
|
||||
(f64::from(v) * p_clamped).round() as i32
|
||||
}
|
||||
Modifier::Absolute(abs) => {
|
||||
let applied = i64::from(v).saturating_add(i64::from(abs));
|
||||
// Mirrors Zig: out-of-range yields maxInt * sign.
|
||||
i32::try_from(applied).unwrap_or(if applied > 0 { i32::MAX } else { -i32::MAX })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to a float value.
|
||||
pub fn apply_f64(&self, v: f64) -> f64 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => v * p.max(0.0),
|
||||
Modifier::Absolute(abs) => v + f64::from(abs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Modifier {
|
||||
type Err = ModifierParseError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Modifier::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig `std.fmt.parseInt(i32, s, 10)`: optional sign, digits only
|
||||
/// (underscores are only accepted with base 0, so not here).
|
||||
fn parse_int_i32(s: &str) -> Option<i32> {
|
||||
let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
|
||||
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
s.parse::<i32>().ok()
|
||||
}
|
||||
|
||||
/// Approximation of Zig `std.fmt.parseFloat(f64, s)` for decimal input.
|
||||
fn parse_float(s: &str) -> Option<f64> {
|
||||
if s.is_empty() || s.contains(char::is_whitespace) {
|
||||
return None;
|
||||
}
|
||||
s.parse::<f64>().ok()
|
||||
}
|
||||
/// A modifier to apply to a metrics value (canonical definition in
|
||||
/// `ghostty-config`, which parses and formats the `adjust-*` options).
|
||||
pub use ghostty_config::metric_modifier::MetricModifier as Modifier;
|
||||
|
||||
/// Key is an enum of all the available metrics keys (every u32/i32/f64
|
||||
/// field of [`Metrics`]).
|
||||
@@ -683,6 +565,27 @@ pub enum Key {
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// Every key, in declaration order.
|
||||
pub const ALL: [Key; 17] = [
|
||||
Key::CellWidth,
|
||||
Key::CellHeight,
|
||||
Key::CellBaseline,
|
||||
Key::UnderlinePosition,
|
||||
Key::UnderlineThickness,
|
||||
Key::StrikethroughPosition,
|
||||
Key::StrikethroughThickness,
|
||||
Key::OverlinePosition,
|
||||
Key::OverlineThickness,
|
||||
Key::BoxThickness,
|
||||
Key::CursorThickness,
|
||||
Key::CursorHeight,
|
||||
Key::IconHeight,
|
||||
Key::IconHeightSingle,
|
||||
Key::FaceWidth,
|
||||
Key::FaceHeight,
|
||||
Key::FaceY,
|
||||
];
|
||||
|
||||
/// The snake_case field name, matching the Zig `@tagName`.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -737,20 +640,6 @@ impl FromStr for Key {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formatconfig_percent() {
|
||||
let p = Modifier::parse_cli(Some("24%")).unwrap();
|
||||
// Zig test checks the config-formatted line "a = 24%\n"; the config
|
||||
// entry formatter lives in ghostty-config, so check the value text.
|
||||
assert_eq!(p.format_entry(), "24%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatconfig_absolute() {
|
||||
let p = Modifier::parse_cli(Some("-30")).unwrap();
|
||||
assert_eq!(p.format_entry(), "-30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_apply_modifiers() {
|
||||
let mut set = ModifierSet::new();
|
||||
@@ -873,17 +762,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn modifier_parse_errors() {
|
||||
assert_eq!(Err(ModifierParseError::InvalidFormat), Modifier::parse(""));
|
||||
assert_eq!(
|
||||
Err(ModifierParseError::InvalidFormat),
|
||||
Modifier::parse("abc")
|
||||
);
|
||||
assert_eq!(Err(ModifierParseError::InvalidFormat), Modifier::parse("%"));
|
||||
assert!(Modifier::parse("").is_err());
|
||||
assert!(Modifier::parse("abc").is_err());
|
||||
assert!(Modifier::parse("%").is_err());
|
||||
assert_eq!(Ok(Modifier::Percent(0.0)), Modifier::parse("-150%"));
|
||||
assert_eq!(
|
||||
Err(ModifierParseError::ValueRequired),
|
||||
Modifier::parse_cli(None)
|
||||
);
|
||||
assert!(Modifier::parse_cli_value(None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
//! The shaper implementation for the Linux backend is HarfBuzz
|
||||
//! ([`crate::shaper::harfbuzz::Shaper`]).
|
||||
|
||||
use crate::collection::Index;
|
||||
|
||||
pub use crate::shaper::cache::Cache;
|
||||
pub use crate::shaper::feature::{DEFAULT_FEATURES, Feature, FeatureList};
|
||||
pub use crate::shaper::run::{RunIterator, RunIteratorHook, RunOptions, TextRun};
|
||||
pub use crate::shaper::{harfbuzz, noop};
|
||||
|
||||
/// Shaper implementation for our compile options (backend
|
||||
@@ -46,26 +45,3 @@ pub struct Options {
|
||||
/// support applying features globally.
|
||||
pub features: Vec<String>,
|
||||
}
|
||||
|
||||
/// A single text run. Port of `shaper/run.zig` `TextRun`.
|
||||
///
|
||||
/// TODO(integration): `run.zig` (RunIterator over terminal rows) is deferred
|
||||
/// to F2; the `grid: *SharedGrid` field is omitted here and should be added
|
||||
/// when `SharedGrid` and the run iterator are wired in.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TextRun {
|
||||
/// A unique hash for this run. This can be used to cache the shaping
|
||||
/// results.
|
||||
pub hash: u64,
|
||||
|
||||
/// The offset in the row where this run started. This is added to the
|
||||
/// X position of the final shaped cells to get the absolute position
|
||||
/// in the row where they belong.
|
||||
pub offset: u16,
|
||||
|
||||
/// The total number of cells produced by this run.
|
||||
pub cells: u16,
|
||||
|
||||
/// The font index to use for the glyphs of this run.
|
||||
pub font_index: Index,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! HarfBuzz shaper. Port of `font/shaper/harfbuzz.zig`.
|
||||
//!
|
||||
//! The Zig shaper is driven by `RunIterator` (shaper/run.zig, deferred to F2)
|
||||
//! The Zig shaper is driven by `RunIterator` (shaper/run.rs)
|
||||
//! through the hooks `prepare` / `addCodepoint` / `finalize`; those hooks are
|
||||
//! exposed here as [`Shaper::prepare`], [`Shaper::add_codepoint`] and
|
||||
//! [`Shaper::finalize`]. [`Shaper::shape`] takes the face of the run's font
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
use super::hb;
|
||||
use crate::face::freetype::Face;
|
||||
use crate::shape::{self, Cell, DEFAULT_FEATURES, FeatureList};
|
||||
use crate::shape::{self, Cell, DEFAULT_FEATURES, FeatureList, TextRun};
|
||||
use crate::shared_grid::SharedGrid;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeError {
|
||||
@@ -22,6 +23,15 @@ pub enum ShapeError {
|
||||
HarfbuzzFailed,
|
||||
}
|
||||
|
||||
/// Errors from [`Shaper::shape_run`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeRunError {
|
||||
#[error(transparent)]
|
||||
GetFace(#[from] crate::collection::GetFaceError),
|
||||
#[error(transparent)]
|
||||
Shape(#[from] ShapeError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Codepoint {
|
||||
cluster: u32,
|
||||
@@ -137,6 +147,28 @@ impl Shaper {
|
||||
self.shape(face)
|
||||
}
|
||||
|
||||
/// Shape the given text run (`shape(run)` in Zig). The run must be the
|
||||
/// immediately previous run that was iterated since it shares state with
|
||||
/// the shaper. The face is looked up in `grid` under its exclusive lock
|
||||
/// (FreeType faces are not thread safe).
|
||||
///
|
||||
/// The return value is only valid until the next shape call is called.
|
||||
pub fn shape_run(
|
||||
&mut self,
|
||||
grid: &SharedGrid,
|
||||
run: &TextRun,
|
||||
) -> Result<&[Cell], ShapeRunError> {
|
||||
// We only do shaping if the font is not a special-case. For
|
||||
// special-case fonts, the codepoint == glyph_index so we don't need
|
||||
// to run any shaping.
|
||||
if run.font_index.special().is_none() {
|
||||
let mut guard = grid.write();
|
||||
let face = guard.resolver.collection.get_face(run.font_index)?;
|
||||
return Ok(self.shape(Some(face))?);
|
||||
}
|
||||
Ok(self.shape(None)?)
|
||||
}
|
||||
|
||||
/// Shape the current run (as built through the hooks). `face` is the face
|
||||
/// for the run's font index, or `None` if the font is a special-case
|
||||
/// font (sprite), for which codepoint == glyph_index.
|
||||
@@ -266,6 +298,11 @@ impl Shaper {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::face::{DesiredSize, FontData, Options};
|
||||
use crate::shaper::run::{RunIterator, RunOptions};
|
||||
use crate::shaper::test_util::{
|
||||
Term, TestFont, shape_all, test_shaper, test_shaper_with_discovered_font,
|
||||
test_shaper_with_font,
|
||||
};
|
||||
use crate::{Library, embedded};
|
||||
|
||||
fn text(s: &str) -> Vec<(u32, u32)> {
|
||||
@@ -299,32 +336,6 @@ mod tests {
|
||||
s.shape(Some(&f)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_inconsolata_ligs() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
// Some of our tests rely on dlig being enabled by default.
|
||||
let mut s = Shaper::new(&shape::Options {
|
||||
features: vec!["dlig".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
let cells = s.shape_text(Some(&f), &text(">=")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_monaspace_ligs() {
|
||||
let f = face(embedded::MONASPACE_NEON);
|
||||
// Some of our tests rely on dlig being enabled by default.
|
||||
let mut s = Shaper::new(&shape::Options {
|
||||
features: vec!["dlig".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_ligatures_disabled_by_feature() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
@@ -336,37 +347,6 @@ mod tests {
|
||||
assert_eq!(3, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_arabic_forced_ltr() {
|
||||
let f = face(embedded::ARABIC);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
let arabic = include_str!("../../testdata/arabic.txt");
|
||||
let line = arabic.lines().next().unwrap();
|
||||
let t = text(line);
|
||||
let n = t.len();
|
||||
let cells = s.shape_text(Some(&f), &t).unwrap();
|
||||
assert_eq!(n, cells.len());
|
||||
let mut x = cells[0].x;
|
||||
for cell in &cells[1..] {
|
||||
assert_eq!(x + 1, cell.x);
|
||||
x = cell.x;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_combining_characters() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
// n + two combining diaeresis in cell 0, then 'a' in cell 1.
|
||||
let t = [('n' as u32, 0), (0x0308, 0), (0x0308, 0), ('a' as u32, 1)];
|
||||
let cells = s.shape_text(Some(&f), &t).unwrap();
|
||||
assert_eq!(4, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x);
|
||||
assert_eq!(1, cells[3].x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_special_font_passthrough() {
|
||||
// Special fonts (sprite) are not shaped: glyph index == codepoint.
|
||||
@@ -397,4 +377,608 @@ mod tests {
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(3, cells[1].x);
|
||||
}
|
||||
|
||||
/// Run all runs over `row` of `t` with the default options and return
|
||||
/// the shaped runs.
|
||||
fn run_row(
|
||||
ts: &mut crate::shaper::test_util::TestShaper,
|
||||
t: &Term,
|
||||
y: u32,
|
||||
) -> Vec<crate::shaper::test_util::Shaped> {
|
||||
let cells = t.row(y);
|
||||
shape_all(&mut ts.shaper, RunOptions::new(&ts.grid, &cells))
|
||||
}
|
||||
|
||||
fn count_runs(
|
||||
ts: &mut crate::shaper::test_util::TestShaper,
|
||||
cells: &[crate::shaper::run::RunCell],
|
||||
selection: Option<[u16; 2]>,
|
||||
cursor_x: Option<usize>,
|
||||
) -> usize {
|
||||
let mut opts = RunOptions::new(&ts.grid, cells);
|
||||
opts.selection = selection;
|
||||
opts.cursor_x = cursor_x;
|
||||
shape_all(&mut ts.shaper, opts).len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_iterator() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
{
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print("ABCD");
|
||||
let cells = t.row(0);
|
||||
let mut it = RunIterator::new(RunOptions::new(&ts.grid, &cells));
|
||||
let mut count = 0;
|
||||
while it.next(&mut ts.shaper).unwrap().is_some() {
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(1, count);
|
||||
}
|
||||
|
||||
// Spaces should be part of a run
|
||||
{
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("ABCD EFG");
|
||||
let cells = t.row(0);
|
||||
let mut it = RunIterator::new(RunOptions::new(&ts.grid, &cells));
|
||||
let mut count = 0;
|
||||
while it.next(&mut ts.shaper).unwrap().is_some() {
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(1, count);
|
||||
}
|
||||
|
||||
{
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print("A\u{1F603}D");
|
||||
let cells = t.row(0);
|
||||
let mut it = RunIterator::new(RunOptions::new(&ts.grid, &cells));
|
||||
let mut count = 0;
|
||||
while it.next(&mut ts.shaper).unwrap().is_some() {
|
||||
count += 1;
|
||||
|
||||
// All runs should be exactly length 1
|
||||
assert_eq!(1, ts.shaper.hb_buf_len());
|
||||
}
|
||||
assert_eq!(3, count);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_iterator_empty_cells_with_background_set() {
|
||||
use ghostty_terminal::page::{Cell, ContentTag};
|
||||
use ghostty_vt::color::Rgb;
|
||||
|
||||
let mut ts = test_shaper();
|
||||
let mut t = Term::new(5, 3);
|
||||
// Set red background and write A
|
||||
t.bg_rgb(255, 0, 0);
|
||||
t.print("A");
|
||||
|
||||
// Get our first row
|
||||
for x in [1, 2] {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_content_tag(ContentTag::BgColorRgb);
|
||||
cell.set_color_rgb(Rgb {
|
||||
r: 0xFF,
|
||||
g: 0,
|
||||
b: 0,
|
||||
});
|
||||
*t.cell_mut(x, 0) = cell;
|
||||
}
|
||||
|
||||
let cells = t.row(0);
|
||||
let mut it = RunIterator::new(RunOptions::new(&ts.grid, &cells));
|
||||
{
|
||||
let run = it.next(&mut ts.shaper).unwrap().unwrap();
|
||||
assert_eq!(3, ts.shaper.hb_buf_len());
|
||||
let shaped = ts.shaper.shape_run(&ts.grid, &run).unwrap();
|
||||
assert_eq!(3, shaped.len());
|
||||
}
|
||||
assert!(it.next(&mut ts.shaper).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape() {
|
||||
let mut ts = test_shaper();
|
||||
// Thumbs up plain, thumbs up plain, medium skin tone
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("\u{1F44D}\u{1F44D}\u{1F3FD}");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(3, runs[0].hb_len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_inconsolata_ligs() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
{
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print(">=");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(2, runs[0].run.cells);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
{
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print("===");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(3, runs[0].run.cells);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_monaspace_ligs() {
|
||||
let mut ts = test_shaper_with_font(TestFont::MonaspaceNeon);
|
||||
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print("===");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(3, runs[0].run.cells);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
// Ghostty doesn't currently support RTL and our renderers assume
|
||||
// that cells are in strict LTR order. This means that we need to
|
||||
// force RTL text to be LTR for rendering. This test ensures that
|
||||
// we are correctly forcing RTL text to be LTR.
|
||||
#[test]
|
||||
fn shape_arabic_forced_ltr() {
|
||||
let mut ts = test_shaper_with_font(TestFont::Arabic);
|
||||
|
||||
let mut t = Term::new(120, 30);
|
||||
t.print(include_str!("../../testdata/arabic.txt"));
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(25, runs[0].run.cells);
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(25, cells.len());
|
||||
let mut x = cells[0].x;
|
||||
for cell in &cells[1..] {
|
||||
assert_eq!(x + 1, cell.x);
|
||||
x = cell.x;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_emoji_width() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(5, 3);
|
||||
t.print("\u{1F44D}");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(2, runs[0].run.cells);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_emoji_width_long() {
|
||||
use ghostty_terminal::page::{Cell, ContentTag};
|
||||
|
||||
let mut ts = test_shaper();
|
||||
|
||||
// Make a screen and add a long emoji sequence to it.
|
||||
let mut t = Term::new(30, 3);
|
||||
{
|
||||
let pin = t
|
||||
.screen
|
||||
.pages
|
||||
.pin(ghostty_terminal::point::Point::Active(
|
||||
ghostty_terminal::point::Coordinate { x: 0, y: 1 },
|
||||
))
|
||||
.unwrap();
|
||||
let page = t.screen.page_mut(pin.node);
|
||||
let y = usize::from(pin.y);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_content_tag(ContentTag::Codepoint);
|
||||
cell.set_codepoint(0x1F9D4); // Person with beard
|
||||
*page.get_cells_mut(y).unwrap().first_mut().unwrap() = cell;
|
||||
let graphemes = [
|
||||
0x1F3FB, // Light skin tone (Fitz 1-2)
|
||||
0x200D, // ZWJ
|
||||
0x2642, // Male sign
|
||||
0xFE0F, // Emoji presentation selector
|
||||
];
|
||||
page.set_graphemes(0, y, &graphemes).unwrap();
|
||||
}
|
||||
|
||||
let runs = run_row(&mut ts, &t, 1);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(4, runs[0].hb_len);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_variation_selector_vs15() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
// Victory sign (default text) + VS15 to force text
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("\u{270C}\u{FE0E}");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(1, runs[0].hb_len);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_variation_selector_vs16() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
// Victory sign (default text) + VS16 to force color
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("\u{270C}\u{FE0F}");
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(1, runs[0].hb_len);
|
||||
assert_eq!(1, runs[0].cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_with_empty_cells_in_between() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
t.print("A");
|
||||
t.cursor_right(5);
|
||||
t.print("B");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(7, runs[0].cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_combining_characters() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
t.print("n\u{0308}\u{0308}a");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(4, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x);
|
||||
assert_eq!(1, cells[3].x);
|
||||
}
|
||||
|
||||
// This test exists because the string it uses causes HarfBuzz to output a
|
||||
// non-monotonic run with our cluster level set to `characters`, which we need
|
||||
// to handle by tracking the max cluster for the run.
|
||||
#[test]
|
||||
fn shape_devanagari_string() {
|
||||
// We need a font that supports devanagari for this to work, if we can't
|
||||
// find Arial Unicode MS, which is a system font on macOS, we just skip
|
||||
// the test.
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Arial Unicode MS") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Disable grapheme clustering
|
||||
let mut t = Term::new(30, 3);
|
||||
t.print("अपार्टमेंट");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(8, cells.len());
|
||||
let xs: Vec<u16> = cells.iter().map(|c| c.x).collect();
|
||||
assert_eq!(vec![0, 1, 2, 4, 4, 5, 5, 6], xs);
|
||||
}
|
||||
|
||||
// This test fails on Linux if you have the "Noto Sans Tai Tham" font installed
|
||||
// locally. Disabled upstream (`return error.SkipZigTest`).
|
||||
#[test]
|
||||
#[ignore = "skipped upstream: fails on Linux with Noto Sans Tai Tham installed"]
|
||||
fn shape_tai_tham_vowels_position_differs_from_advance() {
|
||||
// Note that while this test was necessary for CoreText, the old logic was
|
||||
// working for HarfBuzz. Still we keep it to ensure it has the correct
|
||||
// behavior.
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Noto Sans Tai Tham") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
t.print_clustered("\u{1a2F}\u{1a70}"); // ᨯ ᩰ
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
|
||||
// The first glyph renders in the next cell. We expect the x_offset
|
||||
// to equal the cell width. However, with FreeType the cell_width is
|
||||
// computed from ASCII glyphs, and Noto Sans Tai Tham only has the
|
||||
// space character in ASCII (with a 3px advance), so the cell_width
|
||||
// metric doesn't match the actual Tai Tham glyph positioning.
|
||||
assert_eq!(7, cells[0].x_offset);
|
||||
assert_eq!(0, cells[1].x_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_tibetan_characters() {
|
||||
// We need a font that has multiple glyphs for this codepoint to reproduce
|
||||
// the old broken behavior, and Noto Serif Tibetan is one of them. If we
|
||||
// can't find it we just skip the test.
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Noto Serif Tibetan") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Enable grapheme clustering
|
||||
let mut t = Term::new(30, 3);
|
||||
t.print_clustered("\u{0f00}"); // ༀ
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
|
||||
// The second glyph renders at the correct location
|
||||
assert!(cells[1].x_offset < 2);
|
||||
}
|
||||
|
||||
// Disabled upstream (`return error.SkipZigTest`).
|
||||
#[test]
|
||||
#[ignore = "skipped upstream: fails on Linux with Noto Sans Tai Tham installed"]
|
||||
fn shape_tai_tham_letters_run_offset_y_differs_from_zero() {
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Noto Sans Tai Tham") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
// First grapheme cluster: HA, SAKOT. Second grapheme cluster,
|
||||
// combining with the first in a ligature: YA, U.
|
||||
t.print_clustered("\u{1a49}\u{1a60}\u{1a3f}\u{1a69}");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(3, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x); // U from second grapheme
|
||||
|
||||
// The U glyph renders at a y below zero
|
||||
assert_eq!(-3, cells[2].y_offset);
|
||||
}
|
||||
|
||||
// Disabled upstream (`return error.SkipZigTest`).
|
||||
#[test]
|
||||
#[ignore = "skipped upstream (error.SkipZigTest)"]
|
||||
fn shape_javanese_ligatures() {
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Noto Sans Javanese") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
// NA, PANGKON; then HA, vowel sign SUKU combining in a ligature.
|
||||
t.print_clustered("\u{a9a4}\u{a9c0}\u{a9b2}\u{a9b8}");
|
||||
|
||||
let cell_width = ts.grid.metrics().cell_width;
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(3, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x);
|
||||
|
||||
// The vowel sign SUKU renders with correct x_offset
|
||||
assert!(i64::from(cells[2].x_offset) > 3 * i64::from(cell_width));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_chakma_vowel_sign_with_ligature_vowel_sign_renders_first() {
|
||||
// We need a font that supports Chakma for this to work, if we can't find
|
||||
// Noto Sans Chakma Regular we just skip the test.
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Noto Sans Chakma") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
// BAA; TAA + Virama; YYAA + Vowel Sign U (ligature with the second).
|
||||
t.print_clustered("\u{1111d}\u{11116}\u{11133}\u{11120}\u{1112c}");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(4, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
// See the giant "We need to reset the `cell_offset`" comment, but here
|
||||
// we should technically have the rest of these be `x` of 1, but that
|
||||
// would require going back in the stream to adjust past cells, and
|
||||
// we don't take on that complexity.
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x);
|
||||
assert_eq!(0, cells[3].x);
|
||||
|
||||
// The vowel sign U renders before the TAA:
|
||||
assert!(cells[1].x_offset < cells[2].x_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_bengali_ligatures_with_out_of_order_vowels() {
|
||||
// Whereas this test in CoreText had everything shaping into one giant
|
||||
// ligature, HarfBuzz splits it into a few clusters. It still looks okay
|
||||
// (see #10332).
|
||||
let Some(mut ts) = test_shaper_with_discovered_font("Arial Unicode MS") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut t = Term::new(30, 3);
|
||||
// RA + AA; SSA + Virama; TTA + Virama; RA + Vowel sign E.
|
||||
t.print_clustered("\u{09b0}\u{09be}\u{09b7}\u{09cd}\u{099f}\u{09cd}\u{09b0}\u{09c7}");
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(8, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
// Whereas CoreText puts everything all into the first cell (see the
|
||||
// corresponding test), HarfBuzz splits into two clusters.
|
||||
for c in &cells[2..] {
|
||||
assert_eq!(2, c.x);
|
||||
}
|
||||
|
||||
// The vowel sign E renders before the SSA:
|
||||
assert!(cells[2].x_offset < cells[3].x_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_box_glyphs() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("\u{2500}\u{2501}"); // horiz line
|
||||
|
||||
let runs = run_row(&mut ts, &t, 0);
|
||||
assert_eq!(1, runs.len());
|
||||
assert_eq!(2, runs[0].hb_len);
|
||||
let cells = &runs[0].cells;
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0x2500, cells[0].glyph_index);
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0x2501, cells[1].glyph_index);
|
||||
assert_eq!(1, cells[1].x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_selection_boundary() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("a1b2c3d4e5");
|
||||
let cells = t.row(0);
|
||||
|
||||
// Full line selection
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, Some([0, 9]), None));
|
||||
// Offset x, goes to end of line selection
|
||||
assert_eq!(2, count_runs(&mut ts, &cells, Some([2, 9]), None));
|
||||
// Offset x, starts at beginning of line
|
||||
assert_eq!(2, count_runs(&mut ts, &cells, Some([0, 3]), None));
|
||||
// Selection only subset of line
|
||||
assert_eq!(3, count_runs(&mut ts, &cells, Some([1, 3]), None));
|
||||
// Selection only one character
|
||||
assert_eq!(3, count_runs(&mut ts, &cells, Some([1, 1]), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_cursor_boundary() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print("a1b2c3d4e5");
|
||||
let cells = t.row(0);
|
||||
|
||||
// No cursor is full line
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
|
||||
// Cursor at index 0 is two runs
|
||||
assert_eq!(2, count_runs(&mut ts, &cells, None, Some(0)));
|
||||
// And without cursor splitting remains one
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
|
||||
// Cursor at index 1 is three runs
|
||||
assert_eq!(3, count_runs(&mut ts, &cells, None, Some(1)));
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
|
||||
// Cursor at last col is two runs
|
||||
assert_eq!(2, count_runs(&mut ts, &cells, None, Some(9)));
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
|
||||
// The font break config disables cursor splitting.
|
||||
let mut opts = RunOptions::new(&ts.grid, &cells);
|
||||
opts.cursor_x = Some(1);
|
||||
opts.apply_break_config(ghostty_config::types_a::FontShapingBreak { cursor: false });
|
||||
assert_eq!(1, shape_all(&mut ts.shaper, opts).len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_cursor_boundary_and_colored_emoji() {
|
||||
let mut ts = test_shaper();
|
||||
|
||||
let mut t = Term::new(3, 10);
|
||||
t.print("\u{1F44D}\u{1F3FC}");
|
||||
let cells = t.row(0);
|
||||
|
||||
// No cursor is full line
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
|
||||
// Cursor on emoji does not split it
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, Some(0)));
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, Some(1)));
|
||||
assert_eq!(1, count_runs(&mut ts, &cells, None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_cell_attribute_change() {
|
||||
use ghostty_vt::sgr::Attribute;
|
||||
|
||||
let mut ts = test_shaper();
|
||||
|
||||
// Plain >= should shape into 1 run
|
||||
{
|
||||
let mut t = Term::new(10, 3);
|
||||
t.print(">=");
|
||||
assert_eq!(1, count_runs(&mut ts, &t.row(0), None, None));
|
||||
}
|
||||
|
||||
// Bold vs regular should split
|
||||
{
|
||||
let mut t = Term::new(3, 10);
|
||||
t.print(">");
|
||||
t.sgr(Attribute::Bold);
|
||||
t.print("=");
|
||||
assert_eq!(2, count_runs(&mut ts, &t.row(0), None, None));
|
||||
}
|
||||
|
||||
// Changing fg color should split
|
||||
{
|
||||
let mut t = Term::new(3, 10);
|
||||
t.fg_rgb(1, 2, 3);
|
||||
t.print(">");
|
||||
t.fg_rgb(3, 2, 1);
|
||||
t.print("=");
|
||||
assert_eq!(2, count_runs(&mut ts, &t.row(0), None, None));
|
||||
}
|
||||
|
||||
// Changing bg color should not split
|
||||
{
|
||||
let mut t = Term::new(3, 10);
|
||||
t.bg_rgb(1, 2, 3);
|
||||
t.print(">");
|
||||
t.bg_rgb(3, 2, 1);
|
||||
t.print("=");
|
||||
assert_eq!(1, count_runs(&mut ts, &t.row(0), None, None));
|
||||
}
|
||||
|
||||
// Same bg color should not split
|
||||
{
|
||||
let mut t = Term::new(3, 10);
|
||||
t.bg_rgb(1, 2, 3);
|
||||
t.print(">");
|
||||
t.print("=");
|
||||
assert_eq!(1, count_runs(&mut ts, &t.row(0), None, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Text shapers. Port of `font/shaper/`.
|
||||
//!
|
||||
//! `run.zig` (RunIterator over terminal rows) is deferred (F2).
|
||||
|
||||
pub mod cache;
|
||||
pub mod feature;
|
||||
pub mod harfbuzz;
|
||||
mod hb;
|
||||
pub mod noop;
|
||||
pub mod run;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util;
|
||||
@@ -0,0 +1,568 @@
|
||||
//! Text runs and the run iterator. Port of `font/shaper/run.zig`.
|
||||
//!
|
||||
//! A [`RunIterator`] walks one terminal row (given as a slice of
|
||||
//! [`RunCell`]) and yields [`TextRun`]s: maximal sequences of cells that can
|
||||
//! be shaped together (same font, same style apart from background, not
|
||||
//! crossing a selection or cursor boundary, ...). The codepoints of each run
|
||||
//! are fed to the shaper through the [`RunIteratorHook`] trait
|
||||
//! (`prepare` / `add_codepoint` / `finalize`).
|
||||
|
||||
#![allow(clippy::collapsible_if)]
|
||||
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use ghostty_config::types_a::FontShapingBreak;
|
||||
use ghostty_terminal::page::{Cell, ContentTag, Page, Wide};
|
||||
use ghostty_terminal::style::{Color, Style as TermStyle};
|
||||
|
||||
use crate::collection::Index;
|
||||
use crate::shared_grid::{Error as GridError, SharedGrid};
|
||||
use crate::{Presentation, Style};
|
||||
|
||||
/// The Kitty graphics unicode placeholder codepoint
|
||||
/// (`terminal.kitty.graphics.unicode.placeholder`).
|
||||
// TODO(integration): use the constant from ghostty-terminal once the kitty
|
||||
// graphics module lands there.
|
||||
pub const KITTY_PLACEHOLDER: u32 = 0x10EEEE;
|
||||
|
||||
/// A single text run. A text run is only valid for one Shaper instance and
|
||||
/// until the next run is created. A text run never goes across multiple
|
||||
/// rows in a terminal, so it is guaranteed to always be one line.
|
||||
///
|
||||
/// Deviation: the Zig struct carries a `grid: *SharedGrid` field. Here the
|
||||
/// grid is not part of the run; shapers take it explicitly
|
||||
/// (`harfbuzz::Shaper::shape_run(grid, run)`), which keeps `TextRun` `Copy`
|
||||
/// and lifetime-free.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TextRun {
|
||||
/// A unique hash for this run. This can be used to cache the shaping
|
||||
/// results. We don't provide a means to compare actual values if the
|
||||
/// hash is the same, so we should continue to improve this hash to
|
||||
/// lower the chance of hash collisions if they become a problem. If
|
||||
/// there are hash collisions, it would result in rendering issues but
|
||||
/// the core data would be correct.
|
||||
///
|
||||
/// The hash is position-independent within the row by using relative
|
||||
/// cluster positions. This allows identical runs in different positions
|
||||
/// to share the same cache entry, improving cache efficiency.
|
||||
pub hash: u64,
|
||||
|
||||
/// The offset in the row where this run started. This is added to the
|
||||
/// X position of the final shaped cells to get the absolute position
|
||||
/// in the row where they belong.
|
||||
pub offset: u16,
|
||||
|
||||
/// The total number of cells produced by this run.
|
||||
pub cells: u16,
|
||||
|
||||
/// The font index to use for the glyphs of this run.
|
||||
pub font_index: Index,
|
||||
}
|
||||
|
||||
/// One cell of a row to shape: the equivalent of one element of
|
||||
/// `terminal.RenderState.Cell` (`raw`, `grapheme`, `style`).
|
||||
// TODO(integration): replace with the render-state cell type from
|
||||
// ghostty-terminal once `RenderState` is ported there.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RunCell {
|
||||
/// The raw page cell.
|
||||
pub raw: Cell,
|
||||
/// The extra codepoints of the grapheme (empty unless
|
||||
/// `raw.has_grapheme()`).
|
||||
pub grapheme: Vec<u32>,
|
||||
/// The resolved style (default style if the cell has no styling).
|
||||
pub style: TermStyle,
|
||||
}
|
||||
|
||||
impl RunCell {
|
||||
/// Build the run cells for row `y` of a page.
|
||||
pub fn from_page_row(page: &Page, y: usize) -> Vec<RunCell> {
|
||||
let Some(cells) = page.get_cells(y) else {
|
||||
return Vec::new();
|
||||
};
|
||||
cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(x, &raw)| {
|
||||
let grapheme = if raw.has_grapheme() {
|
||||
page.cell_index(x, y)
|
||||
.ok()
|
||||
.and_then(|i| page.lookup_grapheme(i))
|
||||
.map(<[u32]>::to_vec)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let style = if raw.has_styling() {
|
||||
page.style(raw)
|
||||
} else {
|
||||
TermStyle::default()
|
||||
};
|
||||
RunCell {
|
||||
raw,
|
||||
grapheme,
|
||||
style,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for [`RunIterator`] (`shape.RunOptions`).
|
||||
pub struct RunOptions<'a> {
|
||||
/// The font state for the terminal screen. Cached values may be updated
|
||||
/// during shaping (the grid has interior locking).
|
||||
pub grid: &'a SharedGrid,
|
||||
|
||||
/// The cells for the row to shape.
|
||||
pub cells: &'a [RunCell],
|
||||
|
||||
/// The x boundaries of the selection in this row.
|
||||
pub selection: Option<[u16; 2]>,
|
||||
|
||||
/// The cursor position within this row. This is used to break shaping
|
||||
/// on cursor boundaries. This can be disabled by setting this to
|
||||
/// None.
|
||||
pub cursor_x: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> RunOptions<'a> {
|
||||
pub fn new(grid: &'a SharedGrid, cells: &'a [RunCell]) -> Self {
|
||||
RunOptions {
|
||||
grid,
|
||||
cells,
|
||||
selection: None,
|
||||
cursor_x: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the font break configuration to the run.
|
||||
pub fn apply_break_config(&mut self, config: FontShapingBreak) {
|
||||
if !config.cursor {
|
||||
self.cursor_x = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hooks a shaper provides to the run iterator
|
||||
/// (`font.Shaper.RunIteratorHook`).
|
||||
pub trait RunIteratorHook {
|
||||
/// Called before a new run is started.
|
||||
fn prepare(&mut self);
|
||||
/// Called for each codepoint of the run, with the (run relative) cluster.
|
||||
fn add_codepoint(&mut self, cp: u32, cluster: u32);
|
||||
/// Called when the run is complete.
|
||||
fn finalize(&mut self);
|
||||
}
|
||||
|
||||
impl RunIteratorHook for crate::shaper::harfbuzz::Shaper {
|
||||
fn prepare(&mut self) {
|
||||
Self::prepare(self);
|
||||
}
|
||||
fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
Self::add_codepoint(self, cp, cluster);
|
||||
}
|
||||
fn finalize(&mut self) {
|
||||
Self::finalize(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl RunIteratorHook for crate::shaper::noop::Shaper {
|
||||
fn prepare(&mut self) {
|
||||
Self::prepare(self);
|
||||
}
|
||||
fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
Self::add_codepoint(self, cp, cluster);
|
||||
}
|
||||
fn finalize(&mut self) {
|
||||
Self::finalize(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// RunIterator is an iterator that yields text runs. Unlike Zig, which
|
||||
/// stores the hooks inside the iterator, the shaper is passed to
|
||||
/// [`RunIterator::next`] so it can be used (to shape) between calls.
|
||||
pub struct RunIterator<'a> {
|
||||
pub opts: RunOptions<'a>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a> RunIterator<'a> {
|
||||
pub fn new(opts: RunOptions<'a>) -> Self {
|
||||
RunIterator { opts, i: 0 }
|
||||
}
|
||||
|
||||
pub fn next(&mut self, hooks: &mut dyn RunIteratorHook) -> Result<Option<TextRun>, GridError> {
|
||||
let cells = self.opts.cells;
|
||||
|
||||
// Trim the right side of a row that might be empty
|
||||
let max: usize = (0..cells.len())
|
||||
.rev()
|
||||
.find(|&i| !cells[i].raw.is_empty())
|
||||
.map_or(0, |i| i + 1);
|
||||
|
||||
// Invisible cells don't have any glyphs rendered,
|
||||
// so we explicitly skip them in the shaping process.
|
||||
while self.i < max
|
||||
&& (cells[self.i].raw.has_styling() && cells[self.i].style.flags.invisible)
|
||||
{
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
// We're over at the max
|
||||
if self.i >= max {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Track the font for our current run
|
||||
let mut current_font = Index::default();
|
||||
|
||||
// Allow the hook to prepare
|
||||
hooks.prepare();
|
||||
|
||||
// Initialize our hash for this run.
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
// Let's get our style that we'll expect for the run.
|
||||
let style: TermStyle = if cells[self.i].raw.has_styling() {
|
||||
cells[self.i].style
|
||||
} else {
|
||||
TermStyle::default()
|
||||
};
|
||||
|
||||
// Go through cell by cell and accumulate while we build our run.
|
||||
let start = self.i;
|
||||
let end = 'run: {
|
||||
for j in start..max {
|
||||
// Use relative cluster positions (offset from run start) to make
|
||||
// the shaping cache position-independent. This ensures that runs
|
||||
// with identical content but different starting positions in the
|
||||
// row produce the same hash, enabling cache reuse.
|
||||
let cluster = (j - start) as u32;
|
||||
let cell = &cells[j];
|
||||
let raw = cell.raw;
|
||||
|
||||
// If we have a selection and we're at a boundary point, then
|
||||
// we break the run here.
|
||||
if let Some(bounds) = self.opts.selection {
|
||||
if j > start {
|
||||
let (b0, b1) = (usize::from(bounds[0]), usize::from(bounds[1]));
|
||||
if b0 > 0 && j == b0 {
|
||||
break 'run j;
|
||||
}
|
||||
if b1 > 0 && j == b1 + 1 {
|
||||
break 'run j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're a spacer, then we ignore it
|
||||
match raw.wide() {
|
||||
Wide::Narrow | Wide::Wide => {}
|
||||
Wide::SpacerHead | Wide::SpacerTail => continue,
|
||||
}
|
||||
|
||||
// If our cell attributes are changing, then we split the run.
|
||||
// This prevents a single glyph for ">=" to be rendered with
|
||||
// one color when the two components have different styling.
|
||||
if j > start {
|
||||
let prev_cell = cells[j - 1].raw;
|
||||
|
||||
// If the prev cell and this cell are both plain
|
||||
// codepoints then we check if they are commonly "bad"
|
||||
// ligatures and spit the run if they are.
|
||||
if prev_cell.content_tag() == ContentTag::Codepoint
|
||||
&& raw.content_tag() == ContentTag::Codepoint
|
||||
{
|
||||
let prev_cp = prev_cell.codepoint();
|
||||
let cp = raw.codepoint();
|
||||
match char::from_u32(prev_cp) {
|
||||
// fl, fi
|
||||
Some('f') if cp == u32::from('l') || cp == u32::from('i') => {
|
||||
break 'run j;
|
||||
}
|
||||
// st
|
||||
Some('s') if cp == u32::from('t') => break 'run j,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If the style is exactly the change then fast path out.
|
||||
// Otherwise the style is different. We allow differing
|
||||
// background styles but any other change results in a
|
||||
// new run.
|
||||
if prev_cell.style_id() != raw.style_id() {
|
||||
let c1 = comparable_style(style);
|
||||
let c2 = comparable_style(if raw.has_styling() {
|
||||
cell.style
|
||||
} else {
|
||||
TermStyle::default()
|
||||
});
|
||||
if c1 != c2 {
|
||||
break 'run j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text runs break when font styles change so we need to get
|
||||
// the proper style.
|
||||
let font_style = if style.flags.bold {
|
||||
if style.flags.italic {
|
||||
Style::BoldItalic
|
||||
} else {
|
||||
Style::Bold
|
||||
}
|
||||
} else if style.flags.italic {
|
||||
Style::Italic
|
||||
} else {
|
||||
Style::Regular
|
||||
};
|
||||
|
||||
// Determine the presentation format for this glyph.
|
||||
let presentation: Option<Presentation> = if raw.has_grapheme() {
|
||||
// We only check the FIRST codepoint because I believe the
|
||||
// presentation format must be directly adjacent to the codepoint.
|
||||
let cps = &cell.grapheme;
|
||||
assert_ne!(cps.len(), 0);
|
||||
match cps[0] {
|
||||
0xFE0E => Some(Presentation::Text),
|
||||
0xFE0F => Some(Presentation::Emoji),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
// If we're not a grapheme, our individual char could be
|
||||
// an emoji so we want to check if we expect emoji presentation.
|
||||
// The font grid index_for_codepoint we use below will do this
|
||||
// automatically.
|
||||
None
|
||||
};
|
||||
|
||||
// If our cursor is on this line then we break the run around the
|
||||
// cursor. This means that any row with a cursor has at least
|
||||
// three breaks: before, exactly the cursor, and after.
|
||||
//
|
||||
// We do not break a cell that is exactly the grapheme. If there
|
||||
// are cells following that contain joiners, we allow those to
|
||||
// break. This creates an effect where hovering over an emoji
|
||||
// such as a skin-tone emoji is fine, but hovering over the
|
||||
// joiners will show the joiners allowing you to modify the
|
||||
// emoji.
|
||||
if !raw.has_grapheme() {
|
||||
if let Some(cursor_x) = self.opts.cursor_x {
|
||||
// Exactly: self.i is the cursor and we iterated once. This
|
||||
// means that we started exactly at the cursor and did at
|
||||
// exactly one iteration. Why exactly one? Because we may
|
||||
// start at our cursor but do many if our cursor is exactly
|
||||
// on an emoji.
|
||||
if start == cursor_x && j == start + 1 {
|
||||
break 'run j;
|
||||
}
|
||||
|
||||
// Before: up to and not including the cursor. This means
|
||||
// that we started before the cursor (self.i < cursor_x)
|
||||
// and j is now at the cursor meaning we haven't yet processed
|
||||
// the cursor.
|
||||
if start < cursor_x && j == cursor_x {
|
||||
assert!(j > 0);
|
||||
break 'run j;
|
||||
}
|
||||
|
||||
// After: after the cursor. We don't need to do anything
|
||||
// special, we just let the run complete.
|
||||
}
|
||||
}
|
||||
|
||||
// We need to find a font that supports this character. If
|
||||
// there are additional zero-width codepoints (to form a single
|
||||
// grapheme, i.e. combining characters), we need to find a font
|
||||
// that supports all of them.
|
||||
let (font_idx, fallback): (Index, Option<u32>) = 'font_info: {
|
||||
// If we find a font that supports this entire grapheme
|
||||
// then we use that.
|
||||
if let Some(idx) = self.index_for_cell(cell, font_style, presentation)? {
|
||||
break 'font_info (idx, None);
|
||||
}
|
||||
|
||||
// Otherwise we need a fallback character. Prefer the
|
||||
// official replacement character.
|
||||
if let Some(idx) = self.opts.grid.get_index(0xFFFD, font_style, presentation)? {
|
||||
break 'font_info (idx, Some(0xFFFD));
|
||||
}
|
||||
|
||||
// Fallback to space
|
||||
if let Some(idx) =
|
||||
self.opts
|
||||
.grid
|
||||
.get_index(u32::from(' '), font_style, presentation)?
|
||||
{
|
||||
break 'font_info (idx, Some(u32::from(' ')));
|
||||
}
|
||||
|
||||
// We can't render at all. This is a bug, we should always
|
||||
// have a font that can render a space.
|
||||
unreachable!("no font can render a space");
|
||||
};
|
||||
|
||||
if j == start {
|
||||
current_font = font_idx;
|
||||
}
|
||||
|
||||
// If our fonts are not equal, then we're done with our run.
|
||||
if font_idx != current_font {
|
||||
break 'run j;
|
||||
}
|
||||
|
||||
// If we're a fallback character, add that and continue; we
|
||||
// don't want to add the entire grapheme.
|
||||
if let Some(cp) = fallback {
|
||||
add_codepoint(hooks, &mut hasher, cp, cluster);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're a Kitty unicode placeholder then we add a blank.
|
||||
if raw.codepoint() == KITTY_PLACEHOLDER {
|
||||
add_codepoint(hooks, &mut hasher, u32::from(' '), cluster);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all the codepoints for our grapheme
|
||||
add_codepoint(
|
||||
hooks,
|
||||
&mut hasher,
|
||||
if raw.codepoint() == 0 {
|
||||
u32::from(' ')
|
||||
} else {
|
||||
raw.codepoint()
|
||||
},
|
||||
cluster,
|
||||
);
|
||||
if raw.has_grapheme() {
|
||||
for &cp in &cell.grapheme {
|
||||
// Do not send presentation modifiers
|
||||
if cp == 0xFE0E || cp == 0xFE0F {
|
||||
continue;
|
||||
}
|
||||
add_codepoint(hooks, &mut hasher, cp, cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
max
|
||||
};
|
||||
|
||||
// Finalize our buffer
|
||||
hooks.finalize();
|
||||
|
||||
// Add our length to the hash as an additional mechanism to avoid collisions
|
||||
(end - start).hash(&mut hasher);
|
||||
|
||||
// Add our font index
|
||||
current_font.hash(&mut hasher);
|
||||
|
||||
// Move our cursor.
|
||||
self.i = end;
|
||||
|
||||
Ok(Some(TextRun {
|
||||
hash: hasher.finish(),
|
||||
offset: start as u16,
|
||||
cells: (end - start) as u16,
|
||||
font_index: current_font,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Find a font index that supports the grapheme for the given cell,
|
||||
/// or None if no such font exists.
|
||||
///
|
||||
/// This is used to find a font that supports the entire grapheme.
|
||||
/// We look for fonts that support each individual codepoint and then
|
||||
/// find the common font amongst all candidates.
|
||||
fn index_for_cell(
|
||||
&self,
|
||||
cell: &RunCell,
|
||||
style: Style,
|
||||
presentation: Option<Presentation>,
|
||||
) -> Result<Option<Index>, GridError> {
|
||||
let raw = cell.raw;
|
||||
let grid = self.opts.grid;
|
||||
if raw.is_empty() || raw.codepoint() == 0 || raw.codepoint() == KITTY_PLACEHOLDER {
|
||||
return grid.get_index(u32::from(' '), style, presentation);
|
||||
}
|
||||
|
||||
// Get the font index for the primary codepoint.
|
||||
let primary_cp = raw.codepoint();
|
||||
let Some(primary) = grid.get_index(primary_cp, style, presentation)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Easy, and common: we aren't a multi-codepoint grapheme, so
|
||||
// we just return whatever index for the cell codepoint.
|
||||
if !raw.has_grapheme() {
|
||||
return Ok(Some(primary));
|
||||
}
|
||||
|
||||
// If this is a grapheme, we need to find a font that supports
|
||||
// all of the codepoints in the grapheme.
|
||||
let graphemes = &cell.grapheme;
|
||||
let mut candidates: Vec<Index> = Vec::with_capacity(graphemes.len() + 1);
|
||||
candidates.push(primary);
|
||||
|
||||
for &cp in graphemes {
|
||||
// Ignore Emoji ZWJs
|
||||
if cp == 0xFE0E || cp == 0xFE0F || cp == 0x200D {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a font that supports this codepoint. If none support this
|
||||
// then the whole grapheme can't be rendered so we return None.
|
||||
//
|
||||
// We explicitly do not require the additional grapheme components
|
||||
// to support the base presentation, since it is common for emoji
|
||||
// fonts to support the base emoji with emoji presentation but not
|
||||
// certain ZWJ-combined characters like the male and female signs.
|
||||
let Some(idx) = grid.get_index(cp, style, None)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
candidates.push(idx);
|
||||
}
|
||||
|
||||
// We need to find a candidate that has ALL of our codepoints
|
||||
for &idx in &candidates {
|
||||
if !grid.has_codepoint(idx, primary_cp, presentation) {
|
||||
continue;
|
||||
}
|
||||
let all = graphemes.iter().all(|&cp| {
|
||||
// Ignore Emoji ZWJs
|
||||
cp == 0xFE0E || cp == 0xFE0F || cp == 0x200D || grid.has_codepoint(idx, cp, None)
|
||||
});
|
||||
if all {
|
||||
// We have a candidate that supports all of our codepoints.
|
||||
return Ok(Some(idx));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_codepoint(
|
||||
hooks: &mut dyn RunIteratorHook,
|
||||
hasher: &mut DefaultHasher,
|
||||
cp: u32,
|
||||
cluster: u32,
|
||||
) {
|
||||
cp.hash(hasher);
|
||||
cluster.hash(hasher);
|
||||
hooks.add_codepoint(cp, cluster);
|
||||
}
|
||||
|
||||
/// Returns a style that when compared must be identical for a run to
|
||||
/// continue.
|
||||
fn comparable_style(style: TermStyle) -> TermStyle {
|
||||
let mut s = style;
|
||||
|
||||
// We allow background colors to differ because we'll just paint the
|
||||
// cell background whatever the style is, and wherever the glyph
|
||||
// lands on top of it will be the color of the glyph.
|
||||
s.bg_color = Color::None;
|
||||
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! Test helpers shared by the shaper tests (the Zig `testShaper*` helpers
|
||||
//! plus a tiny terminal emulation on top of `ghostty_terminal::screen::Screen`).
|
||||
//!
|
||||
//! TODO(integration): the Zig tests drive a full `Terminal` through a VT
|
||||
//! stream and read a `RenderState`. `Terminal` is not available in
|
||||
//! ghostty-terminal yet, so these tests print through `Screen` instead
|
||||
//! (`Term`), which covers everything the tests need (text, grapheme
|
||||
//! clusters, SGR, cursor movement).
|
||||
|
||||
#![allow(clippy::collapsible_if)]
|
||||
|
||||
use ghostty_terminal::page::Cell;
|
||||
use ghostty_terminal::point::{Coordinate, Point};
|
||||
use ghostty_terminal::screen::{Options, Screen};
|
||||
use ghostty_vt::color::Rgb;
|
||||
use ghostty_vt::sgr::Attribute;
|
||||
|
||||
use crate::codepoint_resolver::CodepointResolver;
|
||||
use crate::collection::{AddOptions, Collection, LoadOptions, SizeAdjustment};
|
||||
use crate::discovery::{Descriptor, Fontconfig};
|
||||
use crate::face::{DesiredSize, FontData, Options as FaceOptions};
|
||||
use crate::shaper::harfbuzz::Shaper;
|
||||
use crate::shaper::run::{RunCell, RunIterator, RunOptions};
|
||||
use crate::{Face, Library, SharedGrid, Style, embedded, shape};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TestFont {
|
||||
Inconsolata,
|
||||
MonaspaceNeon,
|
||||
Arabic,
|
||||
}
|
||||
|
||||
pub(crate) struct TestShaper {
|
||||
pub shaper: Shaper,
|
||||
pub grid: SharedGrid,
|
||||
}
|
||||
|
||||
fn face_opts() -> FaceOptions {
|
||||
FaceOptions::new(DesiredSize::new(12.0))
|
||||
}
|
||||
|
||||
fn add_regular(c: &mut Collection, face: Face) {
|
||||
c.add(
|
||||
face,
|
||||
AddOptions {
|
||||
style: Style::Regular,
|
||||
fallback: false,
|
||||
size_adjustment: SizeAdjustment::None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Helper to return a fully initialized shaper.
|
||||
pub(crate) fn test_shaper() -> TestShaper {
|
||||
test_shaper_with_font(TestFont::Inconsolata)
|
||||
}
|
||||
|
||||
pub(crate) fn test_shaper_with_font(font_req: TestFont) -> TestShaper {
|
||||
let test_font = match font_req {
|
||||
TestFont::Inconsolata => embedded::INCONSOLATA,
|
||||
TestFont::MonaspaceNeon => embedded::MONASPACE_NEON,
|
||||
TestFont::Arabic => embedded::ARABIC,
|
||||
};
|
||||
|
||||
let lib = Library::init().unwrap();
|
||||
let mut c = Collection::new();
|
||||
c.load_options = Some(LoadOptions::new(lib.clone()));
|
||||
|
||||
add_regular(
|
||||
&mut c,
|
||||
Face::init(&lib, FontData::Static(test_font), face_opts()).unwrap(),
|
||||
);
|
||||
add_regular(
|
||||
&mut c,
|
||||
Face::init(&lib, FontData::Static(embedded::EMOJI), face_opts()).unwrap(),
|
||||
);
|
||||
add_regular(
|
||||
&mut c,
|
||||
Face::init(&lib, FontData::Static(embedded::EMOJI_TEXT), face_opts()).unwrap(),
|
||||
);
|
||||
|
||||
let grid = SharedGrid::new(CodepointResolver::new(c)).unwrap();
|
||||
let shaper = Shaper::new(&shape::Options {
|
||||
// Some of our tests rely on dlig being enabled by default
|
||||
features: vec!["dlig".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
TestShaper { shaper, grid }
|
||||
}
|
||||
|
||||
/// Returns None (the test is skipped, like `error.SkipZigTest`) if the font
|
||||
/// can't be discovered.
|
||||
pub(crate) fn test_shaper_with_discovered_font(font_req: &str) -> Option<TestShaper> {
|
||||
let lib = Library::init().unwrap();
|
||||
let mut c = Collection::new();
|
||||
c.load_options = Some(LoadOptions::new(lib.clone()));
|
||||
|
||||
// Discover and add our font to the collection.
|
||||
{
|
||||
let disco = Fontconfig::init();
|
||||
let mut it = disco
|
||||
.discover(&Descriptor {
|
||||
family: Some(font_req.to_string()),
|
||||
size: 12.0,
|
||||
monospace: false,
|
||||
..Descriptor::default()
|
||||
})
|
||||
.ok()?;
|
||||
let mut face = it.next().ok()??;
|
||||
|
||||
// Check which font was discovered - skip if it doesn't match the request
|
||||
let name = face.name().unwrap_or_else(|_| "(unknown)".to_string());
|
||||
if !name.contains(font_req) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let loaded = face.load(&lib, face_opts()).ok()?;
|
||||
add_regular(&mut c, loaded);
|
||||
}
|
||||
|
||||
let grid = SharedGrid::new(CodepointResolver::new(c)).unwrap();
|
||||
let shaper = Shaper::new(&shape::Options::default()).unwrap();
|
||||
Some(TestShaper { shaper, grid })
|
||||
}
|
||||
|
||||
/// A minimal terminal on top of a [`Screen`].
|
||||
pub(crate) struct Term {
|
||||
pub screen: Screen,
|
||||
}
|
||||
|
||||
impl Term {
|
||||
pub fn new(cols: u16, rows: u16) -> Term {
|
||||
Term {
|
||||
screen: Screen::new(Options::new(cols, rows, 0)).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a string with grapheme clustering disabled (the default of
|
||||
/// `Terminal`: `grapheme_cluster` mode off). Zero-width codepoints
|
||||
/// still combine with the previous cell.
|
||||
pub fn print(&mut self, s: &str) {
|
||||
self.print_mode(s, false);
|
||||
}
|
||||
|
||||
/// Print a string with grapheme clustering enabled
|
||||
/// (`t.modes.set(.grapheme_cluster, true)`): codepoints that do not
|
||||
/// break the grapheme cluster are attached to the cell of the cluster's
|
||||
/// first codepoint.
|
||||
pub fn print_clustered(&mut self, s: &str) {
|
||||
self.print_mode(s, true);
|
||||
}
|
||||
|
||||
fn print_mode(&mut self, s: &str, clustering: bool) {
|
||||
let mut state = ghostty_unicode::BreakState::default();
|
||||
let mut prev: Option<u32> = None;
|
||||
let mut base: Option<ghostty_terminal::page_list::Pin> = None;
|
||||
for ch in s.chars() {
|
||||
let cp = ch as u32;
|
||||
if clustering {
|
||||
if let (Some(p), Some(pin)) = (prev, base) {
|
||||
if !ghostty_unicode::grapheme_break(p, cp, &mut state) {
|
||||
let page = self.screen.page_mut(pin.node);
|
||||
page.append_grapheme(usize::from(pin.x), usize::from(pin.y), cp)
|
||||
.unwrap();
|
||||
prev = Some(cp);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A new cluster starts at the cursor (wide chars never wrap in
|
||||
// these tests).
|
||||
base = Some(self.screen.cursor_pin());
|
||||
state = ghostty_unicode::BreakState::default();
|
||||
self.screen
|
||||
.test_write_string(ch.encode_utf8(&mut [0; 4]))
|
||||
.unwrap();
|
||||
prev = Some(cp);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sgr(&mut self, attr: Attribute<'_>) {
|
||||
self.screen.set_attribute(attr).unwrap();
|
||||
}
|
||||
|
||||
pub fn fg_rgb(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.sgr(Attribute::DirectColorFg(Rgb { r, g, b }));
|
||||
}
|
||||
|
||||
pub fn bg_rgb(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.sgr(Attribute::DirectColorBg(Rgb { r, g, b }));
|
||||
}
|
||||
|
||||
pub fn cursor_right(&mut self, n: u16) {
|
||||
self.screen.cursor_right(n);
|
||||
}
|
||||
|
||||
/// Mutable access to the cell at (x, y) of the active area.
|
||||
pub fn cell_mut(&mut self, x: u16, y: u32) -> &mut Cell {
|
||||
self.screen
|
||||
.pages
|
||||
.get_cell_mut(Point::Active(Coordinate { x, y }))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The run cells of row `y` of the active area (`state.row_data.get(y).cells`).
|
||||
pub fn row(&self, y: u32) -> Vec<RunCell> {
|
||||
let pin = self
|
||||
.screen
|
||||
.pages
|
||||
.pin(Point::Active(Coordinate { x: 0, y }))
|
||||
.unwrap();
|
||||
RunCell::from_page_row(self.screen.page(pin.node), usize::from(pin.y))
|
||||
}
|
||||
}
|
||||
|
||||
/// One iterated run together with its shaped cells and the number of
|
||||
/// codepoints that were in the HarfBuzz buffer before shaping.
|
||||
pub(crate) struct Shaped {
|
||||
pub run: crate::shape::TextRun,
|
||||
pub hb_len: usize,
|
||||
pub cells: Vec<shape::Cell>,
|
||||
}
|
||||
|
||||
/// Iterate all runs of `opts`, shaping each one.
|
||||
pub(crate) fn shape_all(shaper: &mut Shaper, opts: RunOptions<'_>) -> Vec<Shaped> {
|
||||
let mut it = RunIterator::new(opts);
|
||||
let mut out = Vec::new();
|
||||
while let Some(run) = it.next(shaper).unwrap() {
|
||||
let hb_len = shaper.hb_buf_len();
|
||||
let cells = shaper.shape_run(it.opts.grid, &run).unwrap().to_vec();
|
||||
out.push(Shaped { run, hb_len, cells });
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
//! This structure contains a set of SharedGrid structures keyed by
|
||||
//! unique font configuration. Port of `font/SharedGridSet.zig`.
|
||||
//!
|
||||
//! Most terminals (surfaces) will share the same font configuration.
|
||||
//! This structure allows expensive font information such as
|
||||
//! the font atlas, glyph cache, font faces, etc. to be shared.
|
||||
//!
|
||||
//! This structure is thread-safe when the operations are documented
|
||||
//! as thread-safe.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use ghostty_config::config::Config;
|
||||
use ghostty_config::font_types::Variation;
|
||||
use ghostty_config::font_types::VariationId;
|
||||
use ghostty_config::types_a::{
|
||||
FontStyle, FontSyntheticStyle, RepeatableCodepointMap, RepeatableFontVariation,
|
||||
RepeatableString,
|
||||
};
|
||||
use ghostty_config::types_b::FreetypeLoadFlags;
|
||||
|
||||
use crate::codepoint_map::CodepointMap;
|
||||
use crate::codepoint_resolver::{CodepointResolver, StyleStatus};
|
||||
use crate::collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment};
|
||||
use crate::discovery::{Descriptor, Discover, Fontconfig};
|
||||
use crate::face::{self, DesiredSize, FontData};
|
||||
use crate::library::{Library, LibraryError};
|
||||
use crate::metrics::{Key as MetricsKey, Modifier, ModifierSet};
|
||||
use crate::shared_grid::{self, SharedGrid};
|
||||
use crate::{Face, Style, embedded};
|
||||
|
||||
/// Errors from [`SharedGridSet::ref_`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RefError {
|
||||
#[error(transparent)]
|
||||
Discover(#[from] crate::discovery::DiscoverError),
|
||||
#[error("font discovery failed: {0}")]
|
||||
DiscoverNext(#[from] crate::fontconfig::Error),
|
||||
#[error(transparent)]
|
||||
Add(#[from] collection::AddError),
|
||||
#[error(transparent)]
|
||||
AddDeferred(#[from] collection::AddDeferredError),
|
||||
#[error(transparent)]
|
||||
Complete(#[from] collection::CompleteError),
|
||||
#[error(transparent)]
|
||||
GetFace(#[from] collection::GetFaceError),
|
||||
#[error(transparent)]
|
||||
Face(#[from] face::freetype::FaceError),
|
||||
#[error(transparent)]
|
||||
Grid(#[from] shared_grid::InitError),
|
||||
#[error(transparent)]
|
||||
Load(#[from] crate::deferred_face::LoadError),
|
||||
}
|
||||
|
||||
pub type InitError = LibraryError;
|
||||
|
||||
/// Ref-counted SharedGrid.
|
||||
struct ReffedGrid {
|
||||
grid: Arc<SharedGrid>,
|
||||
ref_count: u32,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
/// The map of font configurations to SharedGrid instances. As in
|
||||
/// Zig, key equality is equality of the key hashcode.
|
||||
map: HashMap<Key, ReffedGrid>,
|
||||
|
||||
/// Font discovery mechanism. Initialized once and kept for the
|
||||
/// lifetime of the set because some font discovery mechanisms
|
||||
/// (i.e. fontconfig) are unsafe to reinit.
|
||||
font_discover: Option<Discover>,
|
||||
}
|
||||
|
||||
pub struct SharedGridSet {
|
||||
/// The font library that is used for all font groups.
|
||||
font_lib: Library,
|
||||
|
||||
/// Lock to protect multi-threaded access to the map.
|
||||
inner: Mutex<Inner>,
|
||||
}
|
||||
|
||||
impl SharedGridSet {
|
||||
/// Initialize a new SharedGridSet.
|
||||
pub fn new() -> Result<SharedGridSet, InitError> {
|
||||
Ok(SharedGridSet {
|
||||
font_lib: Library::init()?,
|
||||
inner: Mutex::new(Inner {
|
||||
map: HashMap::new(),
|
||||
font_discover: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn lock(&self) -> MutexGuard<'_, Inner> {
|
||||
self.inner
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Returns the number of cached grids.
|
||||
pub fn count(&self) -> usize {
|
||||
self.lock().map.len()
|
||||
}
|
||||
|
||||
/// Initialize a SharedGrid for the given font configuration. If the
|
||||
/// SharedGrid is not present it will be initialized with a ref count of
|
||||
/// 1. If it is present, the ref count will be incremented.
|
||||
///
|
||||
/// (Zig: `ref`; renamed since `ref` is a Rust keyword.) The returned
|
||||
/// key must be passed to [`SharedGridSet::deref`] when done. The grid
|
||||
/// is shared through an `Arc`; the set drops its own reference when the
|
||||
/// ref count reaches zero.
|
||||
pub fn ref_(
|
||||
&self,
|
||||
config: &DerivedConfig,
|
||||
font_size: DesiredSize,
|
||||
) -> Result<(Key, Arc<SharedGrid>), RefError> {
|
||||
let key = Key::new(config, font_size);
|
||||
|
||||
let mut inner = self.lock();
|
||||
if let Some(entry) = inner.map.get_mut(&key) {
|
||||
log::debug!("found cached grid for font config");
|
||||
|
||||
// Increment our ref count and return the cache
|
||||
entry.ref_count += 1;
|
||||
return Ok((key, Arc::clone(&entry.grid)));
|
||||
}
|
||||
|
||||
log::debug!("initializing new grid for font config");
|
||||
|
||||
// A new font config, initialize the cache.
|
||||
// Build our collection. This is the expensive operation that
|
||||
// involves finding fonts, loading them (maybe, some are deferred),
|
||||
// etc.
|
||||
let c = self.collection(&mut inner, &key, font_size, config)?;
|
||||
|
||||
// Setup our enabled/disabled styles
|
||||
let mut styles = StyleStatus::init_fill(true);
|
||||
styles.set(Style::Bold, config.font_style_bold != FontStyle::False);
|
||||
styles.set(Style::Italic, config.font_style_italic != FontStyle::False);
|
||||
styles.set(
|
||||
Style::BoldItalic,
|
||||
config.font_style_bold_italic != FontStyle::False,
|
||||
);
|
||||
|
||||
// Init our resolver which just requires setting fields.
|
||||
let mut resolver = CodepointResolver::new(c);
|
||||
resolver.styles = styles;
|
||||
resolver.discover = Some(Self::discover(&mut inner).clone());
|
||||
resolver.codepoint_map = Some(key.codepoint_map.clone());
|
||||
|
||||
let grid = Arc::new(SharedGrid::new(resolver)?);
|
||||
inner.map.insert(
|
||||
key.clone(),
|
||||
ReffedGrid {
|
||||
grid: Arc::clone(&grid),
|
||||
ref_count: 1,
|
||||
},
|
||||
);
|
||||
Ok((key, grid))
|
||||
}
|
||||
|
||||
/// Builds the Collection for the given configuration key and
|
||||
/// initial font size.
|
||||
fn collection(
|
||||
&self,
|
||||
inner: &mut Inner,
|
||||
key: &Key,
|
||||
size: DesiredSize,
|
||||
config: &DerivedConfig,
|
||||
) -> Result<Collection, RefError> {
|
||||
let load_options = LoadOptions {
|
||||
library: self.font_lib.clone(),
|
||||
size,
|
||||
freetype_load_flags: key.freetype_load_flags,
|
||||
};
|
||||
|
||||
let mut c = Collection::new();
|
||||
c.load_options = Some(load_options.clone());
|
||||
c.metric_modifiers.clone_from(&key.metric_modifiers);
|
||||
|
||||
// Search for fonts
|
||||
{
|
||||
let disco = Self::discover(inner);
|
||||
for style in Style::ALL {
|
||||
for desc in key.descriptors_for_style(style) {
|
||||
{
|
||||
let mut disco_it = disco.discover(desc)?;
|
||||
if let Some(face) = disco_it.next()? {
|
||||
log::info!(
|
||||
"font {style:?}: {}",
|
||||
face.name().unwrap_or_else(|_| "<error>".into())
|
||||
);
|
||||
|
||||
c.add_deferred(
|
||||
face,
|
||||
AddOptions {
|
||||
style,
|
||||
fallback: false,
|
||||
// No size adjustment for primary fonts.
|
||||
size_adjustment: SizeAdjustment::None,
|
||||
},
|
||||
)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are variation configurations and we didn't find
|
||||
// the font, then we retry the discovery with all stylistic
|
||||
// bits set to false. This is because some fonts may not
|
||||
// set the stylistic bit in their table but still support
|
||||
// axes to mimic the style. At the time of writing, Berkeley
|
||||
// Mono Variable is like this. See #2140.
|
||||
if style != Style::Regular && !desc.variations.is_empty() {
|
||||
let mut copy = desc.clone();
|
||||
copy.bold = false;
|
||||
copy.italic = false;
|
||||
let mut disco_it = disco.discover(©)?;
|
||||
if let Some(face) = disco_it.next()? {
|
||||
log::info!(
|
||||
"font {style:?}: {}",
|
||||
face.name().unwrap_or_else(|_| "<error>".into())
|
||||
);
|
||||
|
||||
c.add_deferred(
|
||||
face,
|
||||
AddOptions {
|
||||
style,
|
||||
fallback: false,
|
||||
// No size adjustment for primary fonts.
|
||||
size_adjustment: SizeAdjustment::None,
|
||||
},
|
||||
)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!(
|
||||
"font-family {style:?} not found: {}",
|
||||
desc.family.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete our styles to ensure we have something to satisfy every
|
||||
// possible style request. We do this before adding our built-in font
|
||||
// because we want to ensure our built-in styles are fallbacks to
|
||||
// the configured styles.
|
||||
c.complete_styles(config.font_synthetic_style)?;
|
||||
|
||||
let fallback = |style: Style, size_adjustment: SizeAdjustment| AddOptions {
|
||||
style,
|
||||
fallback: true,
|
||||
size_adjustment,
|
||||
};
|
||||
let face_opts = load_options.face_options();
|
||||
let wght_700 = [Variation {
|
||||
id: VariationId::init(b"wght"),
|
||||
value: 700.0,
|
||||
}];
|
||||
|
||||
// Our built-in font will be used as a backup
|
||||
c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::VARIABLE),
|
||||
face_opts,
|
||||
)?,
|
||||
fallback(Style::Regular, crate::DEFAULT_FALLBACK_ADJUSTMENT),
|
||||
)?;
|
||||
let idx = c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::VARIABLE),
|
||||
face_opts,
|
||||
)?,
|
||||
fallback(Style::Bold, crate::DEFAULT_FALLBACK_ADJUSTMENT),
|
||||
)?;
|
||||
c.get_face(idx)?.set_variations(&wght_700, face_opts)?;
|
||||
c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::VARIABLE_ITALIC),
|
||||
face_opts,
|
||||
)?,
|
||||
fallback(Style::Italic, crate::DEFAULT_FALLBACK_ADJUSTMENT),
|
||||
)?;
|
||||
let idx = c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::VARIABLE_ITALIC),
|
||||
face_opts,
|
||||
)?,
|
||||
fallback(Style::BoldItalic, crate::DEFAULT_FALLBACK_ADJUSTMENT),
|
||||
)?;
|
||||
c.get_face(idx)?.set_variations(&wght_700, face_opts)?;
|
||||
|
||||
// Nerd-font symbols fallback.
|
||||
c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::SYMBOLS_NERD_FONT),
|
||||
face_opts,
|
||||
)?,
|
||||
// No size adjustment for the symbols font.
|
||||
fallback(Style::Regular, SizeAdjustment::None),
|
||||
)?;
|
||||
|
||||
// Emoji fallback. (Zig skips this on macOS, which is out of scope.)
|
||||
c.add(
|
||||
Face::init(&self.font_lib, FontData::Static(embedded::EMOJI), face_opts)?,
|
||||
// No size adjustment for emojis.
|
||||
fallback(Style::Regular, SizeAdjustment::None),
|
||||
)?;
|
||||
c.add(
|
||||
Face::init(
|
||||
&self.font_lib,
|
||||
FontData::Static(embedded::EMOJI_TEXT),
|
||||
face_opts,
|
||||
)?,
|
||||
fallback(Style::Regular, SizeAdjustment::None),
|
||||
)?;
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Decrement the ref count for the given key. If the ref count is zero,
|
||||
/// the grid will be deinitialized and removed from the map.
|
||||
pub fn deref(&self, key: &Key) {
|
||||
let mut inner = self.lock();
|
||||
let Some(entry) = inner.map.get_mut(key) else {
|
||||
return;
|
||||
};
|
||||
assert!(entry.ref_count >= 1);
|
||||
|
||||
// If we have more than one reference, decrement and return.
|
||||
if entry.ref_count > 1 {
|
||||
entry.ref_count -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// We are at a zero ref count so deinit the group and remove.
|
||||
inner.map.remove(key);
|
||||
}
|
||||
|
||||
/// Initialize once and return the font discovery mechanism. This remains
|
||||
/// initialized throughout the lifetime of the application because some
|
||||
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.
|
||||
fn discover(inner: &mut Inner) -> &Fontconfig {
|
||||
inner.font_discover.get_or_insert_with(Fontconfig::init)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the configuration required to create a key without having
|
||||
/// to keep the full Ghostty configuration around.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DerivedConfig {
|
||||
pub font_family: RepeatableString,
|
||||
pub font_family_bold: RepeatableString,
|
||||
pub font_family_italic: RepeatableString,
|
||||
pub font_family_bold_italic: RepeatableString,
|
||||
pub font_style: FontStyle,
|
||||
pub font_style_bold: FontStyle,
|
||||
pub font_style_italic: FontStyle,
|
||||
pub font_style_bold_italic: FontStyle,
|
||||
pub font_variation: RepeatableFontVariation,
|
||||
pub font_variation_bold: RepeatableFontVariation,
|
||||
pub font_variation_italic: RepeatableFontVariation,
|
||||
pub font_variation_bold_italic: RepeatableFontVariation,
|
||||
pub font_codepoint_map: RepeatableCodepointMap,
|
||||
pub font_synthetic_style: FontSyntheticStyle,
|
||||
pub adjust_cell_width: Option<Modifier>,
|
||||
pub adjust_cell_height: Option<Modifier>,
|
||||
pub adjust_font_baseline: Option<Modifier>,
|
||||
pub adjust_underline_position: Option<Modifier>,
|
||||
pub adjust_underline_thickness: Option<Modifier>,
|
||||
pub adjust_strikethrough_position: Option<Modifier>,
|
||||
pub adjust_strikethrough_thickness: Option<Modifier>,
|
||||
pub adjust_overline_position: Option<Modifier>,
|
||||
pub adjust_overline_thickness: Option<Modifier>,
|
||||
pub adjust_cursor_thickness: Option<Modifier>,
|
||||
pub adjust_cursor_height: Option<Modifier>,
|
||||
pub adjust_box_thickness: Option<Modifier>,
|
||||
pub adjust_icon_height: Option<Modifier>,
|
||||
pub freetype_load_flags: FreetypeLoadFlags,
|
||||
}
|
||||
|
||||
impl DerivedConfig {
|
||||
/// Initialize a DerivedConfig from a config. (Cloning a DerivedConfig
|
||||
/// is `Clone::clone`.)
|
||||
pub fn new(config: &Config) -> DerivedConfig {
|
||||
DerivedConfig {
|
||||
font_family: config.font_family.clone(),
|
||||
font_family_bold: config.font_family_bold.clone(),
|
||||
font_family_italic: config.font_family_italic.clone(),
|
||||
font_family_bold_italic: config.font_family_bold_italic.clone(),
|
||||
font_style: config.font_style.clone(),
|
||||
font_style_bold: config.font_style_bold.clone(),
|
||||
font_style_italic: config.font_style_italic.clone(),
|
||||
font_style_bold_italic: config.font_style_bold_italic.clone(),
|
||||
font_variation: config.font_variation.clone(),
|
||||
font_variation_bold: config.font_variation_bold.clone(),
|
||||
font_variation_italic: config.font_variation_italic.clone(),
|
||||
font_variation_bold_italic: config.font_variation_bold_italic.clone(),
|
||||
font_codepoint_map: config.font_codepoint_map.clone(),
|
||||
font_synthetic_style: config.font_synthetic_style,
|
||||
adjust_cell_width: config.adjust_cell_width,
|
||||
adjust_cell_height: config.adjust_cell_height,
|
||||
adjust_font_baseline: config.adjust_font_baseline,
|
||||
adjust_underline_position: config.adjust_underline_position,
|
||||
adjust_underline_thickness: config.adjust_underline_thickness,
|
||||
adjust_strikethrough_position: config.adjust_strikethrough_position,
|
||||
adjust_strikethrough_thickness: config.adjust_strikethrough_thickness,
|
||||
adjust_overline_position: config.adjust_overline_position,
|
||||
adjust_overline_thickness: config.adjust_overline_thickness,
|
||||
adjust_cursor_thickness: config.adjust_cursor_thickness,
|
||||
adjust_cursor_height: config.adjust_cursor_height,
|
||||
adjust_box_thickness: config.adjust_box_thickness,
|
||||
adjust_icon_height: config.adjust_icon_height,
|
||||
freetype_load_flags: config.freetype_load_flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The key used to uniquely identify a font configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Key {
|
||||
/// The descriptors used for all the fonts added to the
|
||||
/// initial group, including all styles. This is hashed
|
||||
/// in order so the order matters. All users of the struct
|
||||
/// should ensure that the order is consistent.
|
||||
pub descriptors: Vec<Descriptor>,
|
||||
|
||||
/// These are the offsets into the descriptors array for
|
||||
/// each style. For example, bold is from
|
||||
/// offsets[Style::Bold as usize - 1] to
|
||||
/// offsets[Style::Bold as usize].
|
||||
pub style_offsets: [usize; 4],
|
||||
|
||||
/// The codepoint map configuration.
|
||||
pub codepoint_map: CodepointMap,
|
||||
|
||||
/// The metric modifier set configuration.
|
||||
pub metric_modifiers: ModifierSet,
|
||||
|
||||
/// The configured font size for this key. We don't use this
|
||||
/// directly but it is used as part of the hash for the
|
||||
/// font grid.
|
||||
pub font_size: DesiredSize,
|
||||
|
||||
/// The freetype load flags configuration.
|
||||
pub freetype_load_flags: FreetypeLoadFlags,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn new(config: &DerivedConfig, font_size: DesiredSize) -> Key {
|
||||
let mut descriptors: Vec<Descriptor> = Vec::new();
|
||||
for family in &config.font_family.list {
|
||||
descriptors.push(Descriptor {
|
||||
family: Some(family.clone()),
|
||||
style: config.font_style.name_value().map(str::to_string),
|
||||
size: font_size.points,
|
||||
variations: config.font_variation.list.clone(),
|
||||
..Descriptor::default()
|
||||
});
|
||||
}
|
||||
|
||||
// In all the styled cases below, we prefer to specify an exact
|
||||
// style via the `font-style` configuration. If a style is not
|
||||
// specified, we use the discovery mechanism to search for a
|
||||
// style category such as bold, italic, etc. We can't specify both
|
||||
// because the latter will restrict the search to only that. If
|
||||
// a user says `font-style = italic` for the bold face for example,
|
||||
// no results would be found if we restrict to ALSO searching for
|
||||
// italic.
|
||||
for family in &config.font_family_bold.list {
|
||||
let style = config.font_style_bold.name_value().map(str::to_string);
|
||||
descriptors.push(Descriptor {
|
||||
family: Some(family.clone()),
|
||||
bold: style.is_none(),
|
||||
style,
|
||||
size: font_size.points,
|
||||
variations: config.font_variation_bold.list.clone(),
|
||||
..Descriptor::default()
|
||||
});
|
||||
}
|
||||
for family in &config.font_family_italic.list {
|
||||
let style = config.font_style_italic.name_value().map(str::to_string);
|
||||
descriptors.push(Descriptor {
|
||||
family: Some(family.clone()),
|
||||
italic: style.is_none(),
|
||||
style,
|
||||
size: font_size.points,
|
||||
variations: config.font_variation_italic.list.clone(),
|
||||
..Descriptor::default()
|
||||
});
|
||||
}
|
||||
for family in &config.font_family_bold_italic.list {
|
||||
let style = config
|
||||
.font_style_bold_italic
|
||||
.name_value()
|
||||
.map(str::to_string);
|
||||
descriptors.push(Descriptor {
|
||||
family: Some(family.clone()),
|
||||
bold: style.is_none(),
|
||||
italic: style.is_none(),
|
||||
style,
|
||||
size: font_size.points,
|
||||
variations: config.font_variation_bold_italic.list.clone(),
|
||||
..Descriptor::default()
|
||||
});
|
||||
}
|
||||
|
||||
// Setup the codepoint map
|
||||
let codepoint_map = config.font_codepoint_map.map.clone();
|
||||
|
||||
// Metric modifiers
|
||||
let mut metric_modifiers = ModifierSet::new();
|
||||
for (key, m) in [
|
||||
(MetricsKey::CellWidth, config.adjust_cell_width),
|
||||
(MetricsKey::CellHeight, config.adjust_cell_height),
|
||||
(MetricsKey::CellBaseline, config.adjust_font_baseline),
|
||||
(
|
||||
MetricsKey::UnderlinePosition,
|
||||
config.adjust_underline_position,
|
||||
),
|
||||
(
|
||||
MetricsKey::UnderlineThickness,
|
||||
config.adjust_underline_thickness,
|
||||
),
|
||||
(
|
||||
MetricsKey::StrikethroughPosition,
|
||||
config.adjust_strikethrough_position,
|
||||
),
|
||||
(
|
||||
MetricsKey::StrikethroughThickness,
|
||||
config.adjust_strikethrough_thickness,
|
||||
),
|
||||
(
|
||||
MetricsKey::OverlinePosition,
|
||||
config.adjust_overline_position,
|
||||
),
|
||||
(
|
||||
MetricsKey::OverlineThickness,
|
||||
config.adjust_overline_thickness,
|
||||
),
|
||||
(MetricsKey::CursorThickness, config.adjust_cursor_thickness),
|
||||
(MetricsKey::CursorHeight, config.adjust_cursor_height),
|
||||
(MetricsKey::BoxThickness, config.adjust_box_thickness),
|
||||
(MetricsKey::IconHeight, config.adjust_icon_height),
|
||||
] {
|
||||
if let Some(m) = m {
|
||||
metric_modifiers.insert(key, m);
|
||||
}
|
||||
}
|
||||
|
||||
let regular_offset = config.font_family.list.len();
|
||||
let bold_offset = regular_offset + config.font_family_bold.list.len();
|
||||
let italic_offset = bold_offset + config.font_family_italic.list.len();
|
||||
let bold_italic_offset = italic_offset + config.font_family_bold_italic.list.len();
|
||||
|
||||
Key {
|
||||
descriptors,
|
||||
style_offsets: [
|
||||
regular_offset,
|
||||
bold_offset,
|
||||
italic_offset,
|
||||
bold_italic_offset,
|
||||
],
|
||||
codepoint_map,
|
||||
metric_modifiers,
|
||||
font_size,
|
||||
freetype_load_flags: config.freetype_load_flags,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the descriptors for the given font style that can be
|
||||
/// used with discovery.
|
||||
pub fn descriptors_for_style(&self, style: Style) -> &[Descriptor] {
|
||||
let idx = style as usize;
|
||||
let start = if idx == 0 {
|
||||
0
|
||||
} else {
|
||||
self.style_offsets[idx - 1]
|
||||
};
|
||||
let end = self.style_offsets[idx];
|
||||
&self.descriptors[start..end]
|
||||
}
|
||||
|
||||
/// Hash the key with the given hasher.
|
||||
pub fn hash_into<H: Hasher>(&self, hasher: &mut H) {
|
||||
self.font_size.points.to_bits().hash(hasher);
|
||||
self.font_size.xdpi.hash(hasher);
|
||||
self.font_size.ydpi.hash(hasher);
|
||||
self.descriptors.len().hash(hasher);
|
||||
for d in &self.descriptors {
|
||||
d.hash(hasher);
|
||||
}
|
||||
self.codepoint_map.hashcode().hash(hasher);
|
||||
self.metric_modifiers.len().hash(hasher);
|
||||
self.freetype_load_flags.hash(hasher);
|
||||
if !self.metric_modifiers.is_empty() {
|
||||
for key in MetricsKey::ALL {
|
||||
if let Some(value) = self.metric_modifiers.get(&key) {
|
||||
key.hash(hasher);
|
||||
// Modifier::hash: tag, then the value (floats are
|
||||
// bit-cast, as in Zig).
|
||||
match *value {
|
||||
Modifier::Percent(v) => {
|
||||
0u8.hash(hasher);
|
||||
v.to_bits().hash(hasher);
|
||||
}
|
||||
Modifier::Absolute(v) => {
|
||||
1u8.hash(hasher);
|
||||
v.hash(hasher);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a hash code that can be used to uniquely identify this
|
||||
/// key.
|
||||
pub fn hashcode(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.hash_into(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// As in Zig's `Map` context, key equality is hashcode equality.
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.hashcode() == other.hashcode()
|
||||
}
|
||||
}
|
||||
impl Eq for Key {}
|
||||
impl Hash for Key {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.hashcode().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn keycfg() -> DerivedConfig {
|
||||
DerivedConfig::new(&Config::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key() {
|
||||
let keycfg = keycfg();
|
||||
let k = Key::new(&keycfg, DesiredSize::new(12.0));
|
||||
let k2 = Key::new(&keycfg, DesiredSize::new(12.0));
|
||||
assert!(k.hashcode() > 0);
|
||||
assert_eq!(k.hashcode(), k2.hashcode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_different_font_points() {
|
||||
let keycfg = keycfg();
|
||||
let k = Key::new(&keycfg, DesiredSize::new(12.0));
|
||||
let k2 = Key::new(&keycfg, DesiredSize::new(16.0));
|
||||
assert_ne!(k.hashcode(), k2.hashcode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_different_font_dpi() {
|
||||
let keycfg = keycfg();
|
||||
let mut a = DesiredSize::new(12.0);
|
||||
a.xdpi = 1;
|
||||
let mut b = DesiredSize::new(12.0);
|
||||
b.xdpi = 2;
|
||||
let k = Key::new(&keycfg, a);
|
||||
let k2 = Key::new(&keycfg, b);
|
||||
assert_ne!(k.hashcode(), k2.hashcode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_descriptors_and_modifiers() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.font_family.list = vec!["A".into(), "B".into()];
|
||||
cfg.font_family_bold.list = vec!["C".into()];
|
||||
cfg.adjust_cell_width = Some(Modifier::Percent(1.1));
|
||||
let d = DerivedConfig::new(&cfg);
|
||||
let k = Key::new(&d, DesiredSize::new(12.0));
|
||||
assert_eq!(k.style_offsets, [2, 3, 3, 3]);
|
||||
assert_eq!(k.descriptors_for_style(Style::Regular).len(), 2);
|
||||
let bold = k.descriptors_for_style(Style::Bold);
|
||||
assert_eq!(bold.len(), 1);
|
||||
assert!(bold[0].bold);
|
||||
assert_eq!(k.metric_modifiers.len(), 1);
|
||||
let plain = Key::new(&keycfg(), DesiredSize::new(12.0));
|
||||
assert_ne!(k.hashcode(), plain.hashcode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_grid_set() {
|
||||
let set = SharedGridSet::new().unwrap();
|
||||
let keycfg = keycfg();
|
||||
|
||||
// Get a grid for the given config
|
||||
let (key1, grid1) = set.ref_(&keycfg, DesiredSize::new(12.0)).unwrap();
|
||||
assert_eq!(1, set.count());
|
||||
|
||||
// Get another
|
||||
let (key2, grid2) = set.ref_(&keycfg, DesiredSize::new(12.0)).unwrap();
|
||||
assert_eq!(1, set.count());
|
||||
|
||||
// They should be pointer equivalent
|
||||
assert!(Arc::ptr_eq(&grid1, &grid2));
|
||||
|
||||
// If I deref grid2 then we should still have a count of 1
|
||||
set.deref(&key2);
|
||||
assert_eq!(1, set.count());
|
||||
|
||||
// If I deref grid1 then we should have a count of 0
|
||||
set.deref(&key1);
|
||||
assert_eq!(0, set.count());
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,10 @@ Collected from agent reports; resolve during integration phases.
|
||||
- [ ] os `locale::ensure_locale` no-op → call setlocale from apprt-gtk (unsafe allowed) at startup, as Ghostty's global.zig does.
|
||||
- [ ] os `passwd` Flatpak branch needs termio Pty.
|
||||
- [ ] os `open::OpenUrlKind` → replace with core apprt action type.
|
||||
- [ ] font `sprite/face.rs`: local `Presentation`/`RenderOptions` stand-ins → real font types (Phase 3).
|
||||
- [ ] font crate: re-add deps on config/unicode/terminal in Phase 3.
|
||||
- [ ] terminal crate: re-add deps on unicode/datastruct once those land.
|
||||
- [ ] input: key_encode::Options / paste::Options from terminal modes (Zig `fromTerminal`) — in terminal or core.
|
||||
- [ ] input: `binding::set::EntryFormatter` must be implemented by config formatter.
|
||||
- [ ] config: font_types.rs (Variation, CodepointMap, Descriptor), clipboard_codepoint_map::Entry, CursorStyle are local copies → unify with font/terminal.
|
||||
- [ ] config: clipboard_codepoint_map::Entry and CursorStyle are local copies → unify with terminal/formatter (font types are unified: canonical in ghostty-config, re-exported by ghostty-font).
|
||||
- [ ] config: url::REGEX needs onig (lookbehind) in renderer/core link matching.
|
||||
- [ ] config: theme::open needs resources dir from ghostty_os.
|
||||
- NOTE: Ghostty 1.3.1 is installed at /usr/bin/ghostty — use it as reference oracle (e.g. +show-config, +list-actions, terminfo).
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# ghostty-font — F2 (config integration, run iterator, SharedGridSet, terminal-dependent shaper tests)
|
||||
|
||||
## Done
|
||||
* Deps `ghostty-config`, `ghostty-terminal` re-added (+ dev-dep `ghostty-vt` for SGR/Rgb in tests).
|
||||
* Type unification (canonical definitions live in `ghostty-config`, font re-exports):
|
||||
* `face::{Variation, VariationId}` = `ghostty_config::font_types` (`VariationId(u32)`; `VariationId::init(b"wght")`, `.0`, `.str()`).
|
||||
* `discovery::Descriptor` = config's; `Descriptor::toFcPattern` is now the free fn `discovery::to_fc_pattern(&Descriptor)`.
|
||||
* `codepoint_map::{CodepointMap, Entry}` = config's `CodepointMap`/`CodepointMapEntry` (`range: [u32; 2]`).
|
||||
* `metrics::Modifier` = `ghostty_config::metric_modifier::MetricModifier` (type alias; `ModifierParseError` and `Modifier::format_entry/parse_cli` removed, use `parse`/`parse_cli_value` and the config `ConfigField` impl). `ModifierSet`/`Key` stay in font (+ new `Key::ALL`). Config's `apply_i32` out-of-range now yields `-i32::MAX` like Zig (only config edit besides doc comment in `font_types.rs`).
|
||||
* `config_types.rs` is now only re-exports (`FreetypeLoadFlags`, `FontSyntheticStyle`, `FontShapingBreak`, `FontStyle`, `GraphemeWidthMethod`); `face::FreetypeLoadFlags` re-exports config's.
|
||||
* INTEGRATION_TODO: removed the three font lines; config line narrowed to clipboard map / CursorStyle.
|
||||
* `shaper/run.rs` (run.zig): `TextRun`, `RunCell`, `RunOptions`, `RunIteratorHook` trait (impl for harfbuzz + noop shapers), `RunIterator`. Full logic ported: invisible-cell skipping, right trim, selection/cursor splitting, spacer skipping, "fl/fi/st" ligature splits, style change splitting (bg ignored), font-style/presentation selection, grapheme font search (`index_for_cell`), U+FFFD / space fallback, Kitty placeholder, relative-cluster position-independent hash. `shape.rs` re-exports `TextRun/RunIterator/RunOptions/RunIteratorHook`.
|
||||
* `shared_grid_set.rs` (SharedGridSet.zig): `SharedGridSet` (`new`, `ref_` (Zig `ref`), `deref(&Key)`, `count`), `DerivedConfig::new(&Config)`, `Key` (descriptors, style offsets, codepoint map, metric modifiers, font size, freetype flags; hashcode-equality like the Zig map), collection building incl. discovery, variation-retry, built-in JetBrains Mono / Nerd symbols / emoji fallbacks. Grids are `Arc<SharedGrid>`; the set drops its Arc at ref-count 0. `discovery::Fontconfig` is now `Clone` (Arc inside) so the resolver shares the set's discoverer.
|
||||
* `harfbuzz::Shaper::shape_run(&SharedGrid, &TextRun)` = Zig `shape(run)` (takes the grid write lock to get the face).
|
||||
|
||||
## Tests
|
||||
* SharedGridSet.zig: 5/5 (Key, Key different font points, Key different font DPI, SharedGridSet + extra `key_descriptors_and_modifiers`).
|
||||
* All 23 deferred harfbuzz tests ported (in `shaper/harfbuzz.rs`), plus F1's terminal-free ones kept. Font crate: 157 pass, 3 ignored. Ignored (always `error.SkipZigTest` upstream, bodies ported from the commented-out Zig): `shape_tai_tham_vowels_position_differs_from_advance`, `shape_tai_tham_letters_run_offset_y_differs_from_zero`, `shape_javanese_ligatures`.
|
||||
* Tests needing a discovered system font (Devanagari + Bengali = "Arial Unicode MS", Chakma = "Noto Sans Chakma", Tibetan = "Noto Serif Tibetan") return early (Zig `SkipZigTest`) when the font is missing; only Tibetan is installed on this machine (verified running); Devanagari/Chakma/Bengali are therefore not exercised here.
|
||||
* Removed duplicate tests that moved to config: F1's `variation_id_*` in face.rs and `formatconfig_*` in metrics.rs (still covered by ghostty-config tests); adapted `modifier_parse_errors`.
|
||||
|
||||
## Deviations / TODO(integration)
|
||||
* `Terminal`/`RenderState` do not exist (usable) in ghostty-terminal yet, so the tests use a small `Term` helper over `Screen` (`shaper/test_util.rs`): `test_write_string` for text, own grapheme-cluster attach via `ghostty_unicode::grapheme_break` for tests that enable `grapheme_cluster` mode, `Screen::set_attribute` for SGR, `cursor_right` for `\e[5C`. `RunCell::from_page_row(page, y)` replaces `RenderState` cells (`TODO(integration)`: switch to the terminal render-state cell type). Once `Terminal` is final the tests can be rewritten to drive it with the VT stream.
|
||||
* `TextRun` has no `grid` field (keeps it `Copy`); the iterator's `RunOptions.grid` is `&SharedGrid`. `RunIterator::next(&mut shaper)` takes the shaper (hooks) per call instead of storing it, so the shaper can shape between calls.
|
||||
* Run hash uses std `DefaultHasher` (Zig: Wyhash) — only used as a cache key.
|
||||
* `KITTY_PLACEHOLDER` (0x10EEEE) is a local const (TODO(integration): terminal kitty graphics unicode module).
|
||||
* macOS Apple Emoji branch in the collection builder is omitted (Linux only); discovery is always available.
|
||||
* `SharedGridSet::ref_` holds the set lock while building the collection (as Zig).
|
||||
* `shared_grid::CellSize` still stands in for `renderer::CellSize`.
|
||||
* Quality gate: `cargo build/test` ok; clippy with `-D warnings` for ghostty-font passes with `--no-deps` (the dependency ghostty-terminal currently fails clippy in its in-progress `terminal.rs`, not my crate); fmt applied.
|
||||
Reference in new issue
Block a user