diff --git a/Cargo.lock b/Cargo.lock index 2b9c98c6..36edf2c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -56,7 +62,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.5.0", + "bitflags 2.6.0", "cc", "cesu8", "jni", @@ -87,9 +93,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "arbitrary" @@ -105,20 +111,20 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "as-raw-xcb-connection" @@ -199,9 +205,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitstream-io" @@ -238,22 +244,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -270,9 +276,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "calloop" @@ -280,7 +286,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "log", "polling", "rustix", @@ -302,9 +308,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.12" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ "jobserver", "libc", @@ -475,9 +481,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -576,7 +582,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libloading 0.8.5", "winapi", ] @@ -589,13 +595,13 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -652,15 +658,15 @@ dependencies = [ [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "euclid" -version = "0.22.10" +version = "0.22.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" dependencies = [ "num-traits", ] @@ -675,7 +681,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -692,12 +698,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -718,6 +724,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0189ccb084f77c5523e08288d418cbaa09c451a08515678a0aa265df9a8b60" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-cache-parser" version = "0.2.0" @@ -744,7 +759,7 @@ dependencies = [ "objc2-foundation", "peniko", "roxmltree", - "skrifa", + "skrifa 0.19.3", "smallvec", "unicode-script", "windows 0.58.0", @@ -769,7 +784,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -864,7 +879,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-alloc-types", ] @@ -874,7 +889,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -896,7 +911,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-descriptor-types", "hashbrown", ] @@ -907,7 +922,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -946,7 +961,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "com", "libc", "libloading 0.8.5", @@ -1064,7 +1079,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1108,9 +1123,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1124,7 +1139,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1201,9 +1216,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" dependencies = [ "arrayvec", "libm", @@ -1218,9 +1233,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libfuzzer-sys" @@ -1265,7 +1280,7 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] @@ -1300,9 +1315,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loop9" @@ -1339,9 +1354,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -1352,7 +1367,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "core-graphics-types", "foreign-types", @@ -1369,14 +1384,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "naga" version = "0.20.0" @@ -1385,7 +1409,7 @@ checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -1404,7 +1428,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -1477,7 +1501,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1527,7 +1551,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1561,7 +1585,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -1577,7 +1601,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-core-location", @@ -1601,7 +1625,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1643,7 +1667,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "dispatch", "libc", @@ -1668,7 +1692,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1680,7 +1704,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1703,7 +1727,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-cloud-kit", @@ -1735,7 +1759,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "objc2", "objc2-core-location", @@ -1784,7 +1808,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.4", "smallvec", "windows-targets 0.52.6", ] @@ -1794,9 +1818,14 @@ name = "parley" version = "0.1.0" dependencies = [ "fontique", + "kurbo", "peniko", - "skrifa", + "skrifa 0.19.3", "swash", + "tracing", + "unicode-segmentation", + "winit", + "xi-unicode", ] [[package]] @@ -1838,7 +1867,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1863,7 +1892,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -1904,18 +1933,18 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1936,7 +1965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -1956,18 +1985,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2090,7 +2119,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b8af39d1f23869711ad4cea5e7835a20daa987f80232f7f2a2374d648ca64d" dependencies = [ "bytemuck", - "font-types", + "font-types 0.5.5", +] + +[[package]] +name = "read-fonts" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c141b9980e1150201b2a3a32879001c8f975fe313ec3df5471a9b5c79a880cd" +dependencies = [ + "bytemuck", + "font-types 0.6.0", ] [[package]] @@ -2104,11 +2143,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -2119,9 +2158,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "rgb" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -2140,11 +2179,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2187,22 +2226,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.202" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -2243,7 +2282,17 @@ checksum = "0ab45fb68b53576a43d4fc0e9ec8ea64e29a4d2cc7f44506964cb75f288222e9" dependencies = [ "bytemuck", "core_maths", - "read-fonts", + "read-fonts 0.19.3", +] + +[[package]] +name = "skrifa" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abea4738067b1e628c6ce28b2c216c19e9ea95715cdb332680e821c3bec2ef23" +dependencies = [ + "bytemuck", + "read-fonts 0.20.0", ] [[package]] @@ -2276,7 +2325,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -2319,7 +2368,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -2348,11 +2397,11 @@ checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" [[package]] name = "swash" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682a612b50baf09e8a039547ecf49e6c155690dcb751b1bcb19c93cdeb3d42d4" +checksum = "93cdc334a50fcc2aa3f04761af3b28196280a6aaadb1ef11215c478ae32615ac" dependencies = [ - "read-fonts", + "skrifa 0.20.0", "yazi", "zeno", ] @@ -2364,7 +2413,7 @@ dependencies = [ "image", "parley", "peniko", - "skrifa", + "skrifa 0.19.3", "swash", ] @@ -2381,9 +2430,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.65" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2398,7 +2447,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -2431,22 +2480,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -2492,15 +2541,15 @@ version = "0.1.0" dependencies = [ "parley", "peniko", - "skrifa", + "skrifa 0.19.3", "tiny-skia", ] [[package]] name = "tinystr" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -2515,7 +2564,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.20", + "toml_edit", ] [[package]] @@ -2529,26 +2578,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.18", + "winnow", ] [[package]] @@ -2558,14 +2596,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] [[package]] name = "ttf-parser" @@ -2575,33 +2628,33 @@ checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-script" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "v_frame" @@ -2625,7 +2678,7 @@ dependencies = [ "log", "peniko", "raw-window-handle", - "skrifa", + "skrifa 0.19.3", "static_assertions", "thiserror", "vello_encoding", @@ -2655,7 +2708,7 @@ dependencies = [ "bytemuck", "guillotiere", "peniko", - "skrifa", + "skrifa 0.19.3", "smallvec", ] @@ -2679,9 +2732,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -2721,7 +2774,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -2755,7 +2808,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2768,9 +2821,9 @@ checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", @@ -2782,11 +2835,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "rustix", "wayland-backend", "wayland-scanner", @@ -2798,16 +2851,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +checksum = "3a94697e66e76c85923b0d28a0c251e8f0666f58fc47d316c0f4da6da75d37cb" dependencies = [ "rustix", "wayland-client", @@ -2816,11 +2869,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.3" +version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -2828,11 +2881,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +checksum = "8a0a41a6875e585172495f7a96dfa42ca7e0213868f4f15c313f7c33221a7eff" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2841,11 +2894,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +checksum = "dad87b5fd1b1d3ca2f792df8f686a2a11e3fe1077b71096f7a175ab699f89109" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2854,9 +2907,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", "quick-xml", @@ -2865,9 +2918,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -2935,7 +2988,7 @@ checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg_aliases 0.1.1", "codespan-reporting", "document-features", @@ -2964,7 +3017,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "cfg_aliases 0.1.1", "core-graphics-types", @@ -3005,7 +3058,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "js-sys", "web-sys", ] @@ -3097,7 +3150,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -3108,7 +3161,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] @@ -3353,7 +3406,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.5.0", + "bitflags 2.6.0", "block2", "bytemuck", "calloop", @@ -3396,15 +3449,6 @@ dependencies = [ "xkbcommon-dl", ] -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.6.18" @@ -3458,13 +3502,19 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +[[package]] +name = "xi-unicode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" + [[package]] name = "xkbcommon-dl" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "dlib", "log", "once_cell", @@ -3479,9 +3529,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" [[package]] name = "yazi" @@ -3509,7 +3559,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", "synstructure", ] @@ -3521,9 +3571,9 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", @@ -3531,33 +3581,33 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] name = "zerofrom" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655b0814c5c0b19ade497851070c640773304939a6c0fd5f5fb43da0696d05b7" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", "synstructure", ] @@ -3580,7 +3630,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.77", ] [[package]] diff --git a/parley/Cargo.toml b/parley/Cargo.toml index 3726d087..46213d05 100644 --- a/parley/Cargo.toml +++ b/parley/Cargo.toml @@ -24,3 +24,8 @@ swash = { workspace = true } skrifa = { workspace = true } peniko = { workspace = true } fontique = { workspace = true } +winit = "0.30.4" +xi-unicode = "0.3.0" +kurbo = "0.11.0" +unicode-segmentation = "1.11.0" +tracing = "0.1.40" diff --git a/parley/src/editor/backspace.rs b/parley/src/editor/backspace.rs new file mode 100644 index 00000000..e3177c71 --- /dev/null +++ b/parley/src/editor/backspace.rs @@ -0,0 +1,514 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Calc start of a backspace delete interval + +use xi_unicode::{is_keycap_base, is_variation_selector, EmojiExt}; + +use super::{EditableTextCursor, Selectable}; + +/// Logic adapted from Android and +/// https://github.com/xi-editor/xi-editor/pull/837 +/// See links present in that PR for upstream Android Source +/// Matches Android Logic as at 2024-05-10 +#[allow(clippy::cognitive_complexity)] +fn backspace_offset(text: &impl Selectable, start: usize) -> usize { + #[derive(PartialEq)] + enum State { + Start, + Lf, + BeforeKeycap, + BeforeVsAndKeycap, + BeforeEmojiModifier, + BeforeVsAndEmojiModifier, + BeforeVs, + BeforeEmoji, + BeforeZwj, + BeforeVsAndZwj, + OddNumberedRis, + EvenNumberedRis, + InTagSequence, + Finished, + } + let mut state = State::Start; + + let mut delete_code_point_count = 0; + let mut last_seen_vs_code_point_count = 0; + let mut cursor = text + .cursor(start) + .expect("Backspace must begin at a valid codepoint boundary."); + + while state != State::Finished && cursor.pos() > 0 { + let code_point = cursor.prev_codepoint().unwrap_or('0'); + + match state { + State::Start => { + delete_code_point_count = 1; + if code_point == '\n' { + state = State::Lf; + } else if is_variation_selector(code_point) { + state = State::BeforeVs; + } else if code_point.is_regional_indicator_symbol() { + state = State::OddNumberedRis; + } else if code_point.is_emoji_modifier() { + state = State::BeforeEmojiModifier; + } else if code_point.is_emoji_combining_enclosing_keycap() { + state = State::BeforeKeycap; + } else if code_point.is_emoji() { + state = State::BeforeEmoji; + } else if code_point.is_emoji_cancel_tag() { + state = State::InTagSequence; + } else { + state = State::Finished; + } + } + State::Lf => { + if code_point == '\r' { + delete_code_point_count += 1; + } + state = State::Finished; + } + State::OddNumberedRis => { + if code_point.is_regional_indicator_symbol() { + delete_code_point_count += 1; + state = State::EvenNumberedRis; + } else { + state = State::Finished; + } + } + State::EvenNumberedRis => { + if code_point.is_regional_indicator_symbol() { + delete_code_point_count -= 1; + state = State::OddNumberedRis; + } else { + state = State::Finished; + } + } + State::BeforeKeycap => { + if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndKeycap; + } else { + if is_keycap_base(code_point) { + delete_code_point_count += 1; + } + state = State::Finished; + } + } + State::BeforeVsAndKeycap => { + if is_keycap_base(code_point) { + delete_code_point_count += last_seen_vs_code_point_count + 1; + } + state = State::Finished; + } + State::BeforeEmojiModifier => { + if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndEmojiModifier; + } else if code_point.is_emoji_modifier_base() { + delete_code_point_count += 1; + state = State::BeforeEmoji; + } else { + state = State::Finished; + } + } + State::BeforeVsAndEmojiModifier => { + if code_point.is_emoji_modifier_base() { + delete_code_point_count += last_seen_vs_code_point_count + 1; + } + state = State::Finished; + } + State::BeforeVs => { + if code_point.is_emoji() { + delete_code_point_count += 1; + state = State::BeforeEmoji; + } else { + if !is_variation_selector(code_point) { + //TODO: UCharacter.getCombiningClass(codePoint) == 0 + delete_code_point_count += 1; + } + state = State::Finished; + } + } + State::BeforeEmoji => { + if code_point.is_zwj() { + state = State::BeforeZwj; + } else { + state = State::Finished; + } + } + State::BeforeZwj => { + if code_point.is_emoji() { + delete_code_point_count += 2; + state = if code_point.is_emoji_modifier() { + State::BeforeEmojiModifier + } else { + State::BeforeEmoji + }; + } else if is_variation_selector(code_point) { + last_seen_vs_code_point_count = 1; + state = State::BeforeVsAndZwj; + } else { + state = State::Finished; + } + } + State::BeforeVsAndZwj => { + if code_point.is_emoji() { + delete_code_point_count += last_seen_vs_code_point_count + 2; + last_seen_vs_code_point_count = 0; + state = State::BeforeEmoji; + } else { + state = State::Finished; + } + } + State::InTagSequence => { + if code_point.is_tag_spec_char() { + delete_code_point_count += 1; + } else if code_point.is_emoji() { + delete_code_point_count += 1; + state = State::Finished; + } else { + delete_code_point_count = 1; + state = State::Finished; + } + } + State::Finished => { + break; + } + } + } + + cursor.set(start); + for _ in 0..delete_code_point_count { + let _ = cursor.prev_codepoint(); + } + cursor.pos() +} + +/// Calculate resulting offset for a backwards delete. +/// +/// This involves complicated logic to handle various special cases that +/// are unique to backspace. +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn offset_for_delete_backwards(caret_position: usize, text: &impl Selectable) -> usize { + backspace_offset(text, caret_position) +} + +#[cfg(test)] +mod tests { + //! These tests originate from https://github.com/xi-editor/xi-editor/pull/837, with the logic itself + //! originating from + + #[track_caller] + fn assert_delete_backwards(input: &'static str, target: &'static str) { + let result = super::offset_for_delete_backwards(input.len(), &input); + if result != target.len() { + panic!( + "Backspacing got {:?}, expected {:?}. Index: got {result}, expected {target}", + input.get(..result).unwrap_or("[INVALID RESULT INDEX]"), + target + ); + } + } + + #[track_caller] + fn assert_delete_backwards_seq(targets: &[&'static str]) { + let mut ran = false; + for val in targets.windows(2) { + ran = true; + assert_delete_backwards(val[0], val[1]); + } + if !ran { + panic!("Didn't execute"); + } + } + + #[test] + #[should_panic(expected = "Backspacing got \"\", expected \"1\"")] + fn assert_delete_backwards_invalid() { + assert_delete_backwards("1", "1"); + } + + #[test] + fn delete_combining_enclosing_keycaps() { + // Including variation selector-18 + + assert_delete_backwards("1\u{E0101}\u{20E3}", ""); + + // multiple COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["1\u{20E3}\u{20E3}", "1\u{20E3}", ""]); + + // Isolated multiple COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["\u{20E3}\u{20E3}", "\u{20E3}", ""]); + } + + #[test] + fn delete_variation_selector_tests() { + // Isolated variation selector + + assert_delete_backwards("\u{FE0F}", ""); + + assert_delete_backwards("\u{E0100}", ""); + + // Isolated multiple variation selectors + assert_delete_backwards("\u{FE0F}\u{FE0F}", "\u{FE0F}"); + assert_delete_backwards("\u{FE0F}\u{E0100}", "\u{FE0F}"); + + assert_delete_backwards("\u{E0100}\u{FE0F}", "\u{E0100}"); + assert_delete_backwards("\u{E0100}\u{E0100}", "\u{E0100}"); + + // Multiple variation selectors + assert_delete_backwards("#\u{FE0F}\u{FE0F}", "#\u{FE0F}"); + assert_delete_backwards("#\u{FE0F}\u{E0100}", "#\u{FE0F}"); + + assert_delete_backwards("#\u{FE0F}", ""); + + assert_delete_backwards("#\u{E0100}\u{FE0F}", "#\u{E0100}"); + assert_delete_backwards("#\u{E0100}\u{E0100}", "#\u{E0100}"); + + assert_delete_backwards("#\u{E0100}", ""); + } + + #[test] + fn delete_emoji_zwj_sequence_tests() { + // U+200D is ZERO WIDTH JOINER + assert_delete_backwards("\u{1F441}\u{200D}\u{1F5E8}", ""); // ๐Ÿ‘โ€๐Ÿ—จ + + // U+FE0E is variation selector-15 + + assert_delete_backwards("\u{1F441}\u{200D}\u{1F5E8}\u{FE0E}", ""); + // ๐Ÿ‘โ€๐Ÿ—จ๏ธŽ + + assert_delete_backwards("\u{1F469}\u{200D}\u{1F373}", ""); + // ๐Ÿ‘ฉโ€๐Ÿณ + + assert_delete_backwards("\u{1F487}\u{200D}\u{2640}", ""); + // ๐Ÿ’‡โ€โ™€ + + assert_delete_backwards("\u{1F487}\u{200D}\u{2640}\u{FE0F}", ""); + // ๐Ÿ’‡โ€โ™€๏ธ + + assert_delete_backwards( + "\u{1F468}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F48B}\u{200D}\u{1F468}", + "", + ); + // ๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ + + // Emoji modifier can be appended to each emoji. + + assert_delete_backwards("\u{1F469}\u{1F3FB}\u{200D}\u{1F4BC}", ""); + // ๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ผ + + assert_delete_backwards( + "\u{1F468}\u{1F3FF}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F468}\u{1F3FB}", + "", + ); + // ๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป + + // End with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{1F441}\u{200D}", "\u{1F441}", ""]); // ๐Ÿ‘โ€ + + // Start with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{200D}\u{1F5E8}", "\u{200D}", ""]); + + assert_delete_backwards_seq(&[ + "\u{FE0E}\u{200D}\u{1F5E8}", + "\u{FE0E}\u{200D}", + "\u{FE0E}", + "", + ]); + + // Multiple ZERO WIDTH JOINER + assert_delete_backwards_seq(&[ + "\u{1F441}\u{200D}\u{200D}\u{1F5E8}", + "\u{1F441}\u{200D}\u{200D}", + "\u{1F441}\u{200D}", + "\u{1F441}", + "", + ]); + + // Isolated multiple ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{200D}\u{200D}", "\u{200D}", ""]); + } + + #[test] + fn delete_flags_tests() { + // Isolated regional indicator symbol + + assert_delete_backwards("\u{1F1FA}", ""); + + // Odd numbered regional indicator symbols + assert_delete_backwards_seq(&["\u{1F1FA}\u{1F1F8}\u{1F1FA}", "\u{1F1FA}\u{1F1F8}", ""]); + + // Incomplete sequence. (no tag_term: U+E007E) + assert_delete_backwards_seq(&[ + "a\u{1F3F4}\u{E0067}b", + "a\u{1F3F4}\u{E0067}", + "a\u{1F3F4}", + "a", + "", + ]); + + // No tag_base + assert_delete_backwards_seq(&[ + "a\u{E0067}\u{E007F}b", + "a\u{E0067}\u{E007F}", + "a\u{E0067}", + "a", + "", + ]); + + // Isolated tag chars + assert_delete_backwards_seq(&[ + "a\u{E0067}\u{E0067}b", + "a\u{E0067}\u{E0067}", + "a\u{E0067}", + "a", + "", + ]); + + // Isolated tab term. + assert_delete_backwards_seq(&[ + "a\u{E007F}\u{E007F}b", + "a\u{E007F}\u{E007F}", + "a\u{E007F}", + "a", + "", + ]); + + // Immediate tag_term after tag_base + assert_delete_backwards_seq(&[ + "a\u{1F3F4}\u{E007F}\u{1F3F4}\u{E007F}b", + "a\u{1F3F4}\u{E007F}\u{1F3F4}\u{E007F}", + "a\u{1F3F4}\u{E007F}", + "a", + "", + ]); + } + + #[test] + fn delete_emoji_modifier_tests() { + // U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2. + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}", ""]); + + // Isolated emoji modifier + assert_delete_backwards_seq(&["\u{1F3FB}", ""]); + + // Isolated multiple emoji modifier + assert_delete_backwards_seq(&["\u{1F3FB}\u{1F3FB}", "\u{1F3FB}", ""]); + + // Multiple emoji modifiers + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}\u{1F3FB}", "\u{1F466}\u{1F3FB}", ""]); + } + + #[test] + fn delete_mixed_edge_cases_tests() { + // COMBINING ENCLOSING KEYCAP + variation selector + assert_delete_backwards_seq(&["1\u{20E3}\u{FE0F}", "1", ""]); + + // Variation selector + COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["\u{2665}\u{FE0F}\u{20E3}", "\u{2665}\u{FE0F}", ""]); + + // COMBINING ENCLOSING KEYCAP + ending with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["1\u{20E3}\u{200D}", "1\u{20E3}", ""]); + + // COMBINING ENCLOSING KEYCAP + ZERO WIDTH JOINER + assert_delete_backwards_seq(&[ + "1\u{20E3}\u{200D}\u{1F5E8}", + "1\u{20E3}\u{200D}", + "1\u{20E3}", + "", + ]); + + // Start with ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["\u{200D}\u{20E3}", "\u{200D}", ""]); + + // ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&[ + "\u{1F441}\u{200D}\u{20E3}", + "\u{1F441}\u{200D}", + "\u{1F441}", + "", + ]); + + // COMBINING ENCLOSING KEYCAP + regional indicator symbol + assert_delete_backwards_seq(&["1\u{20E3}\u{1F1FA}", "1\u{20E3}", ""]); + + // Regional indicator symbol + COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["\u{1F1FA}\u{20E3}", "\u{1F1FA}", ""]); + + // COMBINING ENCLOSING KEYCAP + emoji modifier + assert_delete_backwards_seq(&["1\u{20E3}\u{1F3FB}", "1\u{20E3}", ""]); + + // Emoji modifier + COMBINING ENCLOSING KEYCAP + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}\u{20E3}", "\u{1F466}\u{1F3FB}", ""]); + + // Variation selector + end with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{2665}\u{FE0F}\u{200D}", "\u{2665}\u{FE0F}", ""]); + + // Variation selector + ZERO WIDTH JOINER + + assert_delete_backwards("\u{1F469}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}", ""); + + // Start with ZERO WIDTH JOINER + variation selector + + assert_delete_backwards("\u{200D}\u{FE0F}", ""); + + // ZERO WIDTH JOINER + variation selector + assert_delete_backwards_seq(&["\u{1F469}\u{200D}\u{FE0F}", "\u{1F469}", ""]); + + // Variation selector + regional indicator symbol + assert_delete_backwards_seq(&["\u{2665}\u{FE0F}\u{1F1FA}", "\u{2665}\u{FE0F}", ""]); + + // Regional indicator symbol + variation selector + + assert_delete_backwards("\u{1F1FA}\u{FE0F}", ""); + + // Variation selector + emoji modifier + assert_delete_backwards_seq(&["\u{2665}\u{FE0F}\u{1F3FB}", "\u{2665}\u{FE0F}", ""]); + + // Emoji modifier + variation selector + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}\u{FE0F}", "\u{1F466}", ""]); + + // Start withj ZERO WIDTH JOINER + regional indicator symbol + assert_delete_backwards_seq(&["\u{200D}\u{1F1FA}", "\u{200D}", ""]); + + // ZERO WIDTH JOINER + Regional indicator symbol + assert_delete_backwards_seq(&[ + "\u{1F469}\u{200D}\u{1F1FA}", + "\u{1F469}\u{200D}", + "\u{1F469}", + "", + ]); + + // Regional indicator symbol + end with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{1F1FA}\u{200D}", "\u{1F1FA}", ""]); + + // Regional indicator symbol + ZERO WIDTH JOINER + + assert_delete_backwards("\u{1F1FA}\u{200D}\u{1F469}", ""); + + // Start with ZERO WIDTH JOINER + emoji modifier + assert_delete_backwards_seq(&["\u{200D}\u{1F3FB}", "\u{200D}", ""]); + + // ZERO WIDTH JOINER + emoji modifier + assert_delete_backwards_seq(&[ + "\u{1F469}\u{200D}\u{1F3FB}", + "\u{1F469}\u{200D}", + "\u{1F469}", + "", + ]); + + // Emoji modifier + end with ZERO WIDTH JOINER + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}\u{200D}", "\u{1F466}\u{1F3FB}", ""]); + + // Regional indicator symbol + Emoji modifier + assert_delete_backwards_seq(&["\u{1F1FA}\u{1F3FB}", "\u{1F1FA}", ""]); + + // Emoji modifier + regional indicator symbol + assert_delete_backwards_seq(&["\u{1F466}\u{1F3FB}\u{1F1FA}", "\u{1F466}\u{1F3FB}", ""]); + + // RIS + LF + assert_delete_backwards_seq(&["\u{1F1E6}\u{000A}", "\u{1F1E6}", ""]); + } +} diff --git a/parley/src/editor/edit.rs b/parley/src/editor/edit.rs new file mode 100644 index 00000000..5afd9f23 --- /dev/null +++ b/parley/src/editor/edit.rs @@ -0,0 +1,382 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::{Deref, DerefMut, Range}; + +use crate::{style::StyleProperty, FontContext, LayoutContext}; +use kurbo::Point; +use winit::{ + event::{Ime, Modifiers}, + keyboard::{Key, NamedKey}, +}; + +use super::masonry_types::{Handled, PointerButton, TextEvent}; + +use super::{ + offset_for_delete_backwards, + selection::{Affinity, Selection}, + Selectable, TextBrush, TextWithSelection, +}; + +/// Text which can be edited +pub trait EditableText: Selectable { + /// Replace range with new text. + /// Can panic if supplied an invalid range. + // TODO: make this generic over Self + fn edit(&mut self, range: Range, new: impl Into); + /// Create a value of this struct + fn from_str(s: &str) -> Self; +} + +impl EditableText for String { + fn edit(&mut self, range: Range, new: impl Into) { + self.replace_range(range, &new.into()); + } + fn from_str(s: &str) -> Self { + s.to_string() + } +} + +// TODO: What advantage does this actually have? +// impl EditableText for Arc { +// fn edit(&mut self, range: Range, new: impl Into) { +// let new = new.into(); +// if !range.is_empty() || !new.is_empty() { +// Arc::make_mut(self).edit(range, new) +// } +// } +// fn from_str(s: &str) -> Self { +// Arc::new(s.to_owned()) +// } +// } + +/// A region of text which can support editing operations +#[derive(Clone)] +pub struct TextEditor { + inner: TextWithSelection, + /// The range of the preedit region in the text + preedit_range: Option>, +} + +impl TextEditor { + pub fn new(text: T, text_size: f32) -> Self { + Self { + inner: TextWithSelection::new(text, text_size), + preedit_range: None, + } + } + + pub fn reset_preedit(&mut self) { + self.preedit_range = None; + } + + /// Rebuild the text. + /// + /// See also [TextLayout::rebuild](crate::text2::TextLayout::rebuild) for more comprehensive docs. + pub fn rebuild( + &mut self, + font_ctx: &mut FontContext, + layout_ctx: &mut LayoutContext, + ) { + self.inner + .rebuild_with_attributes(font_ctx, layout_ctx, |mut builder| { + if let Some(range) = self.preedit_range.as_ref() { + builder.push(&StyleProperty::Underline(true), range.clone()); + } + builder + }); + } + + pub fn pointer_down( + &mut self, + position: Point, + mods: Modifiers, + button: PointerButton, + ) -> bool { + // TODO: If we have a selection and we're hovering over it, + // implement (optional?) click and drag + self.inner.pointer_down(position, mods, button) + } + + pub fn text_event(&mut self, event: &TextEvent) -> Handled { + let inner_handled = self.inner.text_event(event); + if inner_handled.is_handled() { + return inner_handled; + } + match event { + TextEvent::KeyboardKey(event, mods) if event.state.is_pressed() => { + // We don't input actual text when these keys are pressed + if !(mods.control_key() || mods.alt_key() || mods.super_key()) { + match &event.logical_key { + Key::Named(NamedKey::Backspace) => { + if let Some(selection) = self.inner.selection { + if !selection.is_caret() { + self.text_mut().edit(selection.range(), ""); + self.inner.selection = + Some(Selection::caret(selection.min(), Affinity::Upstream)); + } else { + // TODO: more specific behavior may sometimes be warranted here + // because whole EGCs are more coarse than what people expect + // to be able to delete individual indic grapheme cluster + // components among other things. + let text = self.text_mut(); + let offset = + offset_for_delete_backwards(selection.active, text); + self.text_mut().edit(offset..selection.active, ""); + self.inner.selection = + Some(Selection::caret(offset, selection.active_affinity)); + } + Handled::Yes + } else { + Handled::No + } + } + Key::Named(NamedKey::Delete) => { + if let Some(selection) = self.inner.selection { + if !selection.is_caret() { + self.text_mut().edit(selection.range(), ""); + self.inner.selection = Some(Selection::caret( + selection.min(), + Affinity::Downstream, + )); + } else if let Some(offset) = + self.text().next_grapheme_offset(selection.active) + { + self.text_mut().edit(selection.min()..offset, ""); + self.inner.selection = Some(Selection::caret( + selection.min(), + selection.active_affinity, + )); + } + Handled::Yes + } else { + Handled::No + } + } + Key::Named(NamedKey::Space) => { + let selection = self.inner.selection.unwrap_or(Selection { + anchor: 0, + active: 0, + active_affinity: Affinity::Downstream, + h_pos: None, + }); + let c = ' '; + self.text_mut().edit(selection.range(), c); + self.inner.selection = Some(Selection::caret( + selection.min() + c.len_utf8(), + // We have just added this character, so we are "affined" with it + Affinity::Downstream, + )); + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextChanged(contents)); + Handled::Yes + } + Key::Named(NamedKey::Enter) => { + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextEntered(contents)); + Handled::Yes + } + Key::Named(_) => Handled::No, + Key::Character(c) => { + let selection = self.inner.selection.unwrap_or(Selection { + anchor: 0, + active: 0, + active_affinity: Affinity::Downstream, + h_pos: None, + }); + self.text_mut().edit(selection.range(), &**c); + self.inner.selection = Some(Selection::caret( + selection.min() + c.len(), + // We have just added this character, so we are "affined" with it + Affinity::Downstream, + )); + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextChanged(contents)); + Handled::Yes + } + Key::Unidentified(_) => Handled::No, + Key::Dead(d) => { + eprintln!("Got dead key {d:?}. Will handle"); + Handled::No + } + } + } else if mods.control_key() || mods.super_key() + // TODO: do things differently on mac, rather than capturing both super and control. + { + match &event.logical_key { + Key::Named(NamedKey::Backspace) => { + if let Some(selection) = self.inner.selection { + if !selection.is_caret() { + self.text_mut().edit(selection.range(), ""); + self.inner.selection = + Some(Selection::caret(selection.min(), Affinity::Upstream)); + } + let offset = + self.text().prev_word_offset(selection.active).unwrap_or(0); + self.text_mut().edit(offset..selection.active, ""); + self.inner.selection = + Some(Selection::caret(offset, Affinity::Upstream)); + + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextChanged(contents)); + Handled::Yes + } else { + Handled::No + } + } + Key::Named(NamedKey::Delete) => { + if let Some(selection) = self.inner.selection { + if !selection.is_caret() { + self.text_mut().edit(selection.range(), ""); + self.inner.selection = Some(Selection::caret( + selection.min(), + Affinity::Downstream, + )); + } else if let Some(offset) = + self.text().next_word_offset(selection.active) + { + self.text_mut().edit(selection.active..offset, ""); + self.inner.selection = + Some(Selection::caret(selection.min(), Affinity::Upstream)); + } + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextChanged(contents)); + Handled::Yes + } else { + Handled::No + } + } + _ => Handled::No, + } + } else { + Handled::No + } + } + TextEvent::KeyboardKey(_, _) => Handled::No, + TextEvent::Ime(ime) => match ime { + Ime::Commit(text) => { + if let Some(selection_range) = self.selection.map(|x| x.range()) { + self.text_mut().edit(selection_range.clone(), text); + self.selection = Some(Selection::caret( + selection_range.start + text.len(), + Affinity::Upstream, + )); + } + // let contents = self.text().as_str().to_string(); + // ctx.submit_action(Action::TextChanged(contents)); + Handled::Yes + } + Ime::Preedit(preedit_string, preedit_sel) => { + if let Some(preedit) = self.preedit_range.clone() { + // TODO: Handle the case where this is the same value, to avoid some potential infinite loops + self.text_mut().edit(preedit.clone(), preedit_string); + let np = preedit.start..(preedit.start + preedit_string.len()); + self.preedit_range = if preedit_string.is_empty() { + None + } else { + Some(np.clone()) + }; + self.selection = if let Some(pec) = preedit_sel { + Some(Selection::new( + np.start + pec.0, + np.start + pec.1, + Affinity::Upstream, + )) + } else { + Some(Selection::caret(np.end, Affinity::Upstream)) + }; + } else { + // If we've been sent an event to clear the preedit, + // but there was no existing pre-edit, there's nothing to do + // so we report that the event has been handled + // An empty preedit is sent by some environments when the + // context of a text input has changed, even if the contents + // haven't; this also avoids some potential infinite loops + if preedit_string.is_empty() { + return Handled::Yes; + } + let sr = self.selection.map(|x| x.range()).unwrap_or(0..0); + self.text_mut().edit(sr.clone(), preedit_string); + let np = sr.start..(sr.start + preedit_string.len()); + self.preedit_range = if preedit_string.is_empty() { + None + } else { + Some(np.clone()) + }; + self.selection = if let Some(pec) = preedit_sel { + Some(Selection::new( + np.start + pec.0, + np.start + pec.1, + Affinity::Upstream, + )) + } else { + Some(Selection::caret(np.start, Affinity::Upstream)) + }; + } + Handled::Yes + } + Ime::Enabled => { + // Generally this shouldn't happen, but I can't prove it won't. + if let Some(preedit) = self.preedit_range.clone() { + self.text_mut().edit(preedit.clone(), ""); + self.selection = Some( + self.selection + .unwrap_or(Selection::caret(0, Affinity::Upstream)), + ); + self.preedit_range = None; + } + Handled::Yes + } + Ime::Disabled => { + if let Some(preedit) = self.preedit_range.clone() { + self.text_mut().edit(preedit.clone(), ""); + self.preedit_range = None; + let sm = self.selection.map(|x| x.min()).unwrap_or(0); + if preedit.contains(&sm) { + self.selection = + Some(Selection::caret(preedit.start, Affinity::Upstream)); + } + } + Handled::Yes + } + }, + TextEvent::ModifierChange(_) => Handled::No, + TextEvent::FocusChange(_) => Handled::No, + } + } +} + +impl Deref for TextEditor { + type Target = TextWithSelection; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +// TODO: Being able to call `Self::Target::rebuild` (and `draw`) isn't great. +impl DerefMut for TextEditor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +#[cfg(test)] +mod tests { + use super::EditableText; + + // #[test] + // fn arcstring_empty_edit() { + // let a = Arc::new("hello".to_owned()); + // let mut b = a.clone(); + // b.edit(5..5, ""); + // assert!(Arc::ptr_eq(&a, &b)); + // } + + #[test] + fn replace() { + let mut a = String::from("hello world"); + a.edit(1..9, "era"); + assert_eq!("herald", a); + } +} diff --git a/parley/src/editor/layout.rs b/parley/src/editor/layout.rs new file mode 100644 index 00000000..e13b12ac --- /dev/null +++ b/parley/src/editor/layout.rs @@ -0,0 +1,503 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A type for laying out, drawing, and interacting with text. + +use std::rc::Rc; + +use crate::debug_panic; + +use crate::builder::RangedBuilder; +use crate::fontique::{Style, Weight}; +use crate::layout::{Alignment, Cursor, Affinity}; +use crate::style::{FontFamily, FontStack, GenericFamily, StyleProperty}; +use crate::{FontContext, Layout, LayoutContext}; +use kurbo::{Line, Point, Rect, Size}; +use peniko::{self, Color, Gradient}; + +use super::{Link, TextStorage}; + +/// A component for displaying text on screen. +/// +/// This is a type intended to be used by other widgets that display text. +/// It allows for the text itself as well as font and other styling information +/// to be set and modified. It wraps an inner layout object, and handles +/// invalidating and rebuilding it as required. +/// +/// This object is not valid until the [`rebuild_if_needed`] method has been +/// called. You should generally do this in your widget's [`layout`] method. +/// Additionally, you should call [`needs_rebuild_after_update`] +/// as part of your widget's [`update`] method; if this returns `true`, you will need +/// to call [`rebuild_if_needed`] again, generally by scheduling another [`layout`] +/// pass. +/// +/// [`layout`]: trait.Widget.html#tymethod.layout +/// [`update`]: trait.Widget.html#tymethod.update +/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update +/// [`rebuild_if_needed`]: #method.rebuild_if_needed +/// +/// TODO: Update docs to mentionParley +#[derive(Clone)] +pub struct TextLayout { + text: T, + // TODO: Find a way to let this use borrowed data + scale: f32, + + brush: TextBrush, + font: FontStack<'static>, + text_size: f32, + weight: Weight, + style: Style, + + alignment: Alignment, + max_advance: Option, + + links: Rc<[(Rect, usize)]>, + + needs_layout: bool, + needs_line_breaks: bool, + layout: Layout, +} + +/// A custom brush for `Parley`, enabling using Parley to pass-through +/// which glyphs are selected/highlighted +#[derive(Clone, Debug, PartialEq)] +pub enum TextBrush { + Normal(peniko::Brush), + Highlight { + text: peniko::Brush, + fill: peniko::Brush, + }, +} + +impl TextBrush { + pub fn text_brush(&self) -> &peniko::Brush { + match self { + TextBrush::Normal(brush) => brush, + TextBrush::Highlight { text, .. } => text, + } + } +} + +impl From for TextBrush { + fn from(value: peniko::Brush) -> Self { + Self::Normal(value) + } +} + +impl From for TextBrush { + fn from(value: Gradient) -> Self { + Self::Normal(value.into()) + } +} + +impl From for TextBrush { + fn from(value: Color) -> Self { + Self::Normal(value.into()) + } +} + +// Parley requires their Brush implementations to implement Default +impl Default for TextBrush { + fn default() -> Self { + Self::Normal(Default::default()) + } +} + +/// Metrics describing the layout text. +#[derive(Debug, Clone, Copy, Default)] +pub struct LayoutMetrics { + /// The nominal size of the layout. + pub size: Size, + /// The distance from the nominal top of the layout to the first baseline. + pub first_baseline: f32, + /// The width of the layout, inclusive of trailing whitespace. + pub trailing_whitespace_width: f32, + //TODO: add inking_rect +} + +impl TextLayout { + /// Create a new `TextLayout` object. + pub fn new(text: T, text_size: f32) -> Self { + TextLayout { + text, + scale: 1.0, + + brush: Default::default(), + font: FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif)), + text_size, + weight: Weight::NORMAL, + style: Style::Normal, + + max_advance: None, + alignment: Default::default(), + + links: Rc::new([]), + + needs_layout: true, + needs_line_breaks: true, + layout: Layout::new(), + } + } + + /// Mark that the inner layout needs to be updated. + /// + /// This should be used if your `T` has interior mutability + pub fn invalidate(&mut self) { + self.needs_layout = true; + self.needs_line_breaks = true; + } + + /// Set the scaling factor + pub fn set_scale(&mut self, scale: f32) { + if scale != self.scale { + self.scale = scale; + self.invalidate(); + } + } + + /// Set the default brush used for the layout. + /// + /// This is the non-layout impacting styling (primarily colour) + /// used when displaying the text + #[doc(alias = "set_color")] + pub fn set_brush(&mut self, brush: impl Into) { + let brush = brush.into(); + if brush != self.brush { + self.brush = brush; + self.invalidate(); + } + } + + /// Set the default font stack. + pub fn set_font(&mut self, font: FontStack<'static>) { + if font != self.font { + self.font = font; + self.invalidate(); + } + } + + /// Set the font size. + #[doc(alias = "set_font_size")] + pub fn set_text_size(&mut self, size: f32) { + if size != self.text_size { + self.text_size = size; + self.invalidate(); + } + } + + /// Set the font weight. + pub fn set_weight(&mut self, weight: Weight) { + if weight != self.weight { + self.weight = weight; + self.invalidate(); + } + } + + /// Set the font style. + pub fn set_style(&mut self, style: Style) { + if style != self.style { + self.style = style; + self.invalidate(); + } + } + + /// Set the [`Alignment`] for this layout. + pub fn set_text_alignment(&mut self, alignment: Alignment) { + if self.alignment != alignment { + self.alignment = alignment; + self.invalidate(); + } + } + + /// Set the width at which to wrap words. + /// + /// You may pass `None` to disable word wrapping + /// (the default behaviour). + pub fn set_max_advance(&mut self, max_advance: Option) { + let max_advance = max_advance.map(|it| it.max(0.0)); + if self.max_advance.is_some() != max_advance.is_some() + || self + .max_advance + .zip(max_advance) + // 1e-4 is an arbitrary small-enough value that we don't care to rewrap + .map(|(old, new)| (old - new).abs() >= 1e-4) + .unwrap_or(false) + { + self.max_advance = max_advance; + self.needs_line_breaks = true; + } + } + + /// Returns `true` if this layout needs to be rebuilt. + /// + /// This happens (for instance) after style attributes are modified. + /// + /// This does not account for things like the text changing, handling that + /// is the responsibility of the user. + pub fn needs_rebuild(&self) -> bool { + self.needs_layout || self.needs_line_breaks + } + + // TODO: What are the valid use cases for this, where we shouldn't use a run-specific check instead? + // /// Returns `true` if this layout's text appears to be right-to-left. + // /// + // /// See [`piet::util::first_strong_rtl`] for more information. + // /// + // /// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl + // pub fn text_is_rtl(&self) -> bool { + // self.text_is_rtl + // } +} + +impl TextLayout { + #[track_caller] + fn assert_rebuilt(&self, method: &str) { + if self.needs_layout || self.needs_line_breaks { + debug_panic!( + "TextLayout::{method} called without rebuilding layout object. Text was '{}'", + self.text.as_str().chars().take(250).collect::() + ); + } + } + + /// Set the text to display. + pub fn set_text(&mut self, text: T) { + if !self.text.maybe_eq(&text) { + self.text = text; + self.invalidate(); + } + } + + /// Returns the [`TextStorage`] backing this layout, if it exists. + pub fn text(&self) -> &T { + &self.text + } + + /// Returns the [`TextStorage`] backing this layout, if it exists. + /// + /// Invalidates the layout and so should only be used when definitely applying an edit + pub fn text_mut(&mut self) -> &mut T { + self.invalidate(); + &mut self.text + } + + /// Returns the inner Parley [`Layout`] value. + pub fn layout(&self) -> &Layout { + self.assert_rebuilt("layout"); + &self.layout + } + + /// The size of the laid-out text, excluding any trailing whitespace. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + pub fn size(&self) -> Size { + self.assert_rebuilt("size"); + Size::new(self.layout.width().into(), self.layout.height().into()) + } + + /// The size of the laid-out text, including any trailing whitespace. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + pub fn full_size(&self) -> Size { + self.assert_rebuilt("full_size"); + Size::new(self.layout.full_width().into(), self.layout.height().into()) + } + + /// Return the text's [`LayoutMetrics`]. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + pub fn layout_metrics(&self) -> LayoutMetrics { + self.assert_rebuilt("layout_metrics"); + + let first_baseline = self.layout.get(0).unwrap().metrics().baseline; + let size = Size::new(self.layout.width().into(), self.layout.height().into()); + LayoutMetrics { + size, + first_baseline, + trailing_whitespace_width: self.layout.full_width(), + } + } + + /// For a given `Point` (relative to this object's origin), returns index + /// into the underlying text of the nearest grapheme boundary. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + pub fn cursor_for_point(&self, point: Point) -> Cursor { + self.assert_rebuilt("text_position_for_point"); + + // TODO: This is a mostly good first pass, but doesn't handle cursor positions in + // grapheme clusters within a parley cluster. + // We can also try + Cursor::from_point(&self.layout, point.x as f32, point.y as f32) + } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return the `Point` (relative to this object's origin) representing the + /// boundary of the containing grapheme. + /// + /// # Panics + /// + /// Panics if `text_pos` is not a character boundary. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + pub fn cursor_for_text_position(&self, text_pos: usize) -> Cursor { + self.assert_rebuilt("cursor_for_text_position"); + + // TODO: As a reminder, `is_leading` is not very useful to us; we don't know this ahead of time + // We're going to need to do quite a bit of remedial work on these + // e.g. to handle a inside a ligature made of multiple (unicode) grapheme clusters + // https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster + // But we're choosing to defer this work + // This also needs to handle affinity. + Cursor::from_index(&self.layout, text_pos, Affinity::Upstream) + } + + // /// Given the utf-8 position of a character boundary in the underlying text, + // /// return the `Point` (relative to this object's origin) representing the + // /// boundary of the containing grapheme. + // /// + // /// # Panics + // /// + // /// Panics if `text_pos` is not a character boundary. + // /// + // /// This is not meaningful until [`Self::rebuild`] has been called. + // pub fn point_for_text_position(&self, text_pos: usize) -> Point { + // let cursor = self.cursor_for_text_position(text_pos); + + // let geom = cursor.strong_geometry(&self.layout, 0.0); + // Point::new( + // cursor.visual_offset() as f64, + // (cursor.baseline + cursor.offset) as f64, + // ) + // } + + // TODO: needed for text selection + // /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s + // /// representing the nominal bounding boxes of the text in that range. + // /// + // /// # Panics + // /// + // /// Panics if the range start or end is not a character boundary. + // pub fn rects_for_range(&self, range: Range) -> Vec { + // self.layout.rects_for_range(range) + // } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return a `Line` suitable for drawing a vertical cursor at that boundary. + /// + /// This is not meaningful until [`Self::rebuild`] has been called. + // TODO: This is too simplistic. See https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster + // for example. This would break in a `fi` ligature + pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Option { + let from_position = self.cursor_for_text_position(text_pos); + + // TODO: fix in case there is no text + let line = from_position.cluster_path().line(&self.layout)?; + let line_metrics = line.metrics(); + + let baseline = line_metrics.baseline + line_metrics.descent; + let p1 = (from_position.visual_offset() as f64, baseline as f64); + let p2 = ( + from_position.visual_offset() as f64, + (baseline - line_metrics.size()) as f64, + ); + Some(Line::new(p1, p2)) + } + + /// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists. + /// + /// This can be used both for hit-testing (deciding whether to change the mouse cursor, + /// or performing some other action when hovering) as well as for retrieving a [`Link`] + /// on click. + /// + /// [`Link`]: super::attribute::Link + pub fn link_for_pos(&self, pos: Point) -> Option<&Link> { + let (_, i) = self + .links + .iter() + .rfind(|(hit_box, _)| hit_box.contains(pos))?; + + self.text.links().get(*i) + } + + /// Rebuild the inner layout as needed. + /// + /// This `TextLayout` object manages a lower-level layout object that may + /// need to be rebuilt in response to changes to the text or attributes + /// like the font. + /// + /// This method should be called whenever any of these things may have changed. + /// A simple way to ensure this is correct is to always call this method + /// as part of your widget's [`layout`][crate::Widget::layout] method. + pub fn rebuild( + &mut self, + font_ctx: &mut FontContext, + layout_ctx: &mut LayoutContext, + ) { + self.rebuild_with_attributes(font_ctx, layout_ctx, |builder| builder); + } + + /// Rebuild the inner layout as needed, adding attributes to the underlying layout. + /// + /// See [`Self::rebuild`] for more information + pub fn rebuild_with_attributes( + &mut self, + font_ctx: &mut FontContext, + layout_ctx: &mut LayoutContext, + attributes: impl for<'b> FnOnce(RangedBuilder<'b, TextBrush>) -> RangedBuilder<'b, TextBrush>, + ) { + if self.needs_layout { + self.needs_layout = false; + + let mut builder = layout_ctx.ranged_builder(font_ctx, self.text.as_str(), self.scale); + builder.push_default(&StyleProperty::Brush(self.brush.clone())); + builder.push_default(&StyleProperty::FontSize(self.text_size)); + builder.push_default(&StyleProperty::FontStack(self.font)); + builder.push_default(&StyleProperty::FontWeight(self.weight)); + builder.push_default(&StyleProperty::FontStyle(self.style)); + // For more advanced features (e.g. variable font axes), these can be set in add_attributes + + let builder = self.text.add_attributes(builder); + let mut builder = attributes(builder); + builder.build_into(&mut self.layout, self.text.as_str()); + + self.needs_line_breaks = true; + } + if self.needs_line_breaks { + self.needs_line_breaks = false; + self.layout.break_all_lines(self.max_advance); + self.layout.align(self.max_advance, self.alignment); + + // TODO: + // self.links = text + // .links() + // ... + } + } +} + +impl std::fmt::Debug for TextLayout { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("TextLayout") + .field("text", &self.text.as_str().len()) + .field("scale", &self.scale) + .field("brush", &self.brush) + .field("font", &self.font) + .field("text_size", &self.text_size) + .field("weight", &self.weight) + .field("style", &self.style) + .field("alignment", &self.alignment) + .field("wrap_width", &self.max_advance) + .field("outdated?", &self.needs_rebuild()) + .field("width", &self.layout.width()) + .field("height", &self.layout.height()) + .finish() + } +} + +impl Default for TextLayout { + fn default() -> Self { + Self::new(Default::default(), crate::style::DEFAULT_FONT_SIZE as f32) + } +} diff --git a/parley/src/editor/masonry_types.rs b/parley/src/editor/masonry_types.rs new file mode 100644 index 00000000..a103e78c --- /dev/null +++ b/parley/src/editor/masonry_types.rs @@ -0,0 +1,62 @@ +use winit::{ + event::{Ime, KeyEvent}, + keyboard::ModifiersState, +}; + +// TODO - Clipboard Paste? +// TODO skip is_synthetic=true events +#[derive(Debug, Clone)] +pub enum TextEvent { + KeyboardKey(KeyEvent, ModifiersState), + Ime(Ime), + ModifierChange(ModifiersState), + // TODO - Document difference with Lifecycle focus change + FocusChange(bool), +} + +/// An indicator of which pointer button was pressed. +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +#[repr(u8)] +pub enum PointerButton { + /// No mouse button. + None, + /// Primary button, commonly the left mouse button, touch contact, pen contact. + Primary, + /// Secondary button, commonly the right mouse button, pen barrel button. + Secondary, + /// Auxiliary button, commonly the middle mouse button. + Auxiliary, + /// X1 (back) Mouse. + X1, + /// X2 (forward) Mouse. + X2, + /// Other mouse button. This isn't fleshed out yet. + Other, +} + +/// An enum for specifying whether an event was handled. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Handled { + /// An event was already handled, and shouldn't be propagated to other event handlers. + Yes, + /// An event has not yet been handled. + No, +} + +impl Handled { + /// Has the event been handled yet? + pub fn is_handled(self) -> bool { + self == Handled::Yes + } +} + +impl From for Handled { + /// Returns `Handled::Yes` if `handled` is true, and `Handled::No` otherwise. + fn from(handled: bool) -> Handled { + if handled { + Handled::Yes + } else { + Handled::No + } + } +} diff --git a/parley/src/editor/mod.rs b/parley/src/editor/mod.rs new file mode 100644 index 00000000..4f78f47a --- /dev/null +++ b/parley/src/editor/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Support for text display and rendering +//! +//! There are three kinds of text commonly needed: +//! 1) Entirely display text (e.g. a button) +//! 2) Selectable text (e.g. a paragraph of content) +//! 3) Editable text (e.g. a search bar) +//! +//! All of these have the same set of global styling options, and can contain rich text + +mod store; +pub use store::{Link, TextStorage}; + +mod layout; +pub use layout::{LayoutMetrics, TextBrush, TextLayout}; + +mod selection; +pub use selection::{ + len_utf8_from_first_byte, EditableTextCursor, Selectable, StringCursor, TextWithSelection, +}; + +// mod movement; + +mod edit; +pub use edit::{EditableText, TextEditor}; + +mod backspace; +pub use backspace::offset_for_delete_backwards; + +pub mod masonry_types; +pub use masonry_types::*; diff --git a/parley/src/editor/movement.rs b/parley/src/editor/movement.rs new file mode 100644 index 00000000..59e1f5df --- /dev/null +++ b/parley/src/editor/movement.rs @@ -0,0 +1,357 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Text editing movements. + +use std::ops::Range; + +use unicode_segmentation::UnicodeSegmentation; + +use kurbo::Point; + +use super::{layout::TextLayout, Selectable, TextStorage}; + +/// Compute the result of a [`Movement`] on a [`Selection`]. +/// +/// returns a new selection representing the state after the movement. +/// +/// If `modify` is true, only the 'active' edge (the `end`) of the selection +/// should be changed; this is the case when the user moves with the shift +/// key pressed. +pub fn movement( + m: Movement, + s: Selection, + layout: &TextLayout, + modify: bool, +) -> Selection { + if layout.needs_rebuild() { + debug_panic!("movement() called before layout rebuild"); + return s; + } + let text = layout.text(); + let parley_layout = layout.layout(); + + let writing_direction = || { + if layout + .cursor_for_text_position(s.active, s.active_affinity) + .is_rtl + { + WritingDirection::RightToLeft + } else { + WritingDirection::LeftToRight + } + }; + + let (offset, h_pos) = match m { + Movement::Grapheme(d) => { + let direction = writing_direction(); + if d.is_upstream_for_direction(direction) { + if s.is_caret() || modify { + text.prev_grapheme_offset(s.active) + .map(|off| (off, None)) + .unwrap_or((0, s.h_pos)) + } else { + (s.min(), None) + } + } else { + if s.is_caret() || modify { + text.next_grapheme_offset(s.active) + .map(|off| (off, None)) + .unwrap_or((s.active, s.h_pos)) + } else { + (s.max(), None) + } + } + } + Movement::Vertical(VerticalMovement::LineUp) => { + let cur_pos = layout.cursor_for_text_position(s.active, s.active_affinity); + let h_pos = s.h_pos.unwrap_or(cur_pos.advance); + if cur_pos.path.line_index == 0 { + (0, Some(h_pos)) + } else { + let lm = cur_pos.path.line(&parley_layout).unwrap(); + let point_above = Point::new(h_pos, cur_pos.point.y - lm.height); + let up_pos = layout.hit_test_point(point_above); + if up_pos.is_inside { + (up_pos.idx, Some(h_pos)) + } else { + // because we can't specify affinity, moving up when h_pos + // is wider than both the current line and the previous line + // can result in a cursor position at the visual start of the + // current line; so we handle this as a special-case. + let lm_prev = layout.line_metric(cur_pos.line.saturating_sub(1)).unwrap(); + let up_pos = lm_prev.end_offset - lm_prev.trailing_whitespace; + (up_pos, Some(h_pos)) + } + } + } + Movement::Vertical(VerticalMovement::LineDown) => { + let cur_pos = layout.hit_test_text_position(s.active); + let h_pos = s.h_pos.unwrap_or(cur_pos.point.x); + if cur_pos.line == layout.line_count() - 1 { + (text.len(), Some(h_pos)) + } else { + let lm = layout.line_metric(cur_pos.line).unwrap(); + // may not work correctly for point sizes below 1.0 + let y_below = lm.y_offset + lm.height + 1.0; + let point_below = Point::new(h_pos, y_below); + let up_pos = layout.hit_test_point(point_below); + (up_pos.idx, Some(point_below.x)) + } + } + Movement::Vertical(VerticalMovement::DocumentStart) => (0, None), + Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None), + + Movement::ParagraphStart => (text.preceding_line_break(s.active), None), + Movement::ParagraphEnd => (text.next_line_break(s.active), None), + + Movement::Line(d) => { + let hit = layout.hit_test_text_position(s.active); + let lm = layout.line_metric(hit.line).unwrap(); + let offset = if d.is_upstream_for_direction(writing_direction) { + lm.start_offset + } else { + lm.end_offset - lm.trailing_whitespace + }; + (offset, None) + } + Movement::Word(d) => { + if d.is_upstream_for_direction(writing_direction()) { + let offset = if s.is_caret() || modify { + text.prev_word_offset(s.active).unwrap_or(0) + } else { + s.min() + }; + (offset, None) + } else { + let offset = if s.is_caret() || modify { + text.next_word_offset(s.active).unwrap_or(s.active) + } else { + s.max() + }; + (offset, None) + } + } + + // These two are not handled; they require knowledge of the size + // of the viewport. + Movement::Vertical(VerticalMovement::PageDown) + | Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos), + other => { + tracing::warn!("unhandled movement {:?}", other); + (s.anchor, s.h_pos) + } + }; + + let start = if modify { s.anchor } else { offset }; + Selection::new(start, offset).with_h_pos(h_pos) +} + +/// Indicates a movement that transforms a particular text position in a +/// document. +/// +/// These movements transform only single indices โ€” not selections. +/// +/// You'll note that a lot of these operations are idempotent, but you can get +/// around this by first sending a `Grapheme` movement. If for instance, you +/// want a `ParagraphStart` that is not idempotent, you can first send +/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with +/// `ParagraphStart`. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Movement { + /// A movement that stops when it reaches an extended grapheme cluster boundary. + /// + /// This movement is achieved on most systems by pressing the left and right + /// arrow keys. For more information on grapheme clusters, see + /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). + Grapheme(Direction), + /// A movement that stops when it reaches a word boundary. + /// + /// This movement is achieved on most systems by pressing the left and right + /// arrow keys while holding control. For more information on words, see + /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries). + Word(Direction), + /// A movement that stops when it reaches a soft line break. + /// + /// This movement is achieved on macOS by pressing the left and right arrow + /// keys while holding command. `Line` should be idempotent: if the + /// position is already at the end of a soft-wrapped line, this movement + /// should never push it onto another soft-wrapped line. + /// + /// In order to implement this properly, your text positions should remember + /// their affinity. + Line(Direction), + /// An upstream movement that stops when it reaches a hard line break. + /// + /// `ParagraphStart` should be idempotent: if the position is already at the + /// start of a hard-wrapped line, this movement should never push it onto + /// the previous line. + ParagraphStart, + /// A downstream movement that stops when it reaches a hard line break. + /// + /// `ParagraphEnd` should be idempotent: if the position is already at the + /// end of a hard-wrapped line, this movement should never push it onto the + /// next line. + ParagraphEnd, + /// A vertical movement, see `VerticalMovement` for more details. + Vertical(VerticalMovement), +} + +/// Indicates a horizontal direction in the text. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Direction { + /// The direction visually to the left. + /// + /// This may be byte-wise forwards or backwards in the document, depending + /// on the text direction around the position being moved. + Left, + /// The direction visually to the right. + /// + /// This may be byte-wise forwards or backwards in the document, depending + /// on the text direction around the position being moved. + Right, + /// Byte-wise backwards in the document. + /// + /// In a left-to-right context, this value is the same as `Left`. + Upstream, + /// Byte-wise forwards in the document. + /// + /// In a left-to-right context, this value is the same as `Right`. + Downstream, +} + +impl Direction { + /// Returns `true` if this direction is byte-wise backwards for + /// the provided [`WritingDirection`]. + /// + /// The provided direction *must not be* `WritingDirection::Natural`. + pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool { + // assert!( + // !matches!(direction, WritingDirection::Natural), + // "writing direction must be resolved" + // ); + match self { + Direction::Upstream => true, + Direction::Downstream => false, + Direction::Left => matches!(direction, WritingDirection::LeftToRight), + Direction::Right => matches!(direction, WritingDirection::RightToLeft), + } + } +} + +/// Distinguishes between two visually distinct locations with the same byte +/// index. +/// +/// Sometimes, a byte location in a document has two visual locations. For +/// example, the end of a soft-wrapped line and the start of the subsequent line +/// have different visual locations (and we want to be able to place an input +/// caret in either place!) but the same byte-wise location. This also shows up +/// in bidirectional text contexts. Affinity allows us to disambiguate between +/// these two visual locations. +/// +/// Note that in scenarios where soft line breaks interact with bidi text, this gets +/// more complicated. +#[derive(Copy, Clone, Debug, Hash, PartialEq)] +pub enum Affinity { + /// The position which has an apparent position "earlier" in the text. + /// For soft line breaks, this is the position at the end of the first line. + /// + /// For positions in-between bidi contexts, this is the position which is + /// related to the "outgoing" text section. E.g. for the string "abcDEF" (rendered `abcFED`), + /// with the cursor at "abc|DEF" with upstream affinity, the cursor would be rendered at the + /// position `abc|DEF` + Upstream, + /// The position which has a higher apparent position in the text. + /// For soft line breaks, this is the position at the beginning of the second line. + /// + /// For positions in-between bidi contexts, this is the position which is + /// related to the "incoming" text section. E.g. for the string "abcDEF" (rendered `abcFED`), + /// with the cursor at "abc|DEF" with downstream affinity, the cursor would be rendered at the + /// position `abcDEF|` + Downstream, +} + +impl Affinity { + /// Convert into the `parley` form of "leading" + pub fn is_leading(&self) -> bool { + match self { + Affinity::Upstream => false, + Affinity::Downstream => true, + } + } +} + +/// Indicates a horizontal direction for writing text. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum WritingDirection { + LeftToRight, + RightToLeft, + // /// Indicates writing direction should be automatically detected based on + // /// the text contents. + // See also `is_upstream_for_direction` if adding back in + // Natural, +} + +/// Indicates a vertical movement in a text document. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum VerticalMovement { + LineUp, + LineDown, + PageUp, + PageDown, + DocumentStart, + DocumentEnd, +} + +/// Given a position in some text, return the containing word boundaries. +/// +/// The returned range may not necessary be a 'word'; for instance it could be +/// the sequence of whitespace between two words. +/// +/// If the position is on a word boundary, that will be considered the start +/// of the range. +/// +/// This uses Unicode word boundaries, as defined in [UAX#29]. +/// +/// [UAX#29]: http://www.unicode.org/reports/tr29/ +pub(crate) fn word_range_for_pos(text: &str, pos: usize) -> Range { + text.split_word_bound_indices() + .map(|(ix, word)| ix..(ix + word.len())) + .find(|range| range.contains(&pos)) + .unwrap_or(pos..pos) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn word_range_simple() { + assert_eq!(word_range_for_pos("hello world", 3), 0..5); + assert_eq!(word_range_for_pos("hello world", 8), 6..11); + } + + #[test] + fn word_range_whitespace() { + assert_eq!(word_range_for_pos("hello world", 5), 5..6); + } + + #[test] + fn word_range_rtl() { + let rtl = "ู…ุฑุญุจุง ุจุงู„ุนุงู„ู…"; + assert_eq!(word_range_for_pos(rtl, 5), 0..10); + assert_eq!(word_range_for_pos(rtl, 16), 11..25); + assert_eq!(word_range_for_pos(rtl, 10), 10..11); + } + + #[test] + fn word_range_mixed() { + let mixed = "hello ู…ุฑุญุจุง ุจุงู„ุนุงู„ู… world"; + assert_eq!(word_range_for_pos(mixed, 3), 0..5); + assert_eq!(word_range_for_pos(mixed, 8), 6..16); + assert_eq!(word_range_for_pos(mixed, 19), 17..31); + assert_eq!(word_range_for_pos(mixed, 36), 32..37); + } +} diff --git a/parley/src/editor/selection.rs b/parley/src/editor/selection.rs new file mode 100644 index 00000000..eeafc38a --- /dev/null +++ b/parley/src/editor/selection.rs @@ -0,0 +1,903 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors and the Glazier Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Traits for text editing and a basic String implementation. + +use std::borrow::Cow; +use std::ops::{Deref, DerefMut, Range}; + +use crate::debug_panic; + +use crate::builder::RangedBuilder; +use crate::{style::StyleProperty, FontContext, LayoutContext}; +use kurbo::{Line, Point}; +use peniko::Color; +use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; +use winit::event::Modifiers; +use winit::keyboard::NamedKey; + +use super::masonry_types::{Handled, PointerButton, TextEvent}; + +use super::{TextBrush, TextLayout, TextStorage}; + +#[derive(Clone)] +pub struct TextWithSelection { + pub layout: TextLayout, + /// The current selection within this widget + // TODO: Allow multiple selections (i.e. by holding down control) + pub selection: Option, + highlight_brush: TextBrush, + needs_selection_update: bool, + selecting_with_mouse: bool, + // TODO: Cache cursor line, selection boxes + // cursor_line: Option, +} + +impl TextWithSelection { + pub fn new(text: T, text_size: f32) -> Self { + Self { + layout: TextLayout::new(text, text_size), + selection: None, + needs_selection_update: false, + selecting_with_mouse: false, + // cursor_line: None, + highlight_brush: TextBrush::Highlight { + text: Color::WHITE.into(), + fill: Color::LIGHT_BLUE.into(), + }, + } + } + + pub fn set_text(&mut self, text: T) { + self.selection = None; + self.needs_selection_update = true; + self.layout.set_text(text); + } + + pub fn needs_rebuild(&self) -> bool { + self.layout.needs_rebuild() || self.needs_selection_update + } + + pub fn get_cursor_line(&self) -> Option { + // self.cursor_line + if let Some(selection) = self.selection { + self.layout.cursor_line_for_text_position(selection.active) + } else { + None + } + } + + pub fn pointer_down( + &mut self, + position: Point, + mods: Modifiers, + button: PointerButton, + ) -> bool { + // TODO: work out which button is the primary button? + if button == PointerButton::Primary { + self.selecting_with_mouse = true; + self.needs_selection_update = true; + let position = self.layout.cursor_for_point(position); + tracing::warn!("Got cursor point without getting affinity"); + if mods.state().shift_key() { + if let Some(selection) = self.selection.as_mut() { + selection.active = position.index(); + selection.active_affinity = Affinity::Downstream; + return true; + } + } + self.selection = Some(Selection::caret( + position.index(), + Affinity::Downstream, + )); + true + } else { + false + } + } + + pub fn pointer_up(&mut self, _position: Point, _mods: Modifiers, button: PointerButton) { + if button == PointerButton::Primary { + self.selecting_with_mouse = false; + } + } + + pub fn pointer_move(&mut self, position: Point, _mods: Modifiers) -> bool { + if self.selecting_with_mouse { + self.needs_selection_update = true; + let position = self.layout.cursor_for_point(position); + tracing::warn!("Got cursor point without getting affinity"); + if let Some(selection) = self.selection.as_mut() { + selection.active = position.index(); + selection.active_affinity = Affinity::Downstream; + } else { + debug_panic!("No selection set whilst still dragging"); + } + true + } else { + false + } + } + + pub fn text_event(&mut self, event: &TextEvent) -> Handled { + match event { + TextEvent::KeyboardKey(key, mods) if key.state.is_pressed() => { + match shortcut_key(key) { + winit::keyboard::Key::Named(NamedKey::ArrowLeft) => { + if mods.shift_key() { + } else { + let t = self.text(); + if let Some(selection) = self.selection { + if mods.control_key() { + let offset = t.prev_word_offset(selection.active).unwrap_or(0); + self.selection = + Some(Selection::caret(offset, Affinity::Downstream)); + } else { + let offset = + t.prev_grapheme_offset(selection.active).unwrap_or(0); + self.selection = + Some(Selection::caret(offset, Affinity::Downstream)); + }; + self.needs_selection_update = true; + } + } + Handled::Yes + } + winit::keyboard::Key::Named(NamedKey::ArrowRight) => { + if mods.shift_key() { + // TODO: Expand selection + } else { + let t = self.text(); + if let Some(selection) = self.selection { + if mods.control_key() { + if let Some(o) = t.next_word_offset(selection.active) { + self.selection = + Some(Selection::caret(o, Affinity::Upstream)); + } + } else if let Some(o) = t.next_grapheme_offset(selection.active) { + self.selection = Some(Selection::caret(o, Affinity::Upstream)); + }; + self.needs_selection_update = true; + } + } + Handled::Yes + } + winit::keyboard::Key::Named(_) => Handled::No, + winit::keyboard::Key::Character(chr) => match &*chr { + "a" if mods.control_key() || /* macOS, yes this is a hack */ mods.super_key() => + { + self.selection = + Some(Selection::new(0, self.text().len(), Affinity::Downstream)); + self.needs_selection_update = true; + Handled::Yes + } + "c" if mods.control_key() || mods.super_key() => { + let selection = self.selection.unwrap_or(Selection { + anchor: 0, + active: 0, + active_affinity: Affinity::Downstream, + h_pos: None, + }); + // TODO: We know this is not the fullest model of copy-paste, and that we should work with the inner text + // e.g. to put HTML code if supported by the rich text kind + if let Some(text) = self.text().slice(selection.min()..selection.max()) + { + println!(r#"Copying "{text}""#); + } else { + debug_panic!("Had invalid selection"); + } + Handled::Yes + } + _ => Handled::No, + }, + winit::keyboard::Key::Unidentified(_) => todo!(), + winit::keyboard::Key::Dead(_) => todo!(), + } + } + TextEvent::KeyboardKey(_, _) => Handled::No, + TextEvent::Ime(_) => Handled::No, + TextEvent::ModifierChange(_) => { + // TODO: What does it mean to "handle" this change? + Handled::No + } + TextEvent::FocusChange(_) => { + // TODO: What does it mean to "handle" this change + // TODO: Set our highlighting colour to a lighter blue if window unfocused + Handled::No + } + } + } + + /// Call when another widget becomes focused + pub fn focus_lost(&mut self) { + self.selection = None; + self.selecting_with_mouse = false; + self.needs_selection_update = true; + } + + /// Rebuild the text layout. + /// + /// See also [TextLayout::rebuild] for more comprehensive docs. + pub fn rebuild( + &mut self, + font_ctx: &mut FontContext, + layout_ctx: &mut LayoutContext, + ) { + self.rebuild_with_attributes(font_ctx, layout_ctx, |builder| builder); + } + + // Intentionally aliases the method on `TextLayout` + /// Rebuild the text layout, adding attributes to the builder. + /// + /// See also [TextLayout::rebuild_with_attributes] for more comprehensive docs. + pub fn rebuild_with_attributes( + &mut self, + font_ctx: &mut FontContext, + layout_ctx: &mut LayoutContext, + attributes: impl for<'b> FnOnce(RangedBuilder<'b, TextBrush>) -> RangedBuilder<'b, TextBrush>, + ) { + // In theory, we could be clever here and only rebuild the layout if the + // selected range was previously or currently non-zero size (i.e. there is a selected range) + if self.needs_selection_update || self.layout.needs_rebuild() { + self.layout.invalidate(); + self.layout + .rebuild_with_attributes(font_ctx, layout_ctx, |mut builder| { + if let Some(selection) = self.selection { + let range = selection.range(); + if !range.is_empty() { + builder + .push(&StyleProperty::Brush(self.highlight_brush.clone()), range); + } + } + attributes(builder) + }); + self.needs_selection_update = false; + } + } +} + +/// Get the key which should be used for shortcuts from the underlying event +/// +/// `key_without_modifiers` is only available on some platforms +fn shortcut_key(key: &winit::event::KeyEvent) -> winit::keyboard::Key { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + key.key_without_modifiers() + } + #[cfg(any(target_os = "android", target_os = "ios"))] + // We think it will be rare that users are using a physical keyboard with Android, + // and so we don't really need to worry *too much* about the text selection shortcuts + key.logical_key.clone() +} + +impl Deref for TextWithSelection { + type Target = TextLayout; + + fn deref(&self) -> &Self::Target { + &self.layout + } +} + +// TODO: Being able to call `Self::Target::rebuild` (and `draw`) isn't great. +impl DerefMut for TextWithSelection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.layout + } +} + +/// A range of selected text, or a caret. +/// +/// A caret is the blinking vertical bar where text is to be inserted. We +/// represent it as a selection with zero length, where `anchor == active`. +/// Indices are always expressed in UTF-8 bytes, and must be between 0 and the +/// document length, inclusive. +/// +/// As an example, if the input caret is at the start of the document `hello +/// world`, we would expect both `anchor` and `active` to be `0`. If the user +/// holds shift and presses the right arrow key five times, we would expect the +/// word `hello` to be selected, the `anchor` to still be `0`, and the `active` +/// to now be `5`. +#[derive(Clone, Copy, Debug, PartialEq)] +#[non_exhaustive] +pub struct Selection { + /// The 'anchor' end of the selection. + /// + /// This is the end of the selection that stays unchanged while holding + /// shift and pressing the arrow keys. + // TODO: Is usize the right type for these? Is it plausible to be dealing with a + // more than 4gb file on a 32 bit machine? + pub anchor: usize, + /// The 'active' end of the selection. + /// + /// This is the end of the selection that moves while holding shift and + /// pressing the arrow keys. + pub active: usize, + /// The affinity of the `active` side of the cursor + /// + /// The affinity of `anchor` is entirely based on the affinity of active: + /// 1) If `active` is Upstream + pub active_affinity: Affinity, + /// The saved horizontal position, during vertical movement. + /// + /// This should not be set by the IME; it will be tracked and handled by + /// the text field. + pub h_pos: Option, +} + +#[allow(clippy::len_without_is_empty)] +impl Selection { + /// Create a new `Selection` with the provided `anchor` and `active` positions. + /// + /// Both positions refer to UTF-8 byte indices in some text. + /// + /// If your selection is a caret, you can use [`Selection::caret`] instead. + pub fn new(anchor: usize, active: usize, active_affinity: Affinity) -> Selection { + Selection { + anchor, + active, + h_pos: None, + active_affinity, + } + } + + /// Create a new caret (zero-length selection) at the provided UTF-8 byte index. + /// + /// `index` must be a grapheme cluster boundary. + pub fn caret(index: usize, affinity: Affinity) -> Selection { + Selection { + anchor: index, + active: index, + h_pos: None, + active_affinity: affinity, + } + } + + /// Construct a new selection from this selection, with the provided `h_pos`. + /// + /// # Note + /// + /// `h_pos` is used to track the *pixel* location of the cursor when moving + /// vertically; lines may have available cursor positions at different + /// positions, and arrowing down and then back up should always result + /// in a cursor at the original starting location; doing this correctly + /// requires tracking this state. + /// + /// You *probably* don't need to use this, unless you are implementing a new + /// text field, or otherwise implementing vertical cursor motion, in which + /// case you will want to set this during vertical motion if it is not + /// already set. + pub fn with_h_pos(mut self, h_pos: Option) -> Self { + self.h_pos = h_pos; + self + } + + /// Create a new selection that is guaranteed to be valid for the provided + /// text. + #[must_use = "constrained constructs a new Selection"] + pub fn constrained(mut self, s: &str) -> Self { + let s_len = s.len(); + self.anchor = self.anchor.min(s_len); + self.active = self.active.min(s_len); + while !s.is_char_boundary(self.anchor) { + self.anchor += 1; + } + while !s.is_char_boundary(self.active) { + self.active += 1; + } + self + } + + /// Return the position of the upstream end of the selection. + /// + /// This is end with the lesser byte index. + /// + /// Because of bidirectional text, this is not necessarily "left". + pub fn min(&self) -> usize { + usize::min(self.anchor, self.active) + } + + /// Return the position of the downstream end of the selection. + /// + /// This is the end with the greater byte index. + /// + /// Because of bidirectional text, this is not necessarily "right". + pub fn max(&self) -> usize { + usize::max(self.anchor, self.active) + } + + /// The sequential range of the document represented by this selection. + /// + /// This is the range that would be replaced if text were inserted at this + /// selection. + pub fn range(&self) -> Range { + self.min()..self.max() + } + + /// The length, in bytes of the selected region. + /// + /// If the selection is a caret, this is `0`. + pub fn len(&self) -> usize { + if self.anchor > self.active { + self.anchor - self.active + } else { + self.active - self.anchor + } + } + + /// Returns `true` if the selection's length is `0`. + pub fn is_caret(&self) -> bool { + self.len() == 0 + } +} + +/// Distinguishes between two visually distinct locations with the same byte +/// index. +/// +/// Sometimes, a byte location in a document has two visual locations. For +/// example, the end of a soft-wrapped line and the start of the subsequent line +/// have different visual locations (and we want to be able to place an input +/// caret in either place!) but the same byte-wise location. This also shows up +/// in bidirectional text contexts. Affinity allows us to disambiguate between +/// these two visual locations. +/// +/// Note that in scenarios where soft line breaks interact with bidi text, this gets +/// more complicated. +/// +/// This also has an impact on rich text editing. +/// For example, if the cursor is in a region like `a|1`, where `a` is bold and `1` is not. +/// When editing, if we came from the start of the string, we should assume that the next +/// character will be bold, from the right italic. +#[derive(Copy, Clone, Debug, Hash, PartialEq)] +pub enum Affinity { + /// The position which has an apparent position "earlier" in the text. + /// For soft line breaks, this is the position at the end of the first line. + /// + /// For positions in-between bidi contexts, this is the position which is + /// related to the "outgoing" text section. E.g. for the string "abcDEF" (rendered `abcFED`), + /// with the cursor at "abc|DEF" with upstream affinity, the cursor would be rendered at the + /// position `abc|DEF` + Upstream, + /// The position which has a higher apparent position in the text. + /// For soft line breaks, this is the position at the beginning of the second line. + /// + /// For positions in-between bidi contexts, this is the position which is + /// related to the "incoming" text section. E.g. for the string "abcDEF" (rendered `abcFED`), + /// with the cursor at "abc|DEF" with downstream affinity, the cursor would be rendered at the + /// position `abcDEF|` + Downstream, +} + +/// Text which can have internal selections +pub trait Selectable: Sized + TextStorage { + type Cursor<'a>: EditableTextCursor + where + Self: 'a; + + /// Create a cursor with a reference to the text and a offset position. + /// + /// Returns None if the position isn't a codepoint boundary. + fn cursor(&self, position: usize) -> Option>; + /// Get slice of text at range. + fn slice(&self, range: Range) -> Option>; + + /// Get length of text (in bytes). + fn len(&self) -> usize; + + /// Get the previous word offset from the given offset, if it exists. + fn prev_word_offset(&self, offset: usize) -> Option; + + /// Get the next word offset from the given offset, if it exists. + fn next_word_offset(&self, offset: usize) -> Option; + + /// Get the next grapheme offset from the given offset, if it exists. + fn prev_grapheme_offset(&self, offset: usize) -> Option; + + /// Get the next grapheme offset from the given offset, if it exists. + fn next_grapheme_offset(&self, offset: usize) -> Option; + + /// Get the previous codepoint offset from the given offset, if it exists. + fn prev_codepoint_offset(&self, offset: usize) -> Option; + + /// Get the next codepoint offset from the given offset, if it exists. + fn next_codepoint_offset(&self, offset: usize) -> Option; + + /// Get the preceding line break offset from the given offset + fn preceding_line_break(&self, offset: usize) -> usize; + + /// Get the next line break offset from the given offset + fn next_line_break(&self, offset: usize) -> usize; + + /// Returns `true` if this text has 0 length. + fn is_empty(&self) -> bool; +} + +/// A cursor with convenience functions for moving through `EditableText`. +pub trait EditableTextCursor { + /// Set cursor position. + fn set(&mut self, position: usize); + + /// Get cursor position. + fn pos(&self) -> usize; + + /// Check if cursor position is at a codepoint boundary. + fn is_boundary(&self) -> bool; + + /// Move cursor to previous codepoint boundary, if it exists. + /// Returns previous codepoint as usize offset. + fn prev(&mut self) -> Option; + + /// Move cursor to next codepoint boundary, if it exists. + /// Returns current codepoint as usize offset. + fn next(&mut self) -> Option; + + /// Get the next codepoint after the cursor position, without advancing + /// the cursor. + fn peek_next_codepoint(&self) -> Option; + + /// Return codepoint preceding cursor offset and move cursor backward. + fn prev_codepoint(&mut self) -> Option; + + /// Return codepoint at cursor offset and move cursor forward. + fn next_codepoint(&mut self) -> Option; + + /// Return current offset if it's a boundary, else next. + fn at_or_next(&mut self) -> Option; + + /// Return current offset if it's a boundary, else previous. + fn at_or_prev(&mut self) -> Option; +} + +impl + TextStorage> Selectable for Str { + type Cursor<'a> = StringCursor<'a> where Self: 'a; + + fn cursor<'a>(&self, position: usize) -> Option { + let new_cursor = StringCursor { + text: self, + position, + }; + + if new_cursor.is_boundary() { + Some(new_cursor) + } else { + None + } + } + + fn slice(&self, range: Range) -> Option> { + self.get(range).map(Cow::from) + } + + fn len(&self) -> usize { + self.deref().len() + } + + fn prev_grapheme_offset(&self, from: usize) -> Option { + let mut c = GraphemeCursor::new(from, self.len(), true); + c.prev_boundary(self, 0).unwrap() + } + + fn next_grapheme_offset(&self, from: usize) -> Option { + let mut c = GraphemeCursor::new(from, self.len(), true); + c.next_boundary(self, 0).unwrap() + } + + fn prev_codepoint_offset(&self, from: usize) -> Option { + let mut c = self.cursor(from).unwrap(); + c.prev() + } + + fn next_codepoint_offset(&self, from: usize) -> Option { + let mut c = self.cursor(from).unwrap(); + if c.next().is_some() { + Some(c.pos()) + } else { + None + } + } + + fn prev_word_offset(&self, from: usize) -> Option { + let mut offset = from; + let mut passed_alphanumeric = false; + for prev_grapheme in self.get(0..from)?.graphemes(true).rev() { + let is_alphanumeric = prev_grapheme.chars().next()?.is_alphanumeric(); + if is_alphanumeric { + passed_alphanumeric = true; + } else if passed_alphanumeric { + return Some(offset); + } + offset -= prev_grapheme.len(); + } + None + } + + fn next_word_offset(&self, from: usize) -> Option { + let mut offset = from; + let mut passed_alphanumeric = false; + for next_grapheme in self.get(from..)?.graphemes(true) { + let is_alphanumeric = next_grapheme.chars().next()?.is_alphanumeric(); + if is_alphanumeric { + passed_alphanumeric = true; + } else if passed_alphanumeric { + return Some(offset); + } + offset += next_grapheme.len(); + } + Some(self.len()) + } + + fn is_empty(&self) -> bool { + self.deref().is_empty() + } + + fn preceding_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for byte in self.get(0..from).unwrap_or("").bytes().rev() { + if byte == 0x0a { + return offset; + } + offset -= 1; + } + + 0 + } + + fn next_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for char in self.get(from..).unwrap_or("").bytes() { + if char == 0x0a { + return offset; + } + offset += 1; + } + + self.len() + } +} + +/// A cursor type that implements `EditableTextCursor` for string types +#[derive(Debug)] +pub struct StringCursor<'a> { + text: &'a str, + position: usize, +} + +impl<'a> EditableTextCursor for StringCursor<'a> { + fn set(&mut self, position: usize) { + self.position = position; + } + + fn pos(&self) -> usize { + self.position + } + + fn is_boundary(&self) -> bool { + self.text.is_char_boundary(self.position) + } + + fn prev(&mut self) -> Option { + let current_pos = self.pos(); + + if current_pos == 0 { + None + } else { + let mut len = 1; + while !self.text.is_char_boundary(current_pos - len) { + len += 1; + } + self.set(self.pos() - len); + Some(self.pos()) + } + } + + fn next(&mut self) -> Option { + let current_pos = self.pos(); + + if current_pos == self.text.len() { + None + } else { + let b = self.text.as_bytes()[current_pos]; + self.set(current_pos + len_utf8_from_first_byte(b)); + Some(current_pos) + } + } + + fn peek_next_codepoint(&self) -> Option { + self.text[self.pos()..].chars().next() + } + + fn prev_codepoint(&mut self) -> Option { + if let Some(prev) = self.prev() { + self.text[prev..].chars().next() + } else { + None + } + } + + fn next_codepoint(&mut self) -> Option { + let current_index = self.pos(); + if self.next().is_some() { + self.text[current_index..].chars().next() + } else { + None + } + } + + fn at_or_next(&mut self) -> Option { + if self.is_boundary() { + Some(self.pos()) + } else { + self.next() + } + } + + fn at_or_prev(&mut self) -> Option { + if self.is_boundary() { + Some(self.pos()) + } else { + self.prev() + } + } +} + +pub fn len_utf8_from_first_byte(b: u8) -> usize { + match b { + b if b < 0x80 => 1, + b if b < 0xe0 => 2, + b if b < 0xf0 => 3, + _ => 4, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prev_codepoint_offset() { + let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}"); + assert_eq!(Some(6), a.prev_codepoint_offset(10)); + assert_eq!(Some(3), a.prev_codepoint_offset(6)); + assert_eq!(Some(1), a.prev_codepoint_offset(3)); + assert_eq!(Some(0), a.prev_codepoint_offset(1)); + assert_eq!(None, a.prev_codepoint_offset(0)); + let b = a.slice(1..10).unwrap().to_string(); + assert_eq!(Some(5), b.prev_codepoint_offset(9)); + assert_eq!(Some(2), b.prev_codepoint_offset(5)); + assert_eq!(Some(0), b.prev_codepoint_offset(2)); + assert_eq!(None, b.prev_codepoint_offset(0)); + } + + #[test] + fn next_codepoint_offset() { + let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}"); + assert_eq!(Some(10), a.next_codepoint_offset(6)); + assert_eq!(Some(6), a.next_codepoint_offset(3)); + assert_eq!(Some(3), a.next_codepoint_offset(1)); + assert_eq!(Some(1), a.next_codepoint_offset(0)); + assert_eq!(None, a.next_codepoint_offset(10)); + let b = a.slice(1..10).unwrap().to_string(); + assert_eq!(Some(9), b.next_codepoint_offset(5)); + assert_eq!(Some(5), b.next_codepoint_offset(2)); + assert_eq!(Some(2), b.next_codepoint_offset(0)); + assert_eq!(None, b.next_codepoint_offset(9)); + } + + #[test] + fn prev_next() { + let input = String::from("abc"); + let mut cursor = input.cursor(0).unwrap(); + assert_eq!(cursor.next(), Some(0)); + assert_eq!(cursor.next(), Some(1)); + assert_eq!(cursor.prev(), Some(1)); + assert_eq!(cursor.next(), Some(1)); + assert_eq!(cursor.next(), Some(2)); + } + + #[test] + fn peek_next_codepoint() { + let inp = String::from("$ยขโ‚ฌยฃ๐Ÿ’ถ"); + let mut cursor = inp.cursor(0).unwrap(); + assert_eq!(cursor.peek_next_codepoint(), Some('$')); + assert_eq!(cursor.peek_next_codepoint(), Some('$')); + assert_eq!(cursor.next_codepoint(), Some('$')); + assert_eq!(cursor.peek_next_codepoint(), Some('ยข')); + assert_eq!(cursor.prev_codepoint(), Some('$')); + assert_eq!(cursor.peek_next_codepoint(), Some('$')); + assert_eq!(cursor.next_codepoint(), Some('$')); + assert_eq!(cursor.next_codepoint(), Some('ยข')); + assert_eq!(cursor.peek_next_codepoint(), Some('โ‚ฌ')); + assert_eq!(cursor.next_codepoint(), Some('โ‚ฌ')); + assert_eq!(cursor.peek_next_codepoint(), Some('ยฃ')); + assert_eq!(cursor.next_codepoint(), Some('ยฃ')); + assert_eq!(cursor.peek_next_codepoint(), Some('๐Ÿ’ถ')); + assert_eq!(cursor.next_codepoint(), Some('๐Ÿ’ถ')); + assert_eq!(cursor.peek_next_codepoint(), None); + assert_eq!(cursor.next_codepoint(), None); + assert_eq!(cursor.peek_next_codepoint(), None); + } + + #[test] + fn prev_grapheme_offset() { + // A with ring, hangul, regional indicator "US" + let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}"); + assert_eq!(Some(9), a.prev_grapheme_offset(17)); + assert_eq!(Some(3), a.prev_grapheme_offset(9)); + assert_eq!(Some(0), a.prev_grapheme_offset(3)); + assert_eq!(None, a.prev_grapheme_offset(0)); + } + + #[test] + fn next_grapheme_offset() { + // A with ring, hangul, regional indicator "US" + let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}"); + assert_eq!(Some(3), a.next_grapheme_offset(0)); + assert_eq!(Some(9), a.next_grapheme_offset(3)); + assert_eq!(Some(17), a.next_grapheme_offset(9)); + assert_eq!(None, a.next_grapheme_offset(17)); + } + + #[test] + fn prev_word_offset() { + let a = String::from("Technically a word: เงฌ่—A\u{030a}\u{110b}\u{1161}"); + assert_eq!(Some(20), a.prev_word_offset(35)); + assert_eq!(Some(20), a.prev_word_offset(27)); + assert_eq!(Some(20), a.prev_word_offset(23)); + assert_eq!(Some(14), a.prev_word_offset(20)); + assert_eq!(Some(14), a.prev_word_offset(19)); + assert_eq!(Some(12), a.prev_word_offset(13)); + assert_eq!(None, a.prev_word_offset(12)); + assert_eq!(None, a.prev_word_offset(11)); + assert_eq!(None, a.prev_word_offset(0)); + } + + #[test] + fn next_word_offset() { + let a = String::from("Technically a word: เงฌ่—A\u{030a}\u{110b}\u{1161}"); + assert_eq!(Some(11), a.next_word_offset(0)); + assert_eq!(Some(11), a.next_word_offset(7)); + assert_eq!(Some(13), a.next_word_offset(11)); + assert_eq!(Some(18), a.next_word_offset(14)); + assert_eq!(Some(35), a.next_word_offset(18)); + assert_eq!(Some(35), a.next_word_offset(19)); + assert_eq!(Some(35), a.next_word_offset(20)); + assert_eq!(Some(35), a.next_word_offset(26)); + assert_eq!(Some(35), a.next_word_offset(35)); + } + + #[test] + fn preceding_line_break() { + let a = String::from("Technically\na word:\n เงฌ่—A\u{030a}\n\u{110b}\u{1161}"); + assert_eq!(0, a.preceding_line_break(0)); + assert_eq!(0, a.preceding_line_break(11)); + assert_eq!(12, a.preceding_line_break(12)); + assert_eq!(12, a.preceding_line_break(13)); + assert_eq!(20, a.preceding_line_break(21)); + assert_eq!(31, a.preceding_line_break(31)); + assert_eq!(31, a.preceding_line_break(34)); + + let b = String::from("Technically a word: เงฌ่—A\u{030a}\u{110b}\u{1161}"); + assert_eq!(0, b.preceding_line_break(0)); + assert_eq!(0, b.preceding_line_break(11)); + assert_eq!(0, b.preceding_line_break(13)); + assert_eq!(0, b.preceding_line_break(21)); + } + + #[test] + fn next_line_break() { + let a = String::from("Technically\na word:\n เงฌ่—A\u{030a}\n\u{110b}\u{1161}"); + assert_eq!(11, a.next_line_break(0)); + assert_eq!(11, a.next_line_break(11)); + assert_eq!(19, a.next_line_break(13)); + assert_eq!(30, a.next_line_break(21)); + assert_eq!(a.len(), a.next_line_break(31)); + + let b = String::from("Technically a word: เงฌ่—A\u{030a}\u{110b}\u{1161}"); + assert_eq!(b.len(), b.next_line_break(0)); + assert_eq!(b.len(), b.next_line_break(11)); + assert_eq!(b.len(), b.next_line_break(13)); + assert_eq!(b.len(), b.next_line_break(19)); + } +} diff --git a/parley/src/editor/store.rs b/parley/src/editor/store.rs new file mode 100644 index 00000000..21e77041 --- /dev/null +++ b/parley/src/editor/store.rs @@ -0,0 +1,88 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Storing text. + +use std::{ops::Deref, sync::Arc}; + +use crate::builder::RangedBuilder; + +use crate::ArcStr; + +use super::layout::TextBrush; + +#[derive(Copy, Clone)] +// TODO: Implement links +pub struct Link; + +/// Text which can be displayed. +pub trait TextStorage: 'static { + fn as_str(&self) -> &str; + /// If this `TextStorage` object manages style spans, it should implement + /// this method and update the provided builder with its spans, as required. + /// + /// This takes `&self`, as we needed to call `Self::as_str` to get the value stored in + /// the `RangedBuilder` + #[allow(unused_variables)] + fn add_attributes<'b>( + &self, + builder: RangedBuilder<'b, TextBrush>, + ) -> RangedBuilder<'b, TextBrush> { + builder + } + + /// Any additional [`Link`] attributes on this text. + /// + /// If this `TextStorage` object manages link attributes, it should implement this + /// method and return any attached [`Link`]s. + /// + /// Unlike other attributes, links are managed in Masonry, not in [`piet`]; as such they + /// require a separate API. + /// + /// [`Link`]: super::attribute::Link + /// [`piet`]: https://docs.rs/piet + fn links(&self) -> &[Link] { + &[] + } + + /// Determines quickly whether two text objects have the same content. + /// + /// To allow for faster checks, this method is allowed to return false negatives. + fn maybe_eq(&self, other: &Self) -> bool; +} + +impl TextStorage for &'static str { + fn as_str(&self) -> &str { + self + } + fn maybe_eq(&self, other: &Self) -> bool { + self == other + } +} + +impl TextStorage for ArcStr { + fn as_str(&self) -> &str { + self.deref() + } + fn maybe_eq(&self, other: &Self) -> bool { + self == other + } +} + +impl TextStorage for String { + fn as_str(&self) -> &str { + self.deref() + } + fn maybe_eq(&self, other: &Self) -> bool { + self == other + } +} + +impl TextStorage for Arc { + fn as_str(&self) -> &str { + self.deref() + } + fn maybe_eq(&self, other: &Self) -> bool { + self == other + } +} diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 5aa278a6..d874a3d4 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -12,6 +12,12 @@ compile_error!("parley requires either the `std` or `libm` feature to be enabled extern crate alloc; +/// A reference counted string slice. +/// +/// This is a data-friendly way to represent strings in Masonry. Unlike `String` +/// it cannot be mutated, but unlike `String` it can be cheaply cloned. +pub type ArcStr = std::sync::Arc; + pub use fontique; pub use swash; @@ -25,6 +31,7 @@ mod util; pub mod builder; pub mod context; +pub mod editor; pub mod layout; pub mod style; diff --git a/parley/src/style/mod.rs b/parley/src/style/mod.rs index c7e76513..7b106e47 100644 --- a/parley/src/style/mod.rs +++ b/parley/src/style/mod.rs @@ -6,6 +6,9 @@ mod brush; mod font; +pub(crate) const DEFAULT_FONT_SIZE: f32 = 16.0; +pub(crate) const DEFAULT_LINE_HEIGHT: f32 = 1.2; + pub use brush::*; pub use font::{ FontFamily, FontFeature, FontSettings, FontStack, FontStretch, FontStyle, FontVariation, @@ -112,7 +115,7 @@ impl<'a, B: Brush> Default for TextStyle<'a, B> { fn default() -> Self { TextStyle { font_stack: FontStack::Source("sans-serif"), - font_size: 16.0, + font_size: DEFAULT_FONT_SIZE, font_stretch: Default::default(), font_style: Default::default(), font_weight: Default::default(), @@ -128,7 +131,7 @@ impl<'a, B: Brush> Default for TextStyle<'a, B> { strikethrough_offset: Default::default(), strikethrough_size: Default::default(), strikethrough_brush: Default::default(), - line_height: 1.2, + line_height: DEFAULT_LINE_HEIGHT, word_spacing: Default::default(), letter_spacing: Default::default(), } diff --git a/parley/src/util.rs b/parley/src/util.rs index b23f3f74..a96f7d96 100644 --- a/parley/src/util.rs +++ b/parley/src/util.rs @@ -10,3 +10,29 @@ pub fn nearly_eq(x: f32, y: f32) -> bool { pub fn nearly_zero(x: f32) -> bool { nearly_eq(x, 0.) } + +/// Panic in debug and `tracing::error` in release mode. +/// +/// This macro is in some way a combination of `panic` and `debug_assert`, +/// but it will log the provided message instead of ignoring it in release builds. +/// +/// It's useful when a backtrace would aid debugging but a crash can be avoided in release. +#[macro_export] +macro_rules! debug_panic { + () => { ... }; + ($msg:expr) => { + if cfg!(debug_assertions) { + panic!($msg); + } else { + tracing::error!($msg); + } + }; + ($msg:expr,) => { debug_panic!($msg) }; + ($fmt:expr, $($arg:tt)+) => { + if cfg!(debug_assertions) { + panic!($fmt, $($arg)*); + } else { + tracing::error!($fmt, $($arg)*); + } + }; +}