diff --git a/Cargo.lock b/Cargo.lock index 78e9db3..bb8c683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -138,7 +138,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -318,7 +318,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -510,39 +510,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "cosmic-text" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cadaea21e24c49c0c82116f2b465ae6a49d63c90e428b0f8d9ae1f638ac91f" -dependencies = [ - "bitflags 2.10.0", - "fontdb", - "harfrust", - "linebender_resource_handle", - "log", - "rangemap", - "rustc-hash", - "self_cell", - "skrifa 0.39.0", - "smol_str", - "swash", - "sys-locale", - "unicode-bidi", - "unicode-linebreak", - "unicode-script", - "unicode-segmentation", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -602,7 +569,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", ] [[package]] @@ -658,7 +636,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -715,7 +693,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -771,26 +749,34 @@ dependencies = [ ] [[package]] -name = "fontconfig-parser" -version = "0.5.8" +name = "font-types" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" dependencies = [ - "roxmltree", + "bytemuck", ] [[package]] -name = "fontdb" -version = "0.23.0" +name = "fontique" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" dependencies = [ - "fontconfig-parser", - "log", + "hashbrown 0.17.1", + "linebender_resource_handle", "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts 0.41.0", + "roxmltree", + "smallvec", + "windows", + "windows-core", + "yeslogic-fontconfig-sys", ] [[package]] @@ -811,7 +797,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -941,14 +927,13 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.4.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0caaee032384c10dd597af4579c67dee16650d862a9ccbe1233ff1a379abc07" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.10.0", "bytemuck", - "core_maths", - "read-fonts 0.36.0", + "read-fonts 0.41.0", "smallvec", ] @@ -972,6 +957,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -984,6 +978,134 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + [[package]] name = "image" version = "0.25.9" @@ -1042,7 +1164,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1050,13 +1172,12 @@ name = "iris" version = "0.1.0" dependencies = [ "arboard", - "cosmic-text", "image", "iris-core", "iris-macro", + "parley", "pollster", "tokio", - "unicode-segmentation", "wgpu", "winit", ] @@ -1066,9 +1187,10 @@ name = "iris-core" version = "0.1.0" dependencies = [ "bytemuck", - "cosmic-text", "fxhash", "image", + "parley", + "swash", "wgpu", ] @@ -1078,7 +1200,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1216,6 +1338,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -1273,9 +1401,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -1410,7 +1538,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1462,7 +1590,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1492,9 +1620,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -1522,7 +1650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-graphics", "objc2-foundation 0.3.2", ] @@ -1571,7 +1699,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1582,7 +1710,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.10.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -1611,6 +1739,16 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2-core-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -1637,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -1648,7 +1786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -1746,9 +1884,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "orbclient" @@ -1811,6 +1949,39 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parlance" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" + +[[package]] +name = "parley" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa 0.44.0", +] + +[[package]] +name = "parley_data" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889" +dependencies = [ + "icu_properties", +] + [[package]] name = "paste" version = "1.0.15" @@ -1857,7 +2028,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1920,6 +2091,17 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1969,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2007,9 +2189,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2055,12 +2237,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - [[package]] name = "rav1e" version = "0.8.1" @@ -2137,16 +2313,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "read-fonts" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "font-types", -] - [[package]] name = "read-fonts" version = "0.36.0" @@ -2154,8 +2320,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345" dependencies = [ "bytemuck", - "core_maths", - "font-types", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.4", + "once_cell", ] [[package]] @@ -2199,9 +2375,12 @@ checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" [[package]] name = "roxmltree" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] [[package]] name = "rustc-hash" @@ -2275,12 +2454,6 @@ dependencies = [ "tiny-skia", ] -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - [[package]] name = "serde" version = "1.0.228" @@ -2308,7 +2481,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2332,16 +2505,6 @@ dependencies = [ "quote", ] -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts 0.35.0", -] - [[package]] name = "skrifa" version = "0.39.0" @@ -2352,6 +2515,16 @@ dependencies = [ "read-fonts 0.36.0", ] +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + [[package]] name = "slab" version = "0.4.11" @@ -2436,11 +2609,11 @@ checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" [[package]] name = "swash" -version = "0.2.6" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" dependencies = [ - "skrifa 0.37.0", + "skrifa 0.39.0", "yazi", "zeno", ] @@ -2457,12 +2630,25 @@ dependencies = [ ] [[package]] -name = "sys-locale" -version = "0.3.2" +name = "syn" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ - "libc", + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", ] [[package]] @@ -2500,7 +2686,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2511,7 +2697,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2554,20 +2740,16 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.10.0" +name = "tinystr" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ - "tinyvec_macros", + "displaydoc", + "serde_core", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.49.0" @@ -2639,15 +2821,6 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" @@ -2655,18 +2828,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -2679,6 +2840,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "v_frame" version = "0.3.9" @@ -2760,7 +2927,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.113", "wasm-bindgen-shared", ] @@ -3120,7 +3287,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -3131,7 +3298,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -3493,6 +3660,12 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + [[package]] name = "x11-dl" version = "2.21.0" @@ -3568,6 +3741,40 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", + "synstructure", +] + [[package]] name = "zeno" version = "0.3.3" @@ -3591,7 +3798,63 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9c2276a..ff8527e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,7 @@ edition.workspace = true [dependencies] iris-core = { workspace = true } iris-macro = { workspace = true } -cosmic-text = { workspace = true } -unicode-segmentation = { workspace = true } +parley = { workspace = true } winit = { workspace = true } arboard = { workspace = true, features = ["wayland-data-control"] } pollster = { workspace = true } @@ -33,8 +32,8 @@ winit = "0.30.12" wgpu = "28.0.0" bytemuck = "1.23.1" image = "0.25.6" -cosmic-text = "0.16.0" -unicode-segmentation = "1.12.0" +parley = "0.11.1" +swash = "0.2.10" fxhash = "0.2.1" arboard = "3.6.1" iris-core = { path = "core" } diff --git a/core/Cargo.toml b/core/Cargo.toml index 71edca5..6002fd4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,5 +7,6 @@ edition.workspace = true wgpu = { workspace = true } bytemuck ={ workspace = true } image = { workspace = true } -cosmic-text = { workspace = true } +parley = { workspace = true } +swash = { workspace = true } fxhash = { workspace = true } diff --git a/core/src/lib.rs b/core/src/lib.rs index 21882d9..1feb954 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -5,7 +5,6 @@ #![feature(unboxed_closures)] #![feature(fn_traits)] #![feature(const_destruct)] -#![feature(portable_simd)] #![feature(associated_type_defaults)] #![feature(unsize)] #![feature(coerce_unsized)] diff --git a/core/src/primitive/color.rs b/core/src/primitive/color.rs index fc27f1e..d94d516 100644 --- a/core/src/primitive/color.rs +++ b/core/src/primitive/color.rs @@ -10,6 +10,13 @@ pub struct Color { pub a: T, } +/// Defaults to visible, unstyled text for Parley's brush requirement. +impl Default for Color { + fn default() -> Self { + Self::BLACK + } +} + impl Color { pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN); pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX); diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index 820d1f0..cc0b61e 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -1,60 +1,66 @@ -use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2}; -use cosmic_text::{ - Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache, - SwashContent, +use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2}; +use parley::{ + Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, + LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, +}; +use swash::{ + FontRef, + scale::{Render, ScaleContext, Source, StrikeWith}, + zeno::{Format, Vector}, }; -use image::{DynamicImage, GenericImageView, RgbaImage}; -use std::simd::{Simd, num::SimdUint}; - -/// TODO: properly wrap this -pub mod text_lib { - pub use cosmic_text::*; -} +/// Shared font, layout, rasterization, and glyph-atlas state. pub struct TextData { - pub font_system: FontSystem, - pub swash_cache: SwashCache, - glyph_cache: Vec<(Placement, CacheKey, Color)>, + pub font_cx: FontContext, + pub layout_cx: LayoutContext, + scale_cx: ScaleContext, + pub atlas: GlyphAtlas, } impl Default for TextData { fn default() -> Self { Self { - font_system: FontSystem::new(), - swash_cache: SwashCache::new(), - glyph_cache: Default::default(), + font_cx: FontContext::new(), + layout_cx: LayoutContext::new(), + scale_cx: ScaleContext::new(), + atlas: GlyphAtlas::default(), } } } -#[derive(Clone, Copy)] +/// An owned font family that can be stored by a widget. +#[derive(Clone, PartialEq)] +pub enum Family { + SansSerif, + Serif, + Monospace, + Named(String), +} + +impl Family { + fn family(&self) -> FontFamily<'_> { + let name = match self { + Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif), + Self::Serif => FontFamilyName::Generic(GenericFamily::Serif), + Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace), + Self::Named(name) => FontFamilyName::Named(name.as_str().into()), + }; + FontFamily::Single(name) + } +} + +#[derive(Clone, PartialEq)] pub struct TextAttrs { pub color: UiColor, pub font_size: f32, pub line_height: f32, - pub family: Family<'static>, + pub family: Family, pub wrap: bool, - /// inner alignment of text region (within where it's drawn) + /// Alignment within the text's region. pub align: RegionAlign, } -impl TextAttrs { - pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option) { - buf.set_metrics_and_size( - font_system, - Metrics::new(self.font_size, self.line_height), - width, - None, - ); - let attrs = Attrs::new().family(self.family); - let list = AttrsList::new(&attrs); - for line in &mut buf.lines { - line.set_attrs_list(list.clone()); - } - } -} - -pub type TextBuffer = Buffer; +pub const LINE_HEIGHT_MULT: f32 = 1.1; impl Default for TextAttrs { fn default() -> Self { @@ -70,122 +76,181 @@ impl Default for TextAttrs { } } -pub const LINE_HEIGHT_MULT: f32 = 1.1; +/// Keeps text and its corresponding layout from getting out of sync. +pub struct TextBuffer { + text: String, + layout: Layout, + /// The inputs used to build the cached layout. + shaped: Option<(TextAttrs, Option)>, +} -impl TextData { - pub fn draw( - &mut self, - buffer: &mut TextBuffer, - attrs: &TextAttrs, - textures: &mut Textures, - ) -> RenderedText { - // TODO: either this or the layout stuff (or both) is super slow, - // should probably do texture packing and things if possible. - // very visible if you add just a couple of wrapping texts and resize window - // should also be timed to figure out exactly what points need to be sped up - // let mut pixels = HashMap::<_, [u8; 4]>::default(); - let mut min_x = 0; - let mut min_y = 0; - let mut max_x = 0; - let mut max_y = 0; - let text_color = { - let c = attrs.color; - cosmic_text::Color::rgba(c.r, c.g, c.b, c.a) - }; - let mut max_width = 0.0f32; - let mut height = 0.0; - - for run in buffer.layout_runs() { - for glyph in run.glyphs.iter() { - let physical_glyph = glyph.physical((0., 0.), 1.0); - - let glyph_color = match glyph.color_opt { - Some(some) => some, - None => text_color, - }; - - if let Some(img) = self - .swash_cache - .get_image(&mut self.font_system, physical_glyph.cache_key) - { - let mut pos = img.placement; - pos.left += physical_glyph.x; - pos.top = physical_glyph.y + run.line_y as i32 - pos.top; - min_x = min_x.min(pos.left); - min_y = min_y.min(pos.top); - max_x = max_x.max(pos.left + pos.width as i32); - max_y = max_y.max(pos.top + pos.height as i32); - self.glyph_cache - .push((pos, physical_glyph.cache_key, glyph_color)); - } - } - max_width = max_width.max(run.line_w); - height += run.line_height; +impl TextBuffer { + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + layout: Layout::new(), + shaped: None, } - let img_width = (max_x - min_x + 1) as u32; - let img_height = (max_y - min_y + 1) as u32; - let mut image = RgbaImage::new(img_width, img_height); + } - for (pos, key, color) in self.glyph_cache.drain(..) { - let img = self - .swash_cache - .get_image(&mut self.font_system, key) - .as_ref() - .unwrap(); - let mut merge = |i, color: [u8; 4]| { - let i = i as i32; - let x = (i % pos.width as i32 + pos.left - min_x) as u32; - let y = (i / pos.width as i32 + pos.top - min_y) as u32; - let pixel = &mut image[(x, y)].0; - // TODO: no clue if proper alpha blending should be done - *pixel = Simd::from(color).saturating_add(Simd::from(*pixel)).into(); - }; + pub fn new_empty() -> Self { + Self::new("") + } - match img.content { - SwashContent::Mask => { - for (i, a) in img.data.iter().enumerate() { - let mut color = color.as_rgba(); - color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8; - merge(i, color); - } - } - SwashContent::SubpixelMask => todo!("subpixel mask text rendering"), - SwashContent::Color => { - let (colors, _) = img.data.as_chunks::<4>(); - for (i, color) in colors.iter().enumerate() { - merge(i, *color); - } - } - } + pub fn text(&self) -> &str { + &self.text + } + + pub fn layout(&self) -> &Layout { + &self.layout + } + + pub fn is_empty(&self) -> bool { + self.text.is_empty() + } + + pub fn set_text(&mut self, text: impl Into) { + let text = text.into(); + if text != self.text { + self.text = text; + self.shaped = None; } + } - let max_dim = 8192; - if image.width() > max_dim || image.height() > max_dim { - let width = image.width().min(max_dim); - let height = image.height().min(max_dim); - eprintln!( - "WARNING: image of size {:?} cropped to {:?} (texture too big)", - image.dimensions(), - (width, height) - ); - image = image.view(0, 0, width, height).to_image(); - } + /// Invalidates the layout and returns the underlying string for editing. + pub fn edit(&mut self) -> &mut String { + self.shaped = None; + &mut self.text + } - RenderedText { - handle: textures.add(image), - top_left_offset: Vec2::new(min_x as f32, min_y as f32), - size: Vec2::new(max_width, height), + pub fn size(&self) -> Vec2 { + Vec2::new(self.layout.width(), self.layout.height()) + } + + pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option) { + if self.shaped.as_ref() == Some(&(attrs.clone(), width)) { + return; } + let mut builder = data + .layout_cx + .ranged_builder(&mut data.font_cx, &self.text, 1.0, true); + builder.push_default(StyleProperty::FontFamily(attrs.family.family())); + builder.push_default(StyleProperty::FontSize(attrs.font_size)); + builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( + attrs.line_height, + ))); + builder.push_default(StyleProperty::Brush(attrs.color)); + builder.build_into(&mut self.layout, &self.text); + self.layout.break_all_lines(width); + self.layout + .align(Alignment::Start, AlignmentOptions::default()); + self.shaped = Some((attrs.clone(), width)); } } -#[derive(Clone)] -pub struct RenderedText { - pub handle: TextureHandle, - pub top_left_offset: Vec2, - pub size: Vec2, +impl TextData { + /// Rasterizes uncached glyphs and places them relative to the text origin. + pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec { + let mut placed = Vec::new(); + for line in buffer.layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(run) = item else { + continue; + }; + let font = run.run().font(); + let font_size = run.run().font_size(); + let coords = run.run().normalized_coords(); + let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize) + else { + continue; + }; + let coords_hash = hash_coords(coords); + // `font.data.id()` rather than the pointer, so the same font + // loaded twice is still one set of entries. + let font_id = font.data.id(); + + for glyph in run.positioned_glyphs() { + let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8; + let key = GlyphKey { + font: font_id, + glyph: glyph.id, + size: (font_size * 16.0).round() as u32, + subpixel, + coords: coords_hash, + }; + let entry = match self.atlas.get(&key) { + Some(entry) => entry, + None => { + let mut scaler = self + .scale_cx + .builder(font_ref) + .size(font_size) + .hint(true) + .normalized_coords(coords) + .build(); + let image = Render::new(&[ + Source::ColorOutline(0), + Source::ColorBitmap(StrikeWith::BestFit), + Source::Outline, + ]) + .format(Format::Alpha) + .offset(Vector::new(subpixel as f32 / 4.0, 0.0)) + .render(&mut scaler, glyph.id as u16); + match image { + Some(image) => self.atlas.insert(key, &image, textures), + None => { + self.atlas.insert_empty(key); + None + } + } + } + }; + let Some(entry) = entry else { continue }; + placed.push(PlacedGlyph { + entry, + offset: Vec2::new( + glyph.x.floor() + entry.left as f32, + glyph.y.floor() - entry.top as f32, + ), + }); + } + } + } + placed + } } -pub trait HasTextures { - fn add_texture(&mut self, image: DynamicImage) -> TextureHandle; +fn hash_coords(coords: &[i16]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for c in coords { + h ^= *c as u16 as u64; + h = h.wrapping_mul(0x1000_0000_01b3); + } + h +} + +/// Cached glyph placement for a laid-out string. +#[derive(Clone)] +pub struct RenderedText { + pub glyphs: std::sync::Arc>, + pub size: Vec2, + pub color: UiColor, +} + +impl TextData { + pub fn render( + &mut self, + buffer: &mut TextBuffer, + attrs: &TextAttrs, + width: Option, + textures: &mut Textures, + ) -> RenderedText { + buffer.shape(self, attrs, width); + let glyphs = self.place(buffer, textures); + RenderedText { + glyphs: std::sync::Arc::new(glyphs), + size: buffer.size(), + color: attrs.color, + } + } } diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index 74e3bf0..51f9074 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -29,14 +29,25 @@ pub struct Textures { pub enum TextureUpdate<'a> { Push(&'a DynamicImage), Set(u32, &'a DynamicImage), + /// Overwrite one rectangle without uploading the entire texture. + Patch(u32, PatchRect, &'a DynamicImage), Free(u32), PushFree, SetFree, } +#[derive(Debug, Clone, Copy)] +pub struct PatchRect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + enum Update { Push(u32), Set(u32), + Patch(u32, PatchRect), Free(u32), } @@ -81,6 +92,19 @@ impl Textures { } } + /// The stored image for a handle, to be written into before `patch`. + pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage { + self.images[handle.inner.view_idx as usize] + .as_mut() + .expect("texture was freed while still held") + } + + /// Queue an upload of just `rect`, after writing it with `image_mut`. + pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) { + self.updates + .push(Update::Patch(handle.inner.view_idx, rect)); + } + pub fn free(&mut self) { for idx in self.recv.try_iter() { self.images[idx as usize] = None; @@ -99,6 +123,10 @@ impl Textures { .as_ref() .map(|img| TextureUpdate::Set(i, img)) .unwrap_or(TextureUpdate::SetFree), + Update::Patch(i, rect) => self.images[i as usize] + .as_ref() + .map(|img| TextureUpdate::Patch(i, rect, img)) + .unwrap_or(TextureUpdate::SetFree), Update::Free(i) => TextureUpdate::Free(i), }) } diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs new file mode 100644 index 0000000..703114b --- /dev/null +++ b/core/src/render/atlas.rs @@ -0,0 +1,210 @@ +//! Packs reusable rasterized glyphs into shared texture pages. + +use crate::{ + PatchRect, TextureHandle, Textures, + util::{HashMap, Vec2}, +}; +use image::RgbaImage; +use swash::scale::image::{Content, Image}; + +/// A 1024-pixel RGBA8 page occupies 4 MiB. +const PAGE: u32 = 1024; + +/// Transparent margin kept around every glyph, so that sampling one cannot +/// pick up its neighbour along a shared edge. +const PAD: u32 = 1; + +/// Includes every input that can change the rasterized pixels. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct GlyphKey { + pub font: u64, + pub glyph: u32, + /// Font size in 1/16 px, so sizes that round to the same pixels share a + /// raster instead of filling the atlas with near-duplicates. + pub size: u32, + /// Horizontal subpixel phase, in 1/4 px. + pub subpixel: u8, + /// Hash of the variation coordinates; a variable font at two weights is two + /// different sets of pixels from one glyph id. + pub coords: u64, +} + +#[derive(Clone, Copy)] +pub struct GlyphEntry { + pub uv_min: [f32; 2], + pub uv_max: [f32; 2], + /// Offset from the glyph's pen position to the top-left of its pixels. + pub left: i32, + pub top: i32, + pub width: u32, + pub height: u32, + pub is_color: bool, + pub view_idx: u32, + pub sampler_idx: u32, +} + +struct Page { + handle: TextureHandle, + /// Shelf packing is effective because glyphs at one size have similar heights. + x: u32, + y: u32, + shelf_height: u32, +} + +#[derive(Default)] +pub struct GlyphAtlas { + pages: Vec, + /// `None` for a glyph that rasterised to nothing -- a space, say. Cached + /// too, so it is not re-rasterised on every layout. + entries: HashMap>, +} + +impl GlyphAtlas { + pub fn get(&self, key: &GlyphKey) -> Option> { + self.entries.get(key).copied() + } + + /// Returns `None` when a glyph has no pixels or cannot fit on a page. + pub fn insert( + &mut self, + key: GlyphKey, + image: &Image, + textures: &mut Textures, + ) -> Option { + let w = image.placement.width; + let h = image.placement.height; + if w == 0 || h == 0 { + self.entries.insert(key, None); + return None; + } + if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE { + // Never silently crop an oversized glyph. + self.entries.insert(key, None); + return None; + } + + let (page_idx, x, y) = self.allocate(w, h, textures); + let page = &self.pages[page_idx]; + + let img = textures.image_mut(&page.handle); + let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); + write_glyph(rgba, image, x, y); + + let handle = page.handle.clone(); + let rect = PatchRect { + x, + y, + width: w, + height: h, + }; + textures.patch(&handle, rect); + + let page = &self.pages[page_idx]; + let scale = 1.0 / PAGE as f32; + let entry = GlyphEntry { + uv_min: [x as f32 * scale, y as f32 * scale], + uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale], + left: image.placement.left, + top: image.placement.top, + width: w, + height: h, + is_color: matches!(image.content, Content::Color), + view_idx: page.handle.primitive().view_idx, + sampler_idx: page.handle.primitive().sampler_idx, + }; + self.entries.insert(key, Some(entry)); + Some(entry) + } + + fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { + let need_w = w + PAD; + let need_h = h + PAD; + if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) { + let page = &mut self.pages[i]; + if page.x + need_w > PAGE { + page.y += page.shelf_height; + page.x = PAD; + page.shelf_height = 0; + } + let (x, y) = (page.x, page.y); + page.x += need_w; + page.shelf_height = page.shelf_height.max(need_h); + return (i, x, y); + } + + let handle = textures.add(RgbaImage::new(PAGE, PAGE)); + self.pages.push(Page { + handle, + x: PAD + w + PAD, + y: PAD, + shelf_height: h + PAD, + }); + (self.pages.len() - 1, PAD, PAD) + } + + /// Record that a glyph has no pixels, so it is not re-rasterised. + pub fn insert_empty(&mut self, key: GlyphKey) { + self.entries.insert(key, None); + } + + pub fn page_count(&self) -> usize { + self.pages.len() + } + + pub fn glyph_count(&self) -> usize { + self.entries.len() + } +} + +fn fits(page: &Page, need_w: u32, need_h: u32) -> bool { + // On the current shelf, or on a new one above it. + (page.x + need_w <= PAGE && page.y + need_h <= PAGE) + || (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE) +} + +/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time. +fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) { + let w = image.placement.width; + let h = image.placement.height; + match image.content { + Content::Mask => { + for row in 0..h { + for col in 0..w { + let a = image.data[(row * w + col) as usize]; + page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a])); + } + } + } + Content::Color => { + for row in 0..h { + for col in 0..w { + let i = ((row * w + col) * 4) as usize; + let px = [ + image.data[i], + image.data[i + 1], + image.data[i + 2], + image.data[i + 3], + ]; + page.put_pixel(x + col, y + row, image::Rgba(px)); + } + } + } + Content::SubpixelMask => { + // Preserve readable output if the rasterizer returns a subpixel mask. + for row in 0..h { + for col in 0..w { + let i = ((row * w + col) * 4) as usize; + let a = image.data[i + 1]; + page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a])); + } + } + } + } +} + +/// Where a glyph goes on screen, in pixels relative to the text's origin. +#[derive(Clone, Copy)] +pub struct PlacedGlyph { + pub entry: GlyphEntry, + pub offset: Vec2, +} diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 7dec368..8e1a734 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -11,11 +11,13 @@ use wgpu::{ *, }; +mod atlas; mod data; mod primitive; mod texture; mod util; +pub use atlas::*; pub use data::{Mask, MaskIdx}; pub use primitive::*; diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index e249f8f..511a64e 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -202,6 +202,7 @@ impl PrimitiveHandle { primitives!( rects: RectPrimitive => 0, textures: TexturePrimitive => 1, + glyphs: GlyphPrimitive => 2, ); #[repr(C)] @@ -231,6 +232,22 @@ pub struct TexturePrimitive { pub sampler_idx: u32, } +/// An atlas subrectangle, tinted for masks or unchanged for color glyphs. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct GlyphPrimitive { + pub uv_min: [f32; 2], + pub uv_max: [f32; 2], + pub view_idx: u32, + pub sampler_idx: u32, + pub color: Color, + pub flags: u32, +} + +impl GlyphPrimitive { + pub const IS_COLOR: u32 = 1; +} + pub struct PrimitiveVec { vec: Vec, free: Vec, diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl index 197d101..1e6df68 100644 --- a/core/src/render/shader.wgsl +++ b/core/src/render/shader.wgsl @@ -1,5 +1,6 @@ const RECT: u32 = 0u; const TEXTURE: u32 = 1u; +const GLYPH: u32 = 2u; @group(0) @binding(0) var window: WindowUniform; @@ -7,6 +8,8 @@ var window: WindowUniform; var rects: array; @group(1) @binding(TEXTURE) var textures: array; +@group(1) @binding(GLYPH) +var glyphs: array; struct Rect { color: u32, @@ -20,6 +23,15 @@ struct TextureInfo { sampler_idx: u32, } +struct GlyphInfo { + uv_min: vec2, + uv_max: vec2, + view_idx: u32, + sampler_idx: u32, + color: u32, + flags: u32, +} + struct Mask { x: UiSpan, y: UiSpan, @@ -125,6 +137,9 @@ fn fs_main( case TEXTURE: { color = draw_texture(region, textures[i]); } + case GLYPH: { + color = draw_glyph(region, glyphs[i]); + } default: { color = vec4(1.0, 0.0, 1.0, 1.0); } @@ -148,6 +163,17 @@ fn draw_texture(region: Region, info: TextureInfo) -> vec4 { return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv); } +fn draw_glyph(region: Region, g: GlyphInfo) -> vec4 { + let uv = mix(g.uv_min, g.uv_max, region.uv); + let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv); + if (g.flags & 1u) != 0u { + return texel; + } + var color = unpack4x8unorm(g.color); + color.a *= texel.a; + return color; +} + fn draw_rounded_rect(region: Region, rect: Rect) -> vec4 { var color = unpack4x8unorm(rect.color); diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index a9ec637..f8710b6 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -1,11 +1,13 @@ -use image::{DynamicImage, EncodableLayout}; +use image::{DynamicImage, EncodableLayout, GenericImageView}; use wgpu::{util::DeviceExt, *}; -use crate::{TextureUpdate, Textures}; +use crate::{PatchRect, TextureUpdate, Textures}; pub struct GpuTextures { device: Device, queue: Queue, + /// Parallel to `views`; patches require textures rather than views. + textures: Vec>, views: Vec, view_count: usize, samplers: Vec, @@ -17,37 +19,95 @@ impl GpuTextures { pub fn update(&mut self, textures: &mut Textures) -> bool { let mut changed = false; for update in textures.updates() { - changed = true; match update { - TextureUpdate::Push(image) => self.push(image), - TextureUpdate::Set(i, image) => self.set(i, image), - TextureUpdate::SetFree => self.view_count += 1, - TextureUpdate::Free(i) => self.free(i), - TextureUpdate::PushFree => self.push_free(), + TextureUpdate::Push(image) => { + self.push(image); + changed = true; + } + TextureUpdate::Set(i, image) => { + self.set(i, image); + changed = true; + } + TextureUpdate::Patch(i, rect, image) => { + // Patching contents leaves the binding array unchanged. + self.patch(i, rect, image); + } + TextureUpdate::SetFree => { + self.view_count += 1; + changed = true; + } + TextureUpdate::Free(i) => { + self.free(i); + changed = true; + } + TextureUpdate::PushFree => { + self.push_free(); + changed = true; + } } } changed } fn set(&mut self, i: u32, image: &DynamicImage) { self.view_count += 1; - let view = self.create_view(image); + let (texture, view) = self.create(image); + self.textures[i as usize] = Some(texture); self.views[i as usize] = view; } fn free(&mut self, i: u32) { self.view_count -= 1; + self.textures[i as usize] = None; self.views[i as usize] = self.null_view.clone(); } fn push(&mut self, image: &DynamicImage) { self.view_count += 1; - let view = self.create_view(image); + let (texture, view) = self.create(image); + self.textures.push(Some(texture)); self.views.push(view); } fn push_free(&mut self) { self.view_count += 1; + self.textures.push(None); self.views.push(self.null_view.clone()); } - fn create_view(&self, image: &DynamicImage) -> TextureView { + fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { + let Some(texture) = &self.textures[i as usize] else { + return; + }; + if rect.width == 0 || rect.height == 0 { + return; + } + // `write_texture` requires tightly packed rows, unlike the atlas image. + let sub = image + .view(rect.x, rect.y, rect.width, rect.height) + .to_image(); + self.queue.write_texture( + TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: Origin3d { + x: rect.x, + y: rect.y, + z: 0, + }, + aspect: TextureAspect::All, + }, + sub.as_bytes(), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(rect.width * 4), + rows_per_image: Some(rect.height), + }, + Extent3d { + width: rect.width, + height: rect.height, + depth_or_array_layers: 1, + }, + ); + } + + fn create(&self, image: &DynamicImage) -> (Texture, TextureView) { let image = image.to_rgba8(); let (width, height) = image.dimensions(); let texture = self.device.create_texture_with_data( @@ -63,13 +123,14 @@ impl GpuTextures { sample_count: 1, dimension: TextureDimension::D2, format: TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }, wgt::TextureDataOrder::MipMajor, image.as_bytes(), ); - texture.create_view(&TextureViewDescriptor::default()) + let view = texture.create_view(&TextureViewDescriptor::default()); + (texture, view) } pub fn new(device: &Device, queue: &Queue) -> Self { @@ -77,6 +138,7 @@ impl GpuTextures { Self { device: device.clone(), queue: queue.clone(), + textures: Vec::new(), views: Vec::new(), samplers: vec![default_sampler(device)], no_views: vec![null_view.clone()], diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index f7f4939..c120ce6 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,7 @@ use crate::{ Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, - TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId, - render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, + TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, + render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, util::Vec2, }; @@ -90,10 +90,44 @@ impl<'a> Painter<'a> { self.primitive_at(handle.primitive(), region); } - /// returns (handle, offset from top left) - pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText { + pub fn render_text( + &mut self, + buffer: &mut TextBuffer, + attrs: &TextAttrs, + width: Option, + ) -> RenderedText { let ui = self.rsc.ui_mut(); - ui.text.draw(buffer, attrs, &mut ui.textures) + ui.text.render(buffer, attrs, width, &mut ui.textures) + } + + /// Draws one atlas-sampling quad per glyph. + pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { + let flags_for = |is_color| { + if is_color { + GlyphPrimitive::IS_COLOR + } else { + 0 + } + }; + for glyph in text.glyphs.iter() { + let mut region = origin; + region.x.end = region.x.start; + region.y.end = region.y.start; + let mut region = region.offset(UiVec2::abs(glyph.offset)); + region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); + region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32); + self.primitive_at( + GlyphPrimitive { + uv_min: glyph.entry.uv_min, + uv_max: glyph.entry.uv_max, + view_idx: glyph.entry.view_idx, + sampler_idx: glyph.entry.sampler_idx, + color: text.color, + flags: flags_for(glyph.entry.is_color), + }, + region, + ); + } } pub fn region(&self) -> UiRegion { diff --git a/core/src/ui/size.rs b/core/src/ui/size.rs index 6f82492..a874cf9 100644 --- a/core/src/ui/size.rs +++ b/core/src/ui/size.rs @@ -76,8 +76,13 @@ impl SizeCtx<'_> { self.output_size } - pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText { - self.text.draw(buffer, attrs, self.textures) + pub fn draw_text( + &mut self, + buffer: &mut TextBuffer, + attrs: &TextAttrs, + width: Option, + ) -> RenderedText { + self.text.render(buffer, attrs, width, self.textures) } pub fn label(&self, id: WidgetId) -> &String { diff --git a/examples/tabs/main.rs b/examples/tabs/main.rs index d9789f1..6316924 100644 --- a/examples/tabs/main.rs +++ b/examples/tabs/main.rs @@ -1,4 +1,3 @@ -use cosmic_text::Family; use std::{cell::RefCell, rc::Rc}; use winit::event::WindowEvent; diff --git a/src/lib.rs b/src/lib.rs index fa54bba..05ef101 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ #![feature(unboxed_closures)] #![feature(fn_traits)] -#![feature(gen_blocks)] #![feature(associated_type_defaults)] #![feature(unsize)] #![feature(option_into_flat_iter)] diff --git a/src/widget/text/build.rs b/src/widget/text/build.rs index a2e8134..1619bfe 100644 --- a/src/widget/text/build.rs +++ b/src/widget/text/build.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use cosmic_text::{Attrs, Family, Metrics}; use std::marker::{PhantomData, Sized}; pub struct TextBuilder = ()> { @@ -20,7 +19,7 @@ impl> TextBuilder { self.attrs.color = color; self } - pub fn family(mut self, family: Family<'static>) -> Self { + pub fn family(mut self, family: Family) -> Self { self.attrs.family = family; self } @@ -82,19 +81,13 @@ impl TextBuilderOutput for TextOutput { state: &mut Rsc, builder: TextBuilder, ) -> Self::Output { - let mut buf = TextBuffer::new_empty(Metrics::new( - builder.attrs.font_size, - builder.attrs.line_height, - )); + let buf = TextBuffer::new(&builder.content); let hint = builder.hint.get(state); - let font_system = &mut state.ui_mut().text.font_system; - buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None); let mut text = Text { content: builder.content.into(), view: TextView::new(buf, builder.attrs, hint), }; text.content.changed = false; - builder.attrs.apply(font_system, &mut text.view.buf, None); text } } @@ -110,19 +103,11 @@ impl TextBuilderOutput for TextEditOutput { state: &mut State, builder: TextBuilder, ) -> Self::Output { - let buf = TextBuffer::new_empty(Metrics::new( - builder.attrs.font_size, - builder.attrs.line_height, - )); - let mut text = TextEdit::new( + let buf = TextBuffer::new(&builder.content); + TextEdit::new( TextView::new(buf, builder.attrs, builder.hint.get(state)), builder.output.mode, - ); - let font_system = &mut state.ui_mut().text.font_system; - text.buf - .set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None); - builder.attrs.apply(font_system, &mut text.buf, None); - text + ) } } diff --git a/src/widget/text/edit.rs b/src/widget/text/edit.rs index 846d16e..e50fb23 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -1,17 +1,30 @@ use crate::prelude::*; -use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion}; +use iris_core::{TextData, UiColor}; +use parley::{Affinity, Layout, Selection}; use std::ops::{Deref, DerefMut}; -use unicode_segmentation::UnicodeSegmentation; use winit::{ event::KeyEvent, keyboard::{Key, NamedKey}, }; +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Motion { + Left, + Right, + LeftWord, + RightWord, + Up, + Down, + LineStart, + LineEnd, +} + pub struct TextEdit { view: TextView, - selection: TextSelection, - history: Vec<(String, TextSelection)>, - double_hit: Option, + /// `None` represents unfocused, which Parley's `Selection` cannot express. + selection: Option, + history: Vec<(String, Option)>, + double_hit: Option, pub mode: EditMode, } @@ -25,27 +38,19 @@ impl TextEdit { pub fn new(view: TextView, mode: EditMode) -> Self { Self { view, - selection: Default::default(), + selection: None, history: Default::default(), double_hit: None, mode, } } - pub fn select_content(&self, start: Cursor, end: Cursor) -> String { - let (start, end) = sort_cursors(start, end); - let mut iter = self.buf.lines.iter().skip(start.line); - let first = iter.next().unwrap(); - if start.line == end.line { - first.text()[start.index..end.index].to_string() - } else { - let mut str = first.text()[start.index..].to_string(); - for _ in (start.line + 1)..end.line { - str = str + "\n" + iter.next().unwrap().text(); - } - let last = iter.next().unwrap(); - str = str + "\n" + &last.text()[..end.index]; - str + + pub fn selected_text(&self) -> Option { + let sel = self.selection?; + if sel.is_collapsed() { + return None; } + Some(self.buf.text()[sel.text_range()].to_string()) } } @@ -57,39 +62,29 @@ impl Widget for TextEdit { painter.layer = base; let region = self.region(); - let size = vec2(1, self.attrs.line_height); - match self.selection { - TextSelection::None => (), - TextSelection::Pos(cursor) => { - if let Some(offset) = cursor_pos(cursor, &self.buf) { - painter.primitive_within( - RectPrimitive::color(Color::WHITE), - size.align(Align::TOP_LEFT).offset(offset).within(®ion), - ); - } - } - TextSelection::Span { start, end } => { - let (start, end) = sort_cursors(start, end); - for (l, x, width) in iter_layout_lines(start, end, &self.buf) { - let top_left = vec2(x, self.attrs.line_height * l as f32); - painter.primitive_within( - RectPrimitive::color(Color::SKY), - size.with_x(width) - .align(Align::TOP_LEFT) - .offset(top_left) - .within(®ion), - ); - } - if let Some(end_offset) = cursor_pos(end, &self.buf) { - painter.primitive_within( - RectPrimitive::color(Color::WHITE), - size.align(Align::TOP_LEFT) - .offset(end_offset) - .within(®ion), - ); - } - } + let Some(selection) = self.selection else { + return; + }; + let layout = self.view.buf.layout(); + + // parley reports selection as boxes in layout space, so bidi and + // wrapped lines come out right without this code knowing about either. + for (rect, _) in selection.geometry(layout) { + let size = vec2(rect.width() as f32, rect.height() as f32); + let top_left = vec2(rect.x0 as f32, rect.y0 as f32); + painter.primitive_within( + RectPrimitive::color(Color::SKY), + size.align(Align::TOP_LEFT).offset(top_left).within(®ion), + ); } + + let caret = selection.focus().geometry(layout, CARET_WIDTH); + let size = vec2(caret.width() as f32, caret.height() as f32); + let top_left = vec2(caret.x0 as f32, caret.y0 as f32); + painter.primitive_within( + RectPrimitive::color(Color::WHITE), + size.align(Align::TOP_LEFT).offset(top_left).within(®ion), + ); } fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { @@ -101,154 +96,60 @@ impl Widget for TextEdit { } } -/// provides top left + width -fn iter_layout_lines( - start: Cursor, - end: Cursor, - buf: &TextBuffer, -) -> impl Iterator { - gen move { - let mut iter = buf.layout_runs().enumerate(); - for (i, line) in iter.by_ref() { - if line.line_i == start.line - && let Some(start_x) = index_x(&line, start.index) - { - if start.line == end.line - && let Some(end_x) = index_x(&line, end.index) - { - yield (i, start_x, end_x - start_x); - return; - } - yield (i, start_x, line.line_w - start_x); - break; - } - } - for (i, line) in iter { - if line.line_i > end.line { - return; - } - if line.line_i == end.line - && let Some(end_x) = index_x(&line, end.index) - { - yield (i, 0.0, end_x); - return; - } - yield (i, 0.0, line.line_w); - } - } -} - -/// copied & modified from fn found in Editor in cosmic_text -/// returns x pos of a (non layout) index within an layout run -fn index_x(run: &LayoutRun, index: usize) -> Option { - for glyph in run.glyphs.iter() { - if index == glyph.start { - return Some(glyph.x); - } else if index > glyph.start && index < glyph.end { - // Guess x offset based on characters - let mut before = 0; - let mut total = 0; - - let cluster = &run.text[glyph.start..glyph.end]; - for (i, _) in cluster.grapheme_indices(true) { - if glyph.start + i < index { - before += 1; - } - total += 1; - } - - let offset = glyph.w * (before as f32) / (total as f32); - return Some(glyph.x + offset); - } - } - None -} - -/// returns top of line segment where cursor should visually select -fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option { - let mut prev = None; - for run in buf - .layout_runs() - .skip_while(|r| r.line_i < cursor.line) - .take_while(|r| r.line_i == cursor.line) - { - prev = Some(vec2(run.line_w, run.line_top)); - if let Some(pos) = index_x(&run, cursor.index) { - return Some(vec2(pos, run.line_top)); - } - } - prev -} +const CARET_WIDTH: f32 = 1.0; pub struct TextEditCtx<'a> { pub text: &'a mut TextEdit, - pub font_system: &'a mut FontSystem, + pub data: &'a mut TextData, } impl<'a> TextEditCtx<'a> { + /// Returns a layout synchronized with the current text. + fn layout(&mut self) -> &Layout { + let attrs = self.text.view.attrs.clone(); + let width = self.text.view.wrap_width(); + self.text.view.buf.shape(self.data, &attrs, width); + self.text.view.buf.layout() + } + + /// Keep the selection valid after the text underneath it changed. + fn refresh(&mut self) { + if let Some(sel) = self.text.selection { + let layout = self.layout(); + self.text.selection = Some(sel.refresh(layout)); + } + } + pub fn take(&mut self) -> String { - let text = self - .text - .buf - .lines - .drain(..) - .map(|l| l.into_text()) - .collect::>() - .join("\n"); - self.text - .buf - .set_text(self.font_system, "", &Attrs::new(), SHAPING, None); - self.text.selection.clear(); + let text = self.text.view.buf.text().to_string(); + self.set(""); text } pub fn set(&mut self, text: &str) { let text = self.string(text); - self.text - .buf - .set_text(self.font_system, &text, &Attrs::new(), SHAPING, None); - self.text.selection.clear(); + self.text.view.buf.set_text(text); + self.text.view.buf.changed = true; + self.text.selection = None; } pub fn motion(&mut self, motion: Motion, select: bool) { - if let TextSelection::Pos(cursor) = self.text.selection - && let Some(new) = self.buf_motion(cursor, motion) - { - if select { - self.text.selection = TextSelection::Span { - start: cursor, - end: new, - }; - } else { - self.text.selection = TextSelection::Pos(new); - } - } else if let TextSelection::Span { start, end } = self.text.selection { - if select { - if let Some(cursor) = self.buf_motion(end, motion) { - self.text.selection = TextSelection::Span { start, end: cursor }; - } - } else { - let (start, end) = sort_cursors(start, end); - let sel = &mut self.text.selection; - match motion { - Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start), - Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end), - _ => { - if let Some(cursor) = self.buf_motion(end, motion) { - self.text.selection = TextSelection::Pos(cursor); - } - } - } - } - } + let Some(sel) = self.text.selection else { + return; + }; + let layout = self.layout(); + let sel = apply_motion(sel, layout, motion, select); + self.text.selection = Some(sel); } + /// Replace the `len` characters before the caret. This is the IME's + /// preedit path: it re-sends the whole composition each time. pub fn replace(&mut self, len: usize, text: &str) { let text = self.string(text); for _ in 0..len { - self.delete(false); + self.backspace(false); } - self.insert_inner(&text, false); + self.insert_str(&text); } fn string(&self, text: &str) -> String { @@ -261,202 +162,184 @@ impl<'a> TextEditCtx<'a> { pub fn insert(&mut self, text: &str) { let text = self.string(text); - let mut lines = text.split('\n'); - let Some(first) = lines.next() else { + self.insert_str(&text); + } + + fn insert_str(&mut self, text: &str) { + if text.is_empty() { return; - }; - self.insert_inner(first, true); - for line in lines { - self.newline(); - self.insert_inner(line, true); } - } - - pub fn clear_span(&mut self) -> bool { - if let TextSelection::Span { start, end } = self.text.selection { - self.delete_between(start, end); - let (start, _) = sort_cursors(start, end); - self.text.selection = TextSelection::Pos(start); - true - } else { - false - } - } - - pub fn delete_between(&mut self, start: Cursor, end: Cursor) { - let lines = &mut self.text.view.buf.lines; - let (start, end) = sort_cursors(start, end); - if start.line == end.line { - let line = &mut lines[start.line]; - let text = line.text(); - let text = text[..start.index].to_string() + &text[end.index..]; - edit_line(line, text); - } else { - // start - let start_text = lines[start.line].text()[..start.index].to_string(); - let end_text = &lines[end.line].text()[end.index..]; - let text = start_text + end_text; - edit_line(&mut lines[start.line], text); - } - // between - let range = (start.line + 1)..=end.line; - if !range.is_empty() { - lines.splice(range, None); - } - } - - fn insert_inner(&mut self, text: &str, mov: bool) { self.clear_span(); - if let TextSelection::Pos(cursor) = &mut self.text.selection { - let line = &mut self.text.view.buf.lines[cursor.line]; - let mut line_text = line.text().to_string(); - line_text.insert_str(cursor.index, text); - edit_line(line, line_text); - if mov { - for _ in 0..text.chars().count() { - self.motion(Motion::Right, false); - } - } + let at = match self.text.selection { + Some(sel) => sel.focus().index(), + None => return, + }; + let at = at.min(self.text.view.buf.text().len()); + self.text.view.buf.edit().insert_str(at, text); + self.text.view.buf.changed = true; + self.set_caret(at + text.len()); + } + + /// True when there was a span to remove. + pub fn clear_span(&mut self) -> bool { + let Some(sel) = self.text.selection else { + return false; + }; + if sel.is_collapsed() { + return false; } + let range = sel.text_range(); + self.text.view.buf.edit().replace_range(range.clone(), ""); + self.text.view.buf.changed = true; + self.set_caret(range.start); + true + } + + fn set_caret(&mut self, index: usize) { + let index = index.min(self.text.view.buf.text().len()); + let layout = self.layout(); + self.text.selection = Some(Selection::from_byte_index( + layout, + index, + Affinity::default(), + )); } pub fn newline(&mut self) { - if self.text.mode == EditMode::SingleLine { - return; - } - self.clear_span(); - if let TextSelection::Pos(cursor) = &mut self.text.selection { - let lines = &mut self.text.view.buf.lines; - let line = &mut lines[cursor.line]; - let new = line.split_off(cursor.index); - cursor.line += 1; - lines.insert(cursor.line, new); - cursor.index = 0; + if self.text.mode == EditMode::MultiLine { + self.insert_str("\n"); } } pub fn backspace(&mut self, word: bool) { - if !self.clear_span() - && let TextSelection::Pos(cursor) = &mut self.text.selection - && (cursor.index != 0 || cursor.line != 0) - { - self.motion(if word { Motion::LeftWord } else { Motion::Left }, false); - self.delete(word); + if self.clear_span() { + return; } + let Some(sel) = self.text.selection else { + return; + }; + let end = sel.focus().index(); + if end == 0 { + return; + } + let start = { + let layout = self.layout(); + if word { + sel.focus().previous_logical_word(layout).index() + } else { + let Some(cluster) = sel.focus().logical_clusters(layout)[0] else { + return; + }; + let range = cluster.text_range(); + if cluster.is_hard_line_break() || cluster.is_emoji() { + range.start + } else { + self.text.view.buf.text()[..range.end] + .char_indices() + .next_back() + .map_or(range.start, |(start, _)| start) + } + } + }; + self.delete_range(start, end); } pub fn delete(&mut self, word: bool) { - if !self.clear_span() - && let TextSelection::Pos(cursor) = &mut self.text.selection - { + if self.clear_span() { + return; + } + let Some(sel) = self.text.selection else { + return; + }; + let start = sel.focus().index(); + if start >= self.text.view.buf.text().len() { + return; + } + let end = { + let layout = self.layout(); if word { - let start = *cursor; - if let Some(end) = self.buf_motion(start, Motion::RightWord) { - self.delete_between(start, end); - } + sel.focus().next_logical_word(layout).index() } else { - let lines = &mut self.text.view.buf.lines; - let line = &mut lines[cursor.line]; - if cursor.index == line.text().len() { - if cursor.line == lines.len() - 1 { - return; - } - let add = lines.remove(cursor.line + 1).into_text(); - let line = &mut lines[cursor.line]; - let mut cur = line.text().to_string(); - cur.push_str(&add); - edit_line(line, cur); - } else { - let mut text = line.text().to_string(); - text.remove(cursor.index); - edit_line(line, text); - } + let clusters = sel.focus().logical_clusters(layout); + let Some(cluster) = clusters[1].as_ref() else { + return; + }; + cluster.text_range().end } - } + }; + self.delete_range(start, end); } - fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option { - self.text - .buf - .cursor_motion(self.font_system, cursor, None, motion) - .map(|r| r.0) + fn delete_range(&mut self, start: usize, end: usize) { + let len = self.text.view.buf.text().len(); + let (start, end) = (start.min(end).min(len), start.max(end).min(len)); + if start == end { + return; + } + self.text.view.buf.edit().replace_range(start..end, ""); + self.text.view.buf.changed = true; + self.set_caret(start); } - pub fn select_word_at(&mut self, cursor: Cursor) { - if let (Some(start), Some(end)) = ( - self.buf_motion(cursor, Motion::LeftWord), - self.buf_motion(cursor, Motion::RightWord), - ) { - self.text.selection = TextSelection::Span { start, end }; - } - } - - pub fn select_line_at(&mut self, cursor: Cursor) { - let end = self.text.buf.lines[cursor.line].text().len(); - self.text.selection = TextSelection::Span { - start: Cursor::new(cursor.line, 0), - end: Cursor::new(cursor.line, end), + pub fn select_all(&mut self) { + let len = self.text.view.buf.text().len(); + if len == 0 { + return; } + let layout = self.layout(); + let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default()); + let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default()); + self.text.selection = Some(Selection::new(anchor, focus)); } pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { let pos = pos - self.text.region().top_left().to_abs(size); - let hit = self.text.buf.hit(pos.x, pos.y); - let sel = &mut self.text.selection; - match sel { - TextSelection::None => { - if !drag && let Some(hit) = hit { - *sel = TextSelection::Pos(hit) - } + let prev_sel = self.text.selection; + let prev_hit = self.text.double_hit; + + let outcome = { + let layout = self.layout(); + if drag { + prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit)) + } else { + let hit = Selection::from_point(layout, pos.x, pos.y); + let index = hit.focus().index(); + // Successive clicks at one index select the word, then the line. + Some(if recent && prev_hit == Some(index) { + (Some(Selection::line_from_point(layout, pos.x, pos.y)), None) + } else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) { + ( + Some(Selection::word_from_point(layout, pos.x, pos.y)), + Some(index), + ) + } else { + (Some(hit), None) + }) } - TextSelection::Pos(pos) => match (hit, drag) { - (None, false) => *sel = TextSelection::None, - (None, true) => (), - (Some(hit), false) => { - if recent && hit == *pos { - self.text.double_hit = Some(hit); - return self.select_word_at(hit); - } else { - *pos = hit - } - } - (Some(end), true) => *sel = TextSelection::Span { start: *pos, end }, - }, - TextSelection::Span { start, end } => match (hit, drag) { - (None, false) => *sel = TextSelection::None, - (None, true) => *sel = TextSelection::Pos(*start), - (Some(hit), false) => { - if recent - && let Some(double) = self.text.double_hit - && double == hit - { - return self.select_line_at(hit); - } else { - *sel = TextSelection::Pos(hit) - } - } - (Some(hit), true) => *end = hit, - }, - } - if let TextSelection::Span { start, end } = sel - && start == end - { - *sel = TextSelection::Pos(*start); + }; + + if let Some((selection, double_hit)) = outcome { + self.text.selection = selection; + self.text.double_hit = double_hit; } } pub fn deselect(&mut self) { - self.text.selection = TextSelection::None; + self.text.selection = None; + self.text.double_hit = None; } pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { - let old = (self.text.content(), self.text.selection); + let old = (self.text.view.buf.text().to_string(), self.text.selection); let mut undo = false; let res = self.apply_event_inner(event, modifiers, &mut undo); - if undo && let Some((old, selection)) = self.text.history.pop() { - self.set(&old); - self.text.selection = selection; - } else if self.text.content() != old.0 { + if undo { + if let Some((old, selection)) = self.text.history.pop() { + self.set(&old); + self.text.selection = selection; + self.refresh(); + } + } else if self.text.view.buf.text() != old.0 { self.text.history.push(old); } res @@ -481,21 +364,25 @@ impl<'a> TextEditCtx<'a> { } } NamedKey::ArrowRight => { - if modifiers.control { - self.motion(Motion::RightWord, modifiers.shift) + let motion = if modifiers.control { + Motion::RightWord } else { - self.motion(Motion::Right, modifiers.shift) - } + Motion::Right + }; + self.motion(motion, modifiers.shift); } NamedKey::ArrowLeft => { - if modifiers.control { - self.motion(Motion::LeftWord, modifiers.shift) + let motion = if modifiers.control { + Motion::LeftWord } else { - self.motion(Motion::Left, modifiers.shift) - } + Motion::Left + }; + self.motion(motion, modifiers.shift); } NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift), + NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift), + NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift), NamedKey::Escape => { self.deselect(); return TextInputResult::Unfocus; @@ -507,34 +394,18 @@ impl<'a> TextEditCtx<'a> { match text.as_str() { "v" => return TextInputResult::Paste, "c" => { - if let TextSelection::Span { start, end } = self.text.selection { - let content = self.text.select_content(start, end); + if let Some(content) = self.text.selected_text() { return TextInputResult::Copy(content); } } "x" => { - if let TextSelection::Span { start, end } = self.text.selection { - let content = self.text.select_content(start, end); + if let Some(content) = self.text.selected_text() { self.clear_span(); return TextInputResult::Copy(content); } } - "a" => { - if !self.text.buf.lines[0].text().is_empty() - || self.text.buf.lines.len() > 1 - { - let lines = &self.text.buf.lines; - let last_line = lines.len() - 1; - let last_idx = lines[last_line].text().len(); - self.text.selection = TextSelection::Span { - start: Cursor::new(0, 0), - end: Cursor::new(last_line, last_idx), - }; - } - } - "z" => { - *undo = true; - } + "a" => self.select_all(), + "z" => *undo = true, _ => self.insert(text), } } else { @@ -547,6 +418,24 @@ impl<'a> TextEditCtx<'a> { } } +fn apply_motion( + sel: Selection, + layout: &Layout, + motion: Motion, + extend: bool, +) -> Selection { + match motion { + Motion::Left => sel.previous_visual(layout, extend), + Motion::Right => sel.next_visual(layout, extend), + Motion::LeftWord => sel.previous_visual_word(layout, extend), + Motion::RightWord => sel.next_visual_word(layout, extend), + Motion::Up => sel.previous_line(layout, extend), + Motion::Down => sel.next_line(layout, extend), + Motion::LineStart => sel.line_start(layout, extend), + Motion::LineEnd => sel.line_end(layout, extend), + } +} + #[derive(Default)] pub struct Modifiers { pub shift: bool, @@ -569,33 +458,6 @@ pub enum TextInputResult { Paste, } -#[derive(Debug, Default, Clone, Copy)] -pub enum TextSelection { - #[default] - None, - Pos(Cursor), - Span { - start: Cursor, - end: Cursor, - }, -} - -impl TextSelection { - pub fn clear(&mut self) { - match self { - TextSelection::None => (), - TextSelection::Pos(cursor) => { - cursor.line = 0; - cursor.index = 0; - cursor.affinity = Affinity::default(); - } - TextSelection::Span { start: _, end: _ } => { - *self = TextSelection::None; - } - } - } -} - impl TextInputResult { pub fn unfocus(&self) -> bool { matches!(self, TextInputResult::Unfocus) @@ -625,7 +487,7 @@ impl> TextEditable for I { let ui = ui.ui_mut(); TextEditCtx { text: ui.widgets.get_mut(self).unwrap(), - font_system: &mut ui.text.font_system, + data: &mut ui.text, } } } diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index cc9c94f..dbe5e7b 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -6,11 +6,8 @@ pub use edit::*; use iris_core::util::MutDetect; use crate::prelude::*; -use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping}; use std::ops::{Deref, DerefMut}; -pub const SHAPING: Shaping = Shaping::Advanced; - pub struct Text { pub content: MutDetect, view: TextView, @@ -25,6 +22,17 @@ pub struct TextView { pub hint: Option, } +impl TextView { + fn is_blank(&self) -> bool { + self.buf.is_empty() + } + + /// The width used by the cached layout. + pub fn wrap_width(&self) -> Option { + self.width + } +} + impl TextView { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option) -> Self { Self { @@ -45,15 +53,6 @@ impl TextView { .align(self.align) } - fn tex_region(&self, tex: &RenderedText) -> UiRegion { - let region = tex.size.align(self.align); - let dims = tex.handle.size(); - let mut region = region.offset(tex.top_left_offset); - region.x.end = region.x.start + UiScalar::abs(dims.x); - region.y.end = region.y.start + UiScalar::abs(dims.y); - region - } - fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText { let width = if self.attrs.wrap { Some(ctx.px_size().x) @@ -68,10 +67,7 @@ impl TextView { return tex.clone(); } self.width = width; - let font_system = &mut ctx.text.font_system; - self.attrs.apply(font_system, &mut self.buf, width); - self.buf.shape_until_scroll(font_system, false); - let tex = ctx.draw_text(&mut self.buf, &self.attrs); + let tex = ctx.draw_text(&mut self.buf, &self.attrs, width); self.tex = Some(tex.clone()); self.attrs.changed = false; self.buf.changed = false; @@ -81,9 +77,8 @@ impl TextView { self.tex.as_ref() } pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - if let Some(hint) = &self.hint - && let [line] = &self.buf.lines[..] - && line.text().is_empty() + if self.is_blank() + && let Some(hint) = &self.hint { ctx.width(hint) } else { @@ -91,9 +86,8 @@ impl TextView { } } pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - if let Some(hint) = &self.hint - && let [line] = &self.buf.lines[..] - && line.text().is_empty() + if self.is_blank() + && let Some(hint) = &self.hint { ctx.height(hint) } else { @@ -102,47 +96,35 @@ impl TextView { } pub fn draw(&mut self, painter: &mut Painter) -> UiRegion { let tex = self.render(&mut painter.size_ctx()); - let region = self.tex_region(&tex); - if let Some(hint) = &self.hint - && let [line] = &self.buf.lines[..] - && line.text().is_empty() + let region = tex.size.align(self.align); + if self.is_blank() + && let Some(hint) = &self.hint { painter.widget(hint); } else { - painter.texture_within(&tex.handle, region); + let within = region.within(&painter.region()); + painter.glyphs(&tex, within); } region } pub fn content(&self) -> String { - self.buf - .lines - .iter() - .map(|l| l.text()) - .collect::>() - .join("\n") + self.buf.text().to_string() } } impl Text { pub fn new(content: impl Into) -> Self { - let attrs = TextAttrs::default(); - let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height)); + let content: String = content.into(); Self { - content: content.into().into(), - view: TextView::new(buf, attrs, None), + view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None), + content: content.into(), } } - fn update_buf(&mut self, ctx: &mut SizeCtx) { + fn update_buf(&mut self, _ctx: &mut SizeCtx) { if self.content.changed { self.content.changed = false; - self.view.buf.set_text( - &mut ctx.text.font_system, - &self.content, - &Attrs::new().family(self.view.attrs.family), - SHAPING, - None, - ); + self.view.buf.set_text(self.content.as_str()); } } } @@ -164,16 +146,6 @@ impl Widget for Text { } } -pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) { - let start = a.min(b); - let end = a.max(b); - (start, end) -} - -pub fn edit_line(line: &mut BufferLine, text: String) { - line.set_text(text, line.ending(), line.attrs_list().clone()); -} - impl Deref for Text { type Target = TextAttrs;