mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Integrate harfrust for text shaping (#8031)
* Related to #56 (Improve text — tracking issue) ## Summary This PR integrates [harfrust](https://crates.io/crates/harfrust) (a pure-Rust port of HarfBuzz) into epaint's text layout pipeline, replacing the character-by-character glyph positioning with proper OpenType text shaping. ### What this enables - **GPOS kerning**: most modern fonts only ship kerning in GPOS tables (not the legacy `kern` table). Pairs like "AV", "VA", "AT" are now properly tightened. - **GSUB substitutions**: ligatures (fi, fl), contextual alternates, and other OpenType features. - **Combining marks**: diacritics (e.g. ɔ̃) are positioned via anchor tables instead of being rendered as standalone replacement glyphs. ### Before/After #### Kerning, etc. <img width="838" height="726" alt="before_main" src="https://github.com/user-attachments/assets/f0f26d5f-b117-43a6-b39c-ea40d2e73836" /> <img width="838" height="726" alt="after_harfrust" src="https://github.com/user-attachments/assets/d983e5da-486c-4f39-bd4f-5782a90c6b39" /> #### Ligatures <img width="1117" height="698" alt="before_closeup" src="https://github.com/user-attachments/assets/7a3b08b4-cf6f-45b7-98ba-07c473cd3b02" /> <img width="1117" height="698" alt="after_closeup" src="https://github.com/user-attachments/assets/6cfc5f21-d32f-4f09-be0c-59c8c553d44f" /> ### Architecture The shaping integrates into the existing pipeline without changing the public API: 1. **`Font::segment_into_runs`** — segments text into contiguous runs by font face (grapheme-cluster aware, never splits combining sequences) 2. **`FontFace::shape_text`** — calls harfrust to shape each run, returning glyph IDs + positioned advances/offsets 3. **`layout_shaped_run`** — emits `Glyph` structs from the shaping output, with NOTDEF fallback to other font faces for missing glyphs 4. **Buffer recycling** — `FontsImpl` pools a `harfrust::UnicodeBuffer` to avoid per-layout allocations ### Disclaimer I'm far from being a good Rust programmer. Claude Code did most of the heavy lifting here. I did my best and used my limited knowledge to avoid making too many mistakes. If this PR isn't up to quality standards, please don't hesitate to close it. ## Test plan - [x] `cargo test -p epaint` — all 18 text tests pass, including 6 new ones - [x] `cargo clippy -p epaint --all-features` — clean - [x] `cargo fmt` — clean - [ ] Snapshot tests need regeneration (expected: shaping changes glyph positions) - New tests added: - `test_gpos_kerning` — verifies GPOS kerning tightens "AV", "VA", "AT" pairs - `test_combining_diacritics` — combining tilde doesn't add extra width - `test_shaping_basic_latin` — sanity check for Latin text - `test_shaping_empty_string` — empty input doesn't panic - `test_shaping_multiple_newlines` — newline splitting works correctly - `test_shaping_mixed_font_fallback` — Latin + emoji in same string --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
62
Cargo.lock
62
Cargo.lock
@@ -1540,6 +1540,7 @@ dependencies = [
|
||||
"emath",
|
||||
"epaint_default_fonts",
|
||||
"font-types",
|
||||
"harfrust",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"nohash-hasher",
|
||||
@@ -1551,6 +1552,8 @@ dependencies = [
|
||||
"similar-asserts",
|
||||
"skrifa",
|
||||
"smallvec",
|
||||
"unicode-general-category",
|
||||
"unicode-segmentation",
|
||||
"vello_cpu",
|
||||
]
|
||||
|
||||
@@ -1658,12 +1661,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fearless_simd"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a"
|
||||
|
||||
[[package]]
|
||||
name = "file_dialog"
|
||||
@@ -2018,6 +2018,15 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guillotiere"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
|
||||
dependencies = [
|
||||
"euclid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.6.0"
|
||||
@@ -2029,6 +2038,19 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"read-fonts",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
@@ -3732,6 +3754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"font-types",
|
||||
]
|
||||
|
||||
@@ -4732,6 +4755,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -4899,28 +4928,41 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vello_common"
|
||||
version = "0.0.6"
|
||||
name = "vello_api"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b"
|
||||
checksum = "a5088cd0113bc5332c753f24503825e3bc93e26c7883c9dc3ad9637bb62c4634"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"peniko",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vello_common"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "986dc49a501a683477614bf07b8e7b6c79ae4828efce3bf22e51850f4a0a8a4c"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fearless_simd",
|
||||
"guillotiere",
|
||||
"hashbrown 0.16.1",
|
||||
"log",
|
||||
"peniko",
|
||||
"skrifa",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vello_cpu"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc"
|
||||
checksum = "a678f91c7524a3a9ac9a19df9f83552866ec70b2ca26441b916a6b219b6aa2de"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"hashbrown 0.16.1",
|
||||
"vello_api",
|
||||
"vello_common",
|
||||
]
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ font-types = { version = "0.11.0", default-features = false, features = ["std"]
|
||||
glow = "0.17.0"
|
||||
glutin = { version = "0.32.3", default-features = false }
|
||||
glutin-winit = { version = "0.5.0", default-features = false }
|
||||
harfrust = "0.5.2"
|
||||
home = "0.5.9"
|
||||
image = { version = "0.25.6", default-features = false }
|
||||
jiff = { version = "0.2.23", default-features = false }
|
||||
@@ -136,8 +137,9 @@ tokio = "1.49"
|
||||
toml = {version = "1.0.0", default-features = false }
|
||||
type-map = "0.5.1"
|
||||
unicode_names2 = { version = "2.0.0", default-features = false }
|
||||
unicode-general-category = "1.1.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
|
||||
vello_cpu = { version = "0.0.7", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
|
||||
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
|
||||
wasm-bindgen-futures = "0.4.58"
|
||||
wayland-cursor = { version = "0.31.11", default-features = false }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63021012cccfca02d09aa424333453140ae4da3ae58fa32b422f6152ba25741c
|
||||
size 335394
|
||||
oid sha256:288e11a1fa684575155826a760d5aecc5855e1f4b68bc8954441bf3ac015ee84
|
||||
size 335175
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4470063fe210d2e5170d6609c2603fff1984b8ee76fb65a1f60a1c4cfdf46ce8
|
||||
size 92796
|
||||
oid sha256:d674918c635bfc865043f2123c0f5d4a671dd21ba7b878c056e817b19f2e8f00
|
||||
size 92770
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84f0e72ce337d56f3767ebed1ab6a47f3d27c9fbcce4d8a19aeab358e12920f5
|
||||
size 169664
|
||||
oid sha256:fa67354cfe4d9cb8774c8de614be5975853a4a817ceaf96c3ec7a968de638d8d
|
||||
size 169740
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6030f2f3da3dbbdf8bf3eaf429f222acffb624c7696b654d8b6e64273d49be58
|
||||
size 99008
|
||||
oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390
|
||||
size 98934
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bff16d453b960bb9abcdd9f72dab73f8f25da6339a0e1c310ed352f57080db93
|
||||
size 32426
|
||||
oid sha256:10d64e017d1d0eba736a4471d28b1602a0cb69d8e2ab53f4ee604b01c9343116
|
||||
size 32475
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a
|
||||
size 27067
|
||||
oid sha256:ab0d730ce0cf5f1d79947601def4f60c0a015e23a5dcd780df65c7ddc7ae7156
|
||||
size 27194
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7
|
||||
size 26887
|
||||
oid sha256:6fccbda741a056ae30a7bfd7497f7b0adfb337bdb885d247fbdfef43cc24b54c
|
||||
size 26948
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12
|
||||
size 76531
|
||||
oid sha256:26d247655398bae33c724ad3c3bdcab330d194093b07442708d5069e256f636b
|
||||
size 76542
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0
|
||||
size 62604
|
||||
oid sha256:e3389491f9f5c54cfc2e295abe76ad53f223c31e9489e1d07a8323cf14fcf37d
|
||||
size 62628
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e
|
||||
size 27818
|
||||
oid sha256:6e42e801758bb9e6130a4e94bd0857d6f254e68a7dedebec5af5cb7f7d896068
|
||||
size 27822
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d194db1705b36c1ee6189878a323a591555493c9319b7c2ddfc0cb0541055aca
|
||||
size 20948
|
||||
oid sha256:41e68fd3a679e2a6e3dd81129de5aa1ded3a8f24cc7b1f2ba0b876240d309d9b
|
||||
size 21019
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62af1b6d248b731b0930c51e481118ae01a5d8c08f27697ff121a47fefea83bf
|
||||
size 10795
|
||||
oid sha256:3db502ec416b322e0f98e9737faa52d5d2fd308d47649710fffa0b0bc5996f52
|
||||
size 10783
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:deff441cd1d9142352f8759dff4b759f4572f0ddf93752349314da77abe4b254
|
||||
size 115028
|
||||
oid sha256:c39ae3420fe01d696a032d8d052c405c5623a9208fc673f5cf09188b1e6a539b
|
||||
size 115447
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc
|
||||
size 25057
|
||||
oid sha256:19322248e8335301b2ce31fc1f1352993a374dda945d227784fecea2ee831761
|
||||
size 25088
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6
|
||||
size 99297
|
||||
oid sha256:98f8865d866a6f28ae3e3a16c815770ed691a031b1d06f7d3662a7e94f564606
|
||||
size 99318
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e
|
||||
size 18281
|
||||
oid sha256:39208c3c2c95f68fb37880b011f866bedd8dbccee33d163a725cb2a5cc6bb1b3
|
||||
size 18290
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105
|
||||
size 113803
|
||||
oid sha256:d251ffd6c34e4ffa7b5de7fe5ae57a2d8f48ba7c2e03711da2c3f1a3fd84648c
|
||||
size 113998
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72d49d9a35e16e7413158ba14ce9cab762925f5f5e52fbbe16292de499a177f1
|
||||
size 25791
|
||||
oid sha256:4d959e17ee5c8a32534ab98b6f08db884b2fbf25476d8dc2dc90edc79bd87ea4
|
||||
size 25821
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb7e3887cf12bd00ea09b297ac361562a961f64c15b28360bc87f72f270a4065
|
||||
size 51649
|
||||
oid sha256:c923f523cc77d678929f294b360f60b9f546ddec66d5317ad0eb44bd61a5f927
|
||||
size 51733
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30
|
||||
size 22563
|
||||
oid sha256:c9c98d7bfa08e22e217dd9e7031cc49e4b4486f1a9fdd223bd122be07af72365
|
||||
size 22550
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439
|
||||
size 47281
|
||||
oid sha256:68afa93605427e12fe527f3ca9613095664b4983f1f585a60f14bc2370c0a1f2
|
||||
size 47224
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f
|
||||
size 23148
|
||||
oid sha256:33959851f1bd2386fcca8fb5700133d14d90db5a8f783fbfaa9f3aebb7b0d5b2
|
||||
size 23119
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675
|
||||
size 65308
|
||||
oid sha256:7b918565d66594fe53ed62bb14e357d3489335f3d037ccb088f198d944eb367d
|
||||
size 65307
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959
|
||||
size 33469
|
||||
oid sha256:f8c3c2912a3a11892e65f94792c77c79400a81d8c913109d39e8ce12f5b095c6
|
||||
size 33503
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:115398e2b4b459afc2f1c49c4e60ef3f63e562650f05581372002c93030da632
|
||||
size 38374
|
||||
oid sha256:0c2c7b0d4913b59ef932f6d6349ddab5cc8619b2e8e9a8b5eb3e055a62e6ed60
|
||||
size 38382
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791
|
||||
size 18242
|
||||
oid sha256:9567f56f2dd030608798347e1ab755d95730ba5c5dd1721f1c61147be7216e87
|
||||
size 18304
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a46457b23b7b32694564d03b42bccac2f017a756225bc54b508bb6fe2ad8ee7b
|
||||
size 249548
|
||||
oid sha256:d19d694e6a70ab6acb45668b391751e82f7beab19f9918e23821c667e8cc9cdb
|
||||
size 249733
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f
|
||||
size 57181
|
||||
oid sha256:9e1c73e28020371429b3e03d540f72dbf886fd40f0ba08bb194e868bbb3c95ff
|
||||
size 57230
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fadea24444c402695db6cbc9e03aef8a0ed3c5db487a324fb255d38c14f73dce
|
||||
size 19804
|
||||
oid sha256:c2facd3881e6b107a0dcce9d6e00008a3c9b0f31ad270c35357a87e487180f56
|
||||
size 19814
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6105c95470d1342f9003ab03e71243b5e18a6f225261aee94b15f8f0501572c
|
||||
size 33542
|
||||
oid sha256:0ad81eb762150360368a97858ef30bb0d5aff72e71743fa40c3fd4d70ec84cdc
|
||||
size 33400
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b
|
||||
size 23629
|
||||
oid sha256:c22130891755ac095d73e0494f11ee8e89d0fd0c31a321d3afb969648ece11ef
|
||||
size 23675
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:927a497e8b6f9ce3b71dcb67086f477e19d327c163b2b8ad868af10009c2faf2
|
||||
size 172981
|
||||
oid sha256:465f33e53ebe15b776e2d6d0710f13f2453f00bab1f6ef4319b3491f1d1d3a26
|
||||
size 173487
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d
|
||||
size 118562
|
||||
oid sha256:001f7a1310ddf37be4d9a7f56a95a3079f713b741824a348544561bb16c291fa
|
||||
size 118614
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d8d87e4cb944def0dc28e3515243a8c1b07b9b0f88e802924d4381c1cda74db
|
||||
size 26763
|
||||
oid sha256:8105f7c2b519716cc0a45dffd5f08980f53f35c6c6b788592e1d82506cdacccc
|
||||
size 26665
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db4c0cf1c4cdae3d416afce5c58efd1cc382be86431e547fa66bcc95a0a17ddb
|
||||
size 76364
|
||||
oid sha256:a9e83d5e83e3003830f7f719b02dd93273733a9b72d388aa42083387d02c1a20
|
||||
size 76310
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6
|
||||
size 70250
|
||||
oid sha256:3d46c87417c49c8462fac6c488e46b1482bbc75f70fe8f7af8391f0d5d28dac3
|
||||
size 70271
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e
|
||||
size 60947
|
||||
oid sha256:4c47faa75a002577220a92efceab5c0ccee1ddaa17c2d2f3d6d1429dbf8fe718
|
||||
size 60887
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3
|
||||
size 21814
|
||||
oid sha256:9c09529c3a1c26c8f28c00fc15cc5f495842862276870c24b5ee0713954f97fc
|
||||
size 21916
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57018beba5e4fb4f1e6de9c58bf898560b3a7669159d5bad91a4e2382ef57ce0
|
||||
size 64004
|
||||
oid sha256:9d69b9a12e777ee559a481aec012935a5bfb2ca8b0d48725ecd33e7f0880b2b8
|
||||
size 64273
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92
|
||||
size 13700
|
||||
oid sha256:f6c020860fe9cb7503cea548afbf298ebb9dc620133870b528fe7508d04150c8
|
||||
size 13691
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4
|
||||
size 35874
|
||||
oid sha256:d033935ca18ebee7e3c35629233cc3e3a73766ac8c0627fcdd8a12660eed703c
|
||||
size 35873
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:999f9cc006302b8951d97b510a02f1209969c376ecc7909ed5d7b46da27c0637
|
||||
size 483753
|
||||
oid sha256:77c9058b036770a644f1bfcf9ed1ba7a29a7c98107b1823474c55ccc2880e9e7
|
||||
size 485880
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c2990a81dfa8832f0cb1c4c0ce2f86e468a7a6f693e09efffa131ed3259e2e8
|
||||
size 15428
|
||||
oid sha256:2ef07080ca2aa10e128c646479b8b322a092d63f15b35c52ed59015e7c2a0f60
|
||||
size 15434
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07d987ff87c9f41ec71ceea0caff25795bf4dff525ef4ef241d0ba786acee3e1
|
||||
size 35960
|
||||
oid sha256:4edb61c6619d5e44892fa29da0aa0a624306ae637dbbaa057e3fa47c14dc06bd
|
||||
size 35988
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5c5ce0d46231d90ccb04e158947d793d99cd5cce911c72b960b6d04feba2134
|
||||
size 16122
|
||||
oid sha256:f8da0c2e37497968864a91fa6bef7c545c37791f4f9b788ea9a2f43dd4ac16b1
|
||||
size 16116
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60dcd590b1d00361278b135ce9ef084c7382875c71c72b19fb6e23dba68f7902
|
||||
size 39279
|
||||
oid sha256:2a603fcb2eb97943d6be3239b91cacee093adcb55a6dd14af93a72fc8b3a61fa
|
||||
size 39270
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7389e319d9153af313cc113d97b57d462da00feb0d5f99da211552af3ac7e18a
|
||||
size 6704
|
||||
oid sha256:19c9ae55906e0ad18a9507898884ec31ef785c498fb8d10267ee848bd3f17186
|
||||
size 6705
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4480dd34ed36c6bdbc2084843dd136448b3934c22b3df3e40314ba6324b5b39
|
||||
size 10306
|
||||
oid sha256:6f13116cb76e52620221b41c8dbd37b74a0da513402cbb2cb7e70c89027f5e31
|
||||
size 10313
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:624bfa884431c35cc5d852b96653f13da17e60f8545471f9fb1c3bb85b40ffc8
|
||||
size 16555
|
||||
oid sha256:6869b180f2d1dcfd9fcbb215ed3febfbe548f30035560272a4e11e465f20c592
|
||||
size 16567
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf665389ef43524e097a7ae4eec0aa01bb788bdbb306144f20f9133f74a64b2c
|
||||
size 6941
|
||||
oid sha256:132f060b2e20b4cc0c55062367381f47856a5cc10310b903dbddc3790f475581
|
||||
size 6942
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a2480d0f49a929993de70612572b321457b2507c149a25112064cfc27840e6ee
|
||||
size 11005
|
||||
oid sha256:e5ec63c5e2f5bb5c0048ee67df5b821a30f89ba35dbedf72e7bc91e04a5f8359
|
||||
size 11007
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e0013b499934f47370a3a20b3d3a19f8a8c6db360752a35a3fb1d676d122263
|
||||
size 18068
|
||||
oid sha256:491bb98b22cf56e50d3ed10a10649db15aaffb93d7e045dfecfa26e98f537c35
|
||||
size 18070
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6
|
||||
size 47076
|
||||
oid sha256:8bd53b56322123940496700cbd2a73e336fd80eaf49dcb19a958888d66570ddb
|
||||
size 47159
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3
|
||||
size 48209
|
||||
oid sha256:d15954e6183558141e05e1b566ec4a794d372503baf436da2a8c8c67299056f1
|
||||
size 48238
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967
|
||||
size 44295
|
||||
oid sha256:1738ecf660979888c70b1046dd759fe0b082e4958814fb19077c4fed2fb4bdef
|
||||
size 44338
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36
|
||||
size 44055
|
||||
oid sha256:dae7239dc065068147387eb313afdbaa3f0df8b81d060dbbc29f5a6a31ad76c8
|
||||
size 44309
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4
|
||||
size 588252
|
||||
oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
|
||||
size 590679
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28
|
||||
size 740616
|
||||
oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
|
||||
size 745771
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc
|
||||
size 967860
|
||||
oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
|
||||
size 973781
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9
|
||||
size 1076746
|
||||
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
|
||||
size 1081864
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169
|
||||
size 1125286
|
||||
oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
|
||||
size 1131030
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae
|
||||
size 1361407
|
||||
oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
|
||||
size 1363589
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260
|
||||
size 72501
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db
|
||||
size 45107
|
||||
oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7
|
||||
size 45061
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606
|
||||
size 86885
|
||||
oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
|
||||
size 86830
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44
|
||||
size 118961
|
||||
oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3
|
||||
size 118907
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f
|
||||
size 51419
|
||||
oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
|
||||
size 51376
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a
|
||||
size 54626
|
||||
oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
|
||||
size 54582
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085
|
||||
size 55090
|
||||
oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
|
||||
size 55053
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a
|
||||
size 36006
|
||||
oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8
|
||||
size 35960
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0
|
||||
size 35973
|
||||
oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
|
||||
size 35925
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a
|
||||
size 5276
|
||||
oid sha256:16e993bb27d32162fcf573cc4d977af775e6e0b94373fc5e8e1a9890453e508c
|
||||
size 5278
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79
|
||||
size 5301
|
||||
oid sha256:79676693e9f1804c7e0a32ffa9bec3bde281446ffc184b55a1a3fcf34074ac34
|
||||
size 5304
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ef245aae271ccae628bb4171f7e601194c77fd18888ef2ea829bea75bd38b0e5
|
||||
size 64965
|
||||
oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
|
||||
size 64973
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e621561567539ff24b4d22b53b65fac6cddae71d92fccd7800a90972a6de3e0e
|
||||
size 151100
|
||||
oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
|
||||
size 151090
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6c2d538be7971169bbc4473945e6815eac8c5dd6372bc1f1897a032b6bca12b
|
||||
size 59962
|
||||
oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
|
||||
size 59950
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d705af99624cd2824cd1f520fa05481ac67b8913feebae836db7b99ac60cb466
|
||||
size 145841
|
||||
oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
|
||||
size 145940
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00fb02e0cc2c1454d3a3dc0635be24086234c2bc5e2c9fd73741b179622e16d6
|
||||
size 4514
|
||||
oid sha256:698d1e9ecf490e04fb58ebf81cf9c633be29712abe172a6e6adfdc7fe442732d
|
||||
size 4517
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8757e2db9a3892d9347495ad59f14d2bd9164a9ba258375a53c9faf8176b597
|
||||
size 8016
|
||||
oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
|
||||
size 8028
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38ee4acc23d9c66f127d377ac8a0dd3b683a1465ca319fba092f6d3cdff8c266
|
||||
size 11166
|
||||
oid sha256:aac8c973f26d03c25155f0454185dc3ab27fffae1c63443e36f07c49fb0828bf
|
||||
size 11183
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ac1941f5eab71bfad020132eae47e1995efa17410b7861aa9f260032e5b0472c
|
||||
size 21785
|
||||
oid sha256:8dd0d91d0866848e7965e88223ff3ddae2231728197fbcd3fb5b1a6b034e179d
|
||||
size 21804
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b1f1a4dd9de1d8405c527c7f8f04b42ed9d403d0ec507bb3ff650a6896f28df0
|
||||
size 28628
|
||||
oid sha256:11d0081c272986e6fbd00cde05c0931321bc9670d6a0fa3a650323cd7b56d795
|
||||
size 28667
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af05a9b66340e0c128d823d3935a23bcf17cfeac02a822e7277234a9c8eb26e0
|
||||
size 33393
|
||||
oid sha256:6fc08e05c0ab0e167503324fb7c82777afd5866384ae8bbacb9b933f717feb56
|
||||
size 33412
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1dd1f5013587463f002b1becac1560876c462295dbe5dfbb1a9dbce58991e53d
|
||||
size 2209
|
||||
oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
|
||||
size 2205
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cfc03625c268f0ae067d2f4521a8668b47e4bc8525350d77a480840a09cd5083
|
||||
size 2046
|
||||
oid sha256:4ae52d6761a4d0b95b38ca4ca34c10751eef1ab2a54a62786c55bee3a3522ba1
|
||||
size 2050
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be0bd449166878ced27eff4966d1741731e926f9baabe8b590375c20103036dd
|
||||
size 5527
|
||||
oid sha256:7ddac0bff10e6699c0ab2d9ea3679d5047d6099b7ba3540d1ebc144361f07d86
|
||||
size 5517
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6154c8bb550575bcb9fa0bba06da4d47079a00dffc5754b62ef2a6e7529e2090
|
||||
size 7489
|
||||
oid sha256:59939d3347679ff27c1de500a559cdc0e6387f633c86ec7ce51f2a36cd92547b
|
||||
size 7511
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600
|
||||
size 2830
|
||||
oid sha256:db6f35e7dabe8f6d3766df022e5643f8461c3c2476959682622b793d59bf7c40
|
||||
size 2833
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600
|
||||
size 2830
|
||||
oid sha256:db6f35e7dabe8f6d3766df022e5643f8461c3c2476959682622b793d59bf7c40
|
||||
size 2833
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:037f3e356d32e1a2c32767460399f919452bff0933e1db7aa113e7e2bdb083f0
|
||||
size 4927
|
||||
oid sha256:d785e2073627f33a2e7a23a47e0e0b8ca9570f45d4de3d8b545b473c1a9e75b5
|
||||
size 4940
|
||||
|
||||
@@ -63,6 +63,7 @@ ecolor.workspace = true
|
||||
|
||||
ahash.workspace = true
|
||||
font-types.workspace = true
|
||||
harfrust.workspace = true
|
||||
log.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||
@@ -70,6 +71,8 @@ profiling.workspace = true
|
||||
self_cell.workspace = true
|
||||
skrifa.workspace = true
|
||||
smallvec.workspace = true
|
||||
unicode-general-category.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
vello_cpu.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
|
||||
use self_cell::self_cell;
|
||||
use skrifa::{
|
||||
MetadataProvider as _,
|
||||
raw::{TableProvider as _, tables::kern::SubtableKind},
|
||||
};
|
||||
use skrifa::{GlyphId, MetadataProvider as _};
|
||||
use std::collections::BTreeMap;
|
||||
use vello_cpu::{color, kurbo};
|
||||
|
||||
@@ -44,12 +41,10 @@ impl UvRect {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphInfo {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
///
|
||||
/// Is `None` for a special "invisible" glyph.
|
||||
pub(crate) id: Option<skrifa::GlyphId>,
|
||||
pub(crate) id: Option<GlyphId>,
|
||||
|
||||
/// In [`skrifa`]s "unscaled" coordinate system.
|
||||
pub advance_width_unscaled: OrderedFloat<f32>,
|
||||
@@ -124,17 +119,8 @@ impl SubpixelBin {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct GlyphAllocation {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
/// Use [`skrifa::GlyphId::NOTDEF`] if you just want to have an id, and don't care.
|
||||
pub(crate) id: skrifa::GlyphId,
|
||||
|
||||
/// Unit: screen pixels.
|
||||
pub advance_width_px: f32,
|
||||
|
||||
/// UV rectangle for drawing.
|
||||
pub uv_rect: UvRect,
|
||||
}
|
||||
@@ -145,7 +131,7 @@ struct GlyphCacheKey(u64);
|
||||
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
|
||||
|
||||
impl GlyphCacheKey {
|
||||
fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
fn new(glyph_id: GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
let StyledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
@@ -198,12 +184,10 @@ impl FontCell {
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: &GlyphInfo,
|
||||
glyph_id: GlyphId,
|
||||
bin: SubpixelBin,
|
||||
location: skrifa::instance::LocationRef<'_>,
|
||||
) -> Option<GlyphAllocation> {
|
||||
let glyph_id = glyph_info.id?;
|
||||
|
||||
debug_assert!(
|
||||
glyph_id != skrifa::GlyphId::NOTDEF,
|
||||
"Can't allocate glyph for id 0"
|
||||
@@ -288,11 +272,7 @@ impl FontCell {
|
||||
}
|
||||
};
|
||||
|
||||
Some(GlyphAllocation {
|
||||
id: glyph_id,
|
||||
advance_width_px: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
|
||||
uv_rect,
|
||||
})
|
||||
Some(GlyphAllocation { uv_rect })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +317,10 @@ pub struct FontFace {
|
||||
font: FontCell,
|
||||
tweak: FontTweak,
|
||||
|
||||
/// Cached `harfrust` shaper data (parsed GSUB/GPOS tables).
|
||||
/// `ShaperData` is `Copy` — lives outside the `self_cell`.
|
||||
shaper_data: harfrust::ShaperData,
|
||||
|
||||
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
|
||||
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
|
||||
}
|
||||
@@ -393,10 +377,13 @@ impl FontFace {
|
||||
})
|
||||
})?;
|
||||
|
||||
let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
font,
|
||||
tweak,
|
||||
shaper_data,
|
||||
glyph_info_cache: Default::default(),
|
||||
glyph_alloc_cache: Default::default(),
|
||||
})
|
||||
@@ -483,7 +470,7 @@ impl FontFace {
|
||||
let glyph_id = font_data
|
||||
.charmap
|
||||
.map(c)
|
||||
.filter(|id| *id != skrifa::GlyphId::NOTDEF)?;
|
||||
.filter(|id| *id != GlyphId::NOTDEF)?;
|
||||
|
||||
let glyph_info = GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
@@ -497,38 +484,6 @@ impl FontFace {
|
||||
Some(glyph_info)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn pair_kerning_pixels(
|
||||
&self,
|
||||
metrics: &StyledMetrics,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
let skrifa_font = &self.font.borrow_dependent().skrifa;
|
||||
let Ok(kern) = skrifa_font.kern() else {
|
||||
return 0.0;
|
||||
};
|
||||
kern.subtables()
|
||||
.find_map(|st| match st.ok()?.kind().ok()? {
|
||||
SubtableKind::Format0(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
|
||||
SubtableKind::Format1(_) => None,
|
||||
SubtableKind::Format2(subtable2) => subtable2.kerning(last_glyph_id, glyph_id),
|
||||
SubtableKind::Format3(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
|
||||
})
|
||||
.unwrap_or_default() as f32
|
||||
* metrics.px_scale_factor
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pair_kerning(
|
||||
&self,
|
||||
metrics: &StyledMetrics,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn styled_metrics(
|
||||
&self,
|
||||
@@ -571,51 +526,67 @@ impl FontFace {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn skrifa_font_ref(&self) -> &skrifa::FontRef<'_> {
|
||||
&self.font.borrow_dependent().skrifa
|
||||
}
|
||||
|
||||
pub(crate) fn tweak(&self) -> &FontTweak {
|
||||
&self.tweak
|
||||
}
|
||||
|
||||
pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
|
||||
&self.shaper_data
|
||||
}
|
||||
|
||||
pub fn allocate_glyph(
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: GlyphInfo,
|
||||
chr: char,
|
||||
h_pos: f32,
|
||||
shaped: &ShapedGlyph,
|
||||
) -> (GlyphAllocation, i32) {
|
||||
let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor;
|
||||
let ShapedGlyph {
|
||||
glyph_id,
|
||||
h_pos,
|
||||
is_cjk,
|
||||
} = *shaped;
|
||||
|
||||
let Some(glyph_id) = glyph_info.id else {
|
||||
// Invisible.
|
||||
return (GlyphAllocation::default(), h_pos as i32);
|
||||
};
|
||||
if glyph_id == GlyphId::NOTDEF {
|
||||
// invisible
|
||||
return (GlyphAllocation::default(), h_pos.round() as i32);
|
||||
}
|
||||
|
||||
// CJK scripts contain a lot of characters and could hog the glyph atlas if we stored 4 subpixel offsets per
|
||||
// glyph.
|
||||
let (h_pos_round, bin) = if is_cjk(chr) {
|
||||
let (h_pos_round, bin) = if is_cjk {
|
||||
// CJK scripts contain a lot of characters and could hog the glyph atlas
|
||||
// if we stored 4 subpixel offsets per glyph.
|
||||
(h_pos.round() as i32, SubpixelBin::Zero)
|
||||
} else {
|
||||
SubpixelBin::new(h_pos)
|
||||
};
|
||||
|
||||
let entry = match self
|
||||
.glyph_alloc_cache
|
||||
.entry(GlyphCacheKey::new(glyph_id, metrics, bin))
|
||||
{
|
||||
std::collections::hash_map::Entry::Occupied(glyph_alloc) => {
|
||||
let mut glyph_alloc = *glyph_alloc.get();
|
||||
glyph_alloc.advance_width_px = advance_width_px; // Hack to get `\t` and thin space to work, since they use the same glyph id as ` ` (space).
|
||||
return (glyph_alloc, h_pos_round);
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(entry) => entry,
|
||||
};
|
||||
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
|
||||
|
||||
let allocation = self
|
||||
.font
|
||||
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
|
||||
.unwrap_or_default();
|
||||
let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| {
|
||||
self.font
|
||||
.allocate_glyph_uncached(atlas, metrics, glyph_id, bin, (&metrics.location).into())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
entry.insert(allocation);
|
||||
(allocation, h_pos_round)
|
||||
(alloc, h_pos_round)
|
||||
}
|
||||
}
|
||||
|
||||
/// Positioning info for a single glyph, ready for atlas allocation.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ShapedGlyph {
|
||||
pub glyph_id: GlyphId,
|
||||
|
||||
/// Horizontal position of the glyph origin, in physical pixels.
|
||||
pub h_pos: f32,
|
||||
|
||||
/// CJK glyphs skip subpixel positioning to save atlas space.
|
||||
pub is_cjk: bool,
|
||||
}
|
||||
|
||||
// TODO(emilk): rename?
|
||||
/// Wrapper over multiple [`FontFace`] (e.g. a primary + fallbacks for emojis)
|
||||
pub struct Font<'a> {
|
||||
|
||||
@@ -765,6 +765,9 @@ pub struct FontsImpl {
|
||||
fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
fonts_by_name: ahash::HashMap<String, FontFaceKey>,
|
||||
family_cache: ahash::HashMap<FontFamily, CachedFamily>,
|
||||
|
||||
/// Recycled `harfrust` shaping buffer to avoid per-layout allocations.
|
||||
shape_buffer: Option<harfrust::UnicodeBuffer>,
|
||||
}
|
||||
|
||||
impl FontsImpl {
|
||||
@@ -798,6 +801,7 @@ impl FontsImpl {
|
||||
fonts_by_id,
|
||||
fonts_by_name,
|
||||
family_cache: Default::default(),
|
||||
shape_buffer: Some(harfrust::UnicodeBuffer::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,6 +809,16 @@ impl FontsImpl {
|
||||
self.atlas.options()
|
||||
}
|
||||
|
||||
/// Take the recycled shaping buffer (or create a new one if already taken).
|
||||
pub fn take_shape_buffer(&mut self) -> harfrust::UnicodeBuffer {
|
||||
self.shape_buffer.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Return a shaping buffer for reuse.
|
||||
pub fn return_shape_buffer(&mut self, buffer: harfrust::UnicodeBuffer) {
|
||||
self.shape_buffer = Some(buffer);
|
||||
}
|
||||
|
||||
/// Get the right font implementation from [`FontFamily`].
|
||||
pub fn font(&mut self, family: &FontFamily) -> Font<'_> {
|
||||
let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| {
|
||||
|
||||
@@ -8,15 +8,35 @@ use crate::{
|
||||
Color32, Mesh, Stroke, Vertex,
|
||||
stroke::PathStroke,
|
||||
text::{
|
||||
font::{StyledMetrics, is_cjk, is_cjk_break_allowed},
|
||||
TAB_SIZE,
|
||||
font::{StyledMetrics, UvRect, is_cjk, is_cjk_break_allowed},
|
||||
fonts::FontFaceKey,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals};
|
||||
use super::{
|
||||
FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals,
|
||||
VariationCoords,
|
||||
font::{Font, FontFace, ShapedGlyph},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Returns `true` if the character is a Unicode combining mark (categories Mn, Mc, Me).
|
||||
///
|
||||
/// These characters modify the preceding base character and should not be
|
||||
/// rendered as standalone replacement glyphs when the shaper can't handle them.
|
||||
#[inline]
|
||||
fn is_combining_mark(c: char) -> bool {
|
||||
use unicode_general_category::{GeneralCategory, get_general_category};
|
||||
matches!(
|
||||
get_general_category(c),
|
||||
GeneralCategory::NonspacingMark
|
||||
| GeneralCategory::SpacingMark
|
||||
| GeneralCategory::EnclosingMark
|
||||
)
|
||||
}
|
||||
|
||||
/// Represents GUI scale and convenience methods for rounding to pixels.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PointScale {
|
||||
@@ -98,15 +118,21 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>)
|
||||
// For most of this we ignore the y coordinate:
|
||||
|
||||
let mut paragraphs = vec![Paragraph::from_section_index(0)];
|
||||
for (section_index, section) in job.sections.iter().enumerate() {
|
||||
layout_section(
|
||||
fonts,
|
||||
pixels_per_point,
|
||||
&job,
|
||||
section_index as u32,
|
||||
section,
|
||||
&mut paragraphs,
|
||||
);
|
||||
{
|
||||
let mut shape_buffer = fonts.take_shape_buffer();
|
||||
for (section_index, section) in job.sections.iter().enumerate() {
|
||||
let mut font = fonts.font(§ion.format.font_id.family);
|
||||
shape_buffer = layout_section(
|
||||
&mut font,
|
||||
shape_buffer,
|
||||
pixels_per_point,
|
||||
&job,
|
||||
section_index as u32,
|
||||
section,
|
||||
&mut paragraphs,
|
||||
);
|
||||
}
|
||||
fonts.return_shape_buffer(shape_buffer);
|
||||
}
|
||||
|
||||
let point_scale = PointScale::new(pixels_per_point);
|
||||
@@ -144,21 +170,198 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>)
|
||||
galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
|
||||
}
|
||||
|
||||
/// Shared context for emitting shaped glyphs into a [`Paragraph`].
|
||||
struct ShapingContext {
|
||||
pixels_per_point: f32,
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
extra_letter_spacing: f32,
|
||||
section_index: u32,
|
||||
font_metrics: StyledMetrics,
|
||||
is_first_glyph_in_section: bool,
|
||||
prev_cluster: Option<u32>,
|
||||
}
|
||||
|
||||
impl ShapingContext {
|
||||
fn glyph(
|
||||
&self,
|
||||
chr: char,
|
||||
physical_x: i32,
|
||||
advance_width_px: f32,
|
||||
face_metrics: &StyledMetrics,
|
||||
uv_rect: UvRect,
|
||||
) -> Glyph {
|
||||
Glyph {
|
||||
chr,
|
||||
pos: pos2(physical_x as f32 / self.pixels_per_point, f32::NAN),
|
||||
advance_width: advance_width_px / self.pixels_per_point,
|
||||
line_height: self.line_height,
|
||||
font_face_height: face_metrics.row_height,
|
||||
font_face_ascent: face_metrics.ascent,
|
||||
font_height: self.font_metrics.row_height,
|
||||
font_ascent: self.font_metrics.ascent,
|
||||
uv_rect,
|
||||
section_index: self.section_index,
|
||||
first_vertex: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produced by [`segment_into_runs`] for text shaping.
|
||||
#[derive(Debug)]
|
||||
struct TextRun {
|
||||
/// Which font face should shape this run.
|
||||
font_key: FontFaceKey,
|
||||
|
||||
/// Byte range within the section text.
|
||||
byte_range: std::ops::Range<usize>,
|
||||
}
|
||||
|
||||
/// Emit shaped glyphs from a [`harfrust::GlyphBuffer`] into a [`Paragraph`].
|
||||
fn layout_shaped_run(
|
||||
font: &mut Font<'_>,
|
||||
run: &TextRun,
|
||||
run_text: &str,
|
||||
glyph_buffer: &harfrust::GlyphBuffer,
|
||||
face_metrics: &StyledMetrics,
|
||||
ctx: &mut ShapingContext,
|
||||
paragraph: &mut Paragraph,
|
||||
) {
|
||||
let px_scale = face_metrics.px_scale_factor;
|
||||
|
||||
// Reset cluster tracking — cluster values are byte offsets within run_text,
|
||||
// so they are not comparable across runs.
|
||||
ctx.prev_cluster = None;
|
||||
|
||||
for (info, pos) in glyph_buffer
|
||||
.glyph_infos()
|
||||
.iter()
|
||||
.zip(glyph_buffer.glyph_positions())
|
||||
{
|
||||
let glyph_id = skrifa::GlyphId::new(info.glyph_id);
|
||||
let cluster = info.cluster;
|
||||
let mut advance_width_px = pos.x_advance as f32 * px_scale;
|
||||
let x_offset_px = pos.x_offset as f32 * px_scale;
|
||||
let y_offset_px = -(pos.y_offset as f32 * px_scale); // harfrust Y+ up → screen Y+ down
|
||||
|
||||
let chr = run_text
|
||||
.get(cluster as usize..)
|
||||
.and_then(|s| s.chars().next())
|
||||
.unwrap_or('\u{FFFD}'); // Unicode Replacement Character
|
||||
|
||||
// Tab is a layout concept, not a glyph — the shaper doesn't know about tab stops.
|
||||
// Override the advance width to TAB_SIZE × space width.
|
||||
if chr == '\t' {
|
||||
let (_, space_info) = font.glyph_info(' ');
|
||||
let space_width_px = space_info.advance_width_unscaled.0 * px_scale;
|
||||
advance_width_px = TAB_SIZE as f32 * space_width_px;
|
||||
}
|
||||
|
||||
// Apply extra_letter_spacing only at cluster boundaries,
|
||||
// never between glyphs within the same cluster (e.g. base + mark).
|
||||
let is_new_cluster = ctx.prev_cluster.is_none_or(|pc| pc != cluster);
|
||||
if !ctx.is_first_glyph_in_section && is_new_cluster {
|
||||
paragraph.cursor_x_px += ctx.extra_letter_spacing * ctx.pixels_per_point;
|
||||
}
|
||||
if is_new_cluster {
|
||||
ctx.is_first_glyph_in_section = false;
|
||||
}
|
||||
ctx.prev_cluster = Some(cluster);
|
||||
|
||||
let glyph = if glyph_id == skrifa::GlyphId::NOTDEF {
|
||||
// The shaper couldn't map this character. Drop combining marks
|
||||
// (Unicode category M) and duplicate NOTDEF glyphs within the same
|
||||
// cluster — only the first base character gets a replacement glyph.
|
||||
if is_combining_mark(chr) || !is_new_cluster {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the fallback font face (not run.font_key which returned NOTDEF).
|
||||
let (fallback_key, glyph_info) = font.glyph_info(chr);
|
||||
let fallback_metrics = font
|
||||
.fonts_by_id
|
||||
.get(&fallback_key)
|
||||
.map(|ff| {
|
||||
ff.styled_metrics(ctx.pixels_per_point, ctx.font_size, &Default::default())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let advance_width_px =
|
||||
glyph_info.advance_width_unscaled.0 * fallback_metrics.px_scale_factor;
|
||||
let (glyph_alloc, physical_x) =
|
||||
if let Some(ff) = font.fonts_by_id.get_mut(&fallback_key) {
|
||||
ff.allocate_glyph(
|
||||
font.atlas,
|
||||
&fallback_metrics,
|
||||
&ShapedGlyph {
|
||||
glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF),
|
||||
h_pos: paragraph.cursor_x_px,
|
||||
is_cjk: is_cjk(chr),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
paragraph.cursor_x_px += advance_width_px;
|
||||
|
||||
ctx.glyph(
|
||||
chr,
|
||||
physical_x,
|
||||
advance_width_px,
|
||||
&fallback_metrics,
|
||||
glyph_alloc.uv_rect,
|
||||
)
|
||||
} else {
|
||||
let (mut glyph_alloc, physical_x) =
|
||||
if let Some(ff) = font.fonts_by_id.get_mut(&run.font_key) {
|
||||
ff.allocate_glyph(
|
||||
font.atlas,
|
||||
face_metrics,
|
||||
&ShapedGlyph {
|
||||
glyph_id,
|
||||
h_pos: paragraph.cursor_x_px + x_offset_px,
|
||||
is_cjk: is_cjk(chr),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
// Apply shaper y_offset — this varies per glyph instance so it
|
||||
// is not part of the cached ShapedGlyph / GlyphAllocation.
|
||||
glyph_alloc.uv_rect.offset.y += y_offset_px / ctx.pixels_per_point;
|
||||
|
||||
paragraph.cursor_x_px += advance_width_px;
|
||||
|
||||
ctx.glyph(
|
||||
chr,
|
||||
physical_x,
|
||||
advance_width_px,
|
||||
face_metrics,
|
||||
glyph_alloc.uv_rect,
|
||||
)
|
||||
};
|
||||
paragraph.glyphs.push(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignores the Y coordinate.
|
||||
#[must_use]
|
||||
fn layout_section(
|
||||
fonts: &mut FontsImpl,
|
||||
font: &mut Font<'_>,
|
||||
mut shape_buffer: harfrust::UnicodeBuffer,
|
||||
pixels_per_point: f32,
|
||||
job: &LayoutJob,
|
||||
section_index: u32,
|
||||
section: &LayoutSection,
|
||||
out_paragraphs: &mut Vec<Paragraph>,
|
||||
) {
|
||||
) -> harfrust::UnicodeBuffer {
|
||||
let LayoutSection {
|
||||
leading_space,
|
||||
byte_range,
|
||||
format,
|
||||
} = section;
|
||||
let mut font = fonts.font(&format.font_id.family);
|
||||
|
||||
let font_size = format.font_id.size;
|
||||
let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
|
||||
let line_height = section
|
||||
@@ -169,76 +372,100 @@ fn layout_section(
|
||||
|
||||
let mut paragraph = out_paragraphs.last_mut().unwrap();
|
||||
if paragraph.glyphs.is_empty() {
|
||||
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
||||
paragraph.empty_paragraph_height = line_height;
|
||||
}
|
||||
|
||||
paragraph.cursor_x_px += leading_space * pixels_per_point;
|
||||
|
||||
let mut last_glyph_id = None;
|
||||
let section_text = &job.text[byte_range.clone()];
|
||||
let mut ctx = ShapingContext {
|
||||
pixels_per_point,
|
||||
font_size,
|
||||
line_height,
|
||||
extra_letter_spacing,
|
||||
section_index,
|
||||
font_metrics,
|
||||
is_first_glyph_in_section: paragraph.glyphs.is_empty(),
|
||||
prev_cluster: None,
|
||||
};
|
||||
let mut runs = Vec::new();
|
||||
|
||||
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
|
||||
let mut current_font = FontFaceKey::INVALID;
|
||||
let mut current_font_face_metrics = StyledMetrics::default();
|
||||
|
||||
for chr in job.text[byte_range.clone()].chars() {
|
||||
if job.break_on_newline && chr == '\n' {
|
||||
// Process each paragraph segment (split on newlines — the shaper can't handle them).
|
||||
for (seg_idx, segment) in SplitOrWhole::new(section_text, job.break_on_newline).enumerate() {
|
||||
if 0 < seg_idx {
|
||||
out_paragraphs.push(Paragraph::from_section_index(section_index));
|
||||
paragraph = out_paragraphs.last_mut().unwrap();
|
||||
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
||||
} else {
|
||||
let (font_id, glyph_info) = font.glyph_info(chr);
|
||||
let mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
if current_font != font_id {
|
||||
current_font = font_id;
|
||||
current_font_face_metrics = font_face
|
||||
.as_ref()
|
||||
.map(|font_face| {
|
||||
font_face.styled_metrics(pixels_per_point, font_size, &format.coords)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
paragraph.empty_paragraph_height = line_height;
|
||||
ctx.is_first_glyph_in_section = true;
|
||||
}
|
||||
|
||||
if let (Some(font_face), Some(last_glyph_id), Some(glyph_id)) =
|
||||
(&font_face, last_glyph_id, glyph_info.id)
|
||||
{
|
||||
paragraph.cursor_x_px += font_face.pair_kerning_pixels(
|
||||
¤t_font_face_metrics,
|
||||
last_glyph_id,
|
||||
glyph_id,
|
||||
);
|
||||
if segment.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only apply extra_letter_spacing to glyphs after the first one:
|
||||
paragraph.cursor_x_px += extra_letter_spacing * pixels_per_point;
|
||||
}
|
||||
segment_into_runs(font, segment, &mut runs);
|
||||
|
||||
let (glyph_alloc, physical_x) = if let Some(font_face) = font_face.as_mut() {
|
||||
font_face.allocate_glyph(
|
||||
font.atlas,
|
||||
¤t_font_face_metrics,
|
||||
glyph_info,
|
||||
chr,
|
||||
paragraph.cursor_x_px,
|
||||
)
|
||||
} else {
|
||||
Default::default()
|
||||
let num_runs = runs.len();
|
||||
for (run_idx, run) in runs.iter().enumerate() {
|
||||
let run_text = &segment[run.byte_range.clone()];
|
||||
let Some(font_face) = font.fonts_by_id.get(&run.font_key) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
paragraph.glyphs.push(Glyph {
|
||||
chr,
|
||||
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
|
||||
advance_width: glyph_alloc.advance_width_px / pixels_per_point,
|
||||
line_height,
|
||||
font_face_height: current_font_face_metrics.row_height,
|
||||
font_face_ascent: current_font_face_metrics.ascent,
|
||||
font_height: font_metrics.row_height,
|
||||
font_ascent: font_metrics.ascent,
|
||||
uv_rect: glyph_alloc.uv_rect,
|
||||
section_index,
|
||||
first_vertex: 0, // filled in later
|
||||
});
|
||||
let face_metrics =
|
||||
font_face.styled_metrics(pixels_per_point, font_size, &format.coords);
|
||||
|
||||
paragraph.cursor_x_px += glyph_alloc.advance_width_px;
|
||||
last_glyph_id = Some(glyph_alloc.id);
|
||||
// Set buffer flags for paragraph boundary context.
|
||||
let mut flags = harfrust::BufferFlags::empty();
|
||||
if run_idx == 0 {
|
||||
flags |= harfrust::BufferFlags::BEGINNING_OF_TEXT;
|
||||
}
|
||||
if run_idx + 1 == num_runs {
|
||||
flags |= harfrust::BufferFlags::END_OF_TEXT;
|
||||
}
|
||||
|
||||
let glyph_buffer = shape_text(font_face, run_text, &format.coords, shape_buffer, flags);
|
||||
|
||||
layout_shaped_run(
|
||||
font,
|
||||
run,
|
||||
run_text,
|
||||
&glyph_buffer,
|
||||
&face_metrics,
|
||||
&mut ctx,
|
||||
paragraph,
|
||||
);
|
||||
|
||||
shape_buffer = glyph_buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
shape_buffer
|
||||
}
|
||||
|
||||
/// Iterator that either splits on `'\n'` or yields the whole string once.
|
||||
/// Avoids `Box<dyn Iterator>` and `Vec<&str>` allocation.
|
||||
enum SplitOrWhole<'a> {
|
||||
Split(std::str::Split<'a, char>),
|
||||
Whole(std::iter::Once<&'a str>),
|
||||
}
|
||||
|
||||
impl<'a> SplitOrWhole<'a> {
|
||||
fn new(text: &'a str, split: bool) -> Self {
|
||||
if split {
|
||||
Self::Split(text.split('\n'))
|
||||
} else {
|
||||
Self::Whole(std::iter::once(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SplitOrWhole<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<&'a str> {
|
||||
match self {
|
||||
Self::Split(iter) => iter.next(),
|
||||
Self::Whole(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,33 +706,14 @@ fn replace_last_glyph_with_overflow_character(
|
||||
.unwrap_or_default();
|
||||
|
||||
let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
|
||||
// Kern the overflow character properly
|
||||
let pair_kerning = font_face
|
||||
.as_mut()
|
||||
.map(|font_face| {
|
||||
if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = (
|
||||
font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id),
|
||||
font_face.glyph_info(overflow_character).and_then(|g| g.id),
|
||||
) {
|
||||
font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
prev_glyph.max_x() + extra_letter_spacing + pair_kerning
|
||||
prev_glyph.max_x() + extra_letter_spacing
|
||||
} else {
|
||||
0.0 // TODO(emilk): heed paragraph leading_space 😬
|
||||
};
|
||||
|
||||
let replacement_glyph_width = font_face
|
||||
.as_mut()
|
||||
.and_then(|f| f.glyph_info(overflow_character))
|
||||
.map(|i| {
|
||||
i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let advance_width_px =
|
||||
glyph_info.advance_width_unscaled.0 * font_face_metrics.px_scale_factor;
|
||||
let replacement_glyph_width = advance_width_px / pixels_per_point;
|
||||
|
||||
// Check if we're within width budget:
|
||||
if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
|
||||
@@ -519,9 +727,11 @@ fn replace_last_glyph_with_overflow_character(
|
||||
f.allocate_glyph(
|
||||
font.atlas,
|
||||
&font_face_metrics,
|
||||
glyph_info,
|
||||
overflow_character,
|
||||
overflow_glyph_x * pixels_per_point,
|
||||
&ShapedGlyph {
|
||||
glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF),
|
||||
h_pos: overflow_glyph_x * pixels_per_point,
|
||||
is_cjk: is_cjk(overflow_character),
|
||||
},
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -536,7 +746,7 @@ fn replace_last_glyph_with_overflow_character(
|
||||
row.glyphs.push(Glyph {
|
||||
chr: overflow_character,
|
||||
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
|
||||
advance_width: replacement_glyph_alloc.advance_width_px / pixels_per_point,
|
||||
advance_width: advance_width_px / pixels_per_point,
|
||||
line_height,
|
||||
font_face_height: font_face_metrics.row_height,
|
||||
font_face_ascent: font_face_metrics.ascent,
|
||||
@@ -1060,6 +1270,90 @@ impl RowBreakCandidates {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Segment text into runs where each run uses a single font face.
|
||||
///
|
||||
/// Grapheme clusters are never split across runs: if a combining mark
|
||||
/// falls back to a different font than its base character, it stays
|
||||
/// with the base character's font (the shaper will handle it).
|
||||
///
|
||||
/// NOTE: Segmentation is by font face, not by Unicode script. A run may
|
||||
/// mix scripts (e.g. Latin + Cyrillic) when they share the same font.
|
||||
/// This is acceptable for scripts with similar shaping rules, but would
|
||||
/// need script-aware splitting once RTL/bidi support is added.
|
||||
///
|
||||
/// Results are appended to `out` (which is cleared first) to allow
|
||||
/// the caller to reuse the allocation across calls.
|
||||
fn segment_into_runs(font: &mut Font<'_>, text: &str, out: &mut Vec<TextRun>) {
|
||||
use unicode_segmentation::UnicodeSegmentation as _;
|
||||
|
||||
out.clear();
|
||||
|
||||
for (byte_offset, grapheme_str) in text.grapheme_indices(true) {
|
||||
let byte_end = byte_offset + grapheme_str.len();
|
||||
|
||||
let base_char = grapheme_str.chars().next().unwrap_or(' ');
|
||||
let (font_key, _) = font.glyph_info(base_char);
|
||||
|
||||
if let Some(last_run) = out.last_mut()
|
||||
&& last_run.font_key == font_key
|
||||
{
|
||||
last_run.byte_range.end = byte_end;
|
||||
continue;
|
||||
}
|
||||
out.push(TextRun {
|
||||
font_key,
|
||||
byte_range: byte_offset..byte_end,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape a text run and return the raw [`harfrust::GlyphBuffer`].
|
||||
///
|
||||
/// The caller should iterate `glyph_infos()` / `glyph_positions()` (both
|
||||
/// `Copy` slices) and convert font units to pixels using `metrics.px_scale_factor`.
|
||||
/// After iteration, recycle the buffer via `glyph_buffer.clear()`.
|
||||
fn shape_text(
|
||||
font_face: &FontFace,
|
||||
text: &str,
|
||||
coords: &VariationCoords,
|
||||
mut buffer: harfrust::UnicodeBuffer,
|
||||
flags: harfrust::BufferFlags,
|
||||
) -> harfrust::GlyphBuffer {
|
||||
let font_ref = font_face.skrifa_font_ref();
|
||||
let tweak = font_face.tweak();
|
||||
|
||||
// Build shaper with variable font instance if variation coordinates are set.
|
||||
let variations: Vec<harfrust::Variation> = tweak
|
||||
.coords
|
||||
.as_ref()
|
||||
.iter()
|
||||
.chain(coords.as_ref().iter())
|
||||
.map(|&(tag, value)| harfrust::Variation { tag, value })
|
||||
.collect();
|
||||
|
||||
let instance = if variations.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(harfrust::ShaperInstance::from_variations(
|
||||
font_ref, variations,
|
||||
))
|
||||
};
|
||||
|
||||
let shaper = font_face
|
||||
.shaper_data()
|
||||
.shaper(font_ref)
|
||||
.instance(instance.as_ref())
|
||||
.build();
|
||||
|
||||
buffer.set_flags(flags);
|
||||
buffer.push_str(text);
|
||||
buffer.guess_segment_properties();
|
||||
|
||||
shaper.shape(buffer, &[])
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1277,4 +1571,177 @@ mod tests {
|
||||
"Unexpected intrinsic size"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combining_diacritics() {
|
||||
// ɔ̃ = U+0254 (LATIN SMALL LETTER OPEN O) + U+0303 (COMBINING TILDE)
|
||||
// With text shaping, the combining tilde should NOT produce a separate
|
||||
// advance — it should be positioned above ɔ via GPOS anchors.
|
||||
// Note: the default fonts don't contain U+0254, so the replacement glyph
|
||||
// is used. The key test is that the combining mark does NOT add extra width.
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let job_combined = LayoutJob::simple(
|
||||
"ɔ\u{0303}".to_owned(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley_combined = layout(&mut fonts, pixels_per_point, job_combined.into());
|
||||
|
||||
let job_base = LayoutJob::simple(
|
||||
"ɔ".to_owned(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley_base = layout(&mut fonts, pixels_per_point, job_base.into());
|
||||
|
||||
let width_combined = galley_combined.size().x;
|
||||
let width_base = galley_base.size().x;
|
||||
|
||||
assert!(
|
||||
(width_combined - width_base).abs() < 2.0,
|
||||
"Combining diacritic should not add significant width. \
|
||||
Base width: {width_base}, Combined width: {width_combined}"
|
||||
);
|
||||
|
||||
let glyphs = &galley_combined.rows[0].row.glyphs;
|
||||
assert!(!glyphs.is_empty(), "Expected at least 1 glyph for ɔ̃");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shaping_basic_latin() {
|
||||
// Basic test: shaped Latin text should produce the same number of glyphs as characters.
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
"Hello".to_owned(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley = layout(&mut fonts, pixels_per_point, job.into());
|
||||
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.rows[0].row.glyphs.len(), 5);
|
||||
assert!(galley.size().x > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shaping_empty_string() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
String::new(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley = layout(&mut fonts, pixels_per_point, job.into());
|
||||
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.rows[0].row.glyphs.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shaping_multiple_newlines() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
"A\n\nB".to_owned(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley = layout(&mut fonts, pixels_per_point, job.into());
|
||||
|
||||
assert_eq!(galley.rows.len(), 3, "Expected 3 rows for 'A\\n\\nB'");
|
||||
assert_eq!(galley.rows[0].row.glyphs.len(), 1); // "A"
|
||||
assert_eq!(galley.rows[1].row.glyphs.len(), 0); // empty line
|
||||
assert_eq!(galley.rows[2].row.glyphs.len(), 1); // "B"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shaping_mixed_font_fallback() {
|
||||
// Text with both Latin and emoji should work without panicking,
|
||||
// even though they use different font faces.
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
"Hi 🎉 bye".to_owned(),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley = layout(&mut fonts, pixels_per_point, job.into());
|
||||
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
// "Hi " (3) + "🎉" (1) + " bye" (4) = at least 8 glyphs
|
||||
assert!(
|
||||
galley.rows[0].row.glyphs.len() >= 8,
|
||||
"Expected >= 8 glyphs, got {}",
|
||||
galley.rows[0].row.glyphs.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gpos_kerning() {
|
||||
// GPOS kerning: pairs like "AV", "VA", "AT" should be tighter than
|
||||
// the sum of individual character widths. Without text shaping, egui
|
||||
// only uses the legacy `kern` table, so these pairs had diff ≈ 0.
|
||||
// With harfrust, GPOS kerning applies proper negative adjustments.
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let font_id = FontId::proportional(14.0);
|
||||
|
||||
for pair in ["AV", "VA", "AT"] {
|
||||
let (pair_w, _, _) = measure_text(&mut fonts, pair, &font_id, pixels_per_point);
|
||||
let chars: Vec<char> = pair.chars().collect();
|
||||
let (w1, _, _) = measure_text(
|
||||
&mut fonts,
|
||||
&chars[0].to_string(),
|
||||
&font_id,
|
||||
pixels_per_point,
|
||||
);
|
||||
let (w2, _, _) = measure_text(
|
||||
&mut fonts,
|
||||
&chars[1].to_string(),
|
||||
&font_id,
|
||||
pixels_per_point,
|
||||
);
|
||||
let sum = w1 + w2;
|
||||
let kern_adjustment = sum - pair_w;
|
||||
|
||||
assert!(
|
||||
kern_adjustment > 0.5,
|
||||
"GPOS kerning for '{pair}': expected pair to be noticeably tighter \
|
||||
than sum of individuals. pair_width={pair_w:.2}, sum={sum:.2}, \
|
||||
kern_adjustment={kern_adjustment:.2} (should be > 0.5)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn measure_text(
|
||||
fonts: &mut FontsImpl,
|
||||
text: &str,
|
||||
font_id: &FontId,
|
||||
pixels_per_point: f32,
|
||||
) -> (f32, usize, Vec<(char, f32)>) {
|
||||
let job = LayoutJob::simple(
|
||||
text.to_owned(),
|
||||
font_id.clone(),
|
||||
Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
);
|
||||
let galley = layout(fonts, pixels_per_point, job.into());
|
||||
let glyphs = &galley.rows[0].row.glyphs;
|
||||
let details: Vec<_> = glyphs.iter().map(|g| (g.chr, g.advance_width)).collect();
|
||||
(galley.size().x, glyphs.len(), details)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4aaf541ed0245777c802d31f01edb0cc4e53ebd2f4444e094336c180b98091d3
|
||||
size 2221
|
||||
oid sha256:14a8e05b81da82b086fe1ba006a39951a8bca3ff7a2b05c5385a425383b30961
|
||||
size 2207
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cbf68b6934dae0868bc9cf0891baf5acf110284d297cfa348e756237fca64a28
|
||||
size 1564
|
||||
oid sha256:1cdfd5e248b3a1f2053f038a4e45c68f085ddbc085a6e1719b1c4c43b18f8d6c
|
||||
size 1566
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83c3e19004462b793a5929f60f8b81a795c57529bfc74c6e87890aa4b9b8d939
|
||||
size 13930
|
||||
oid sha256:d74498e867f1ede9fa5e8769afeb24d7cfdd464db7accf0e4770aa94c7860bbc
|
||||
size 13929
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ef21b42f90401f6b85685e1cc37d07970b38d2b40394f53bbde5bd4f0d54fb95
|
||||
size 5340
|
||||
oid sha256:1bf4b21569bb28659808ee668be82ac89275dfbbbefba3237560aa2bcbc986b2
|
||||
size 5339
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user