From 60b99d116bcb99f8a6e481e941ecee1f2492c121 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 14 Aug 2024 21:32:18 -0400 Subject: [PATCH 01/18] Add next_line/prev_line methods to Cursor Shows how to implement the next/previous line behaviors for cursors. Note that this is untested. --- parley/src/layout/cursor.rs | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index 83064e2f..fcb4a801 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -155,6 +155,44 @@ impl Cursor { pub fn is_trailing(&self) -> bool { self.text_end == self.insert_point } + + /// Given the layout that generated this cursor, return a new cursor + /// for the corresponding position on the next line. + /// + /// If `h_pos` is provided, then it will be used as the horizontal offset + /// for computing the position on the next line. + /// + /// Returns `None` if the cursor should remain in its current position. + pub fn next_line(&self, layout: &Layout, h_pos: Option) -> Option { + move_to_line(layout, self, h_pos, self.path.line_index.checked_sub(1)?) + } + + /// Given the layout that generated this cursor, return a new cursor + /// for the corresponding position on the previous line. + /// + /// If `h_pos` is provided, then it will be used as the horizontal offset + /// for computing the position on the previous line. + /// + /// Returns `None` if the cursor should remain in its current position. + pub fn prev_line(&self, layout: &Layout, h_pos: Option) -> Option { + move_to_line(layout, self, h_pos, self.path.line_index.checked_add(1)?) + } +} + +fn move_to_line( + layout: &Layout, + cursor: &Cursor, + h_pos: Option, + line_index: usize, +) -> Option { + let line = layout.get(line_index)?; + let metrics = line.metrics(); + let y = metrics.baseline - metrics.line_height * 0.5; + Some(Cursor::from_point( + layout, + h_pos.unwrap_or(cursor.offset), + y, + )) } /// Index based path to a cluster. From 54d08a0575361318556d3dbd067268fdeabd6186 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 14 Aug 2024 21:36:17 -0400 Subject: [PATCH 02/18] swap ops --- parley/src/layout/cursor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index fcb4a801..a91f6be8 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -164,7 +164,7 @@ impl Cursor { /// /// Returns `None` if the cursor should remain in its current position. pub fn next_line(&self, layout: &Layout, h_pos: Option) -> Option { - move_to_line(layout, self, h_pos, self.path.line_index.checked_sub(1)?) + move_to_line(layout, self, h_pos, self.path.line_index.checked_add(1)?) } /// Given the layout that generated this cursor, return a new cursor @@ -175,7 +175,7 @@ impl Cursor { /// /// Returns `None` if the cursor should remain in its current position. pub fn prev_line(&self, layout: &Layout, h_pos: Option) -> Option { - move_to_line(layout, self, h_pos, self.path.line_index.checked_add(1)?) + move_to_line(layout, self, h_pos, self.path.line_index.checked_sub(1)?) } } From 272ca76eefec9d787263f4127df33fc921044bd2 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 16 Aug 2024 12:32:27 -0400 Subject: [PATCH 03/18] Squashed commit of the following: commit 6b11f213a7f47548d60ee831bbe195f6212f0218 Author: Chad Brokaw Date: Fri Aug 16 12:31:34 2024 -0400 selection and editing commit 951fc961a1a144a706ef5d6abbe8ea9bafd44881 Author: Chad Brokaw Date: Fri Aug 16 10:13:13 2024 -0400 checkpoint commit ecd232d795fac5034516341778eec25a14897f55 Author: Chad Brokaw Date: Thu Aug 15 21:30:27 2024 -0400 checkpoint --- .vscode/launch.json | 157 ++ Cargo.lock | 2603 +++++++++++++++++++++++++---- Cargo.toml | 1 + examples/vello_editor/Cargo.toml | 18 + examples/vello_editor/src/main.rs | 223 +++ examples/vello_editor/src/text.rs | 249 +++ parley/src/layout/cursor.rs | 336 +++- parley/src/layout/line/greedy.rs | 15 +- parley/src/layout/mod.rs | 4 +- 9 files changed, 3257 insertions(+), 349 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 examples/vello_editor/Cargo.toml create mode 100644 examples/vello_editor/src/main.rs create mode 100644 examples/vello_editor/src/text.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0a2ffce1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,157 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'fontique'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=fontique" + ], + "filter": { + "name": "fontique", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'parley'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=parley" + ], + "filter": { + "name": "parley", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'tiny_skia_render'", + "cargo": { + "args": [ + "build", + "--bin=tiny_skia_render", + "--package=tiny_skia_render" + ], + "filter": { + "name": "tiny_skia_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'tiny_skia_render'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=tiny_skia_render", + "--package=tiny_skia_render" + ], + "filter": { + "name": "tiny_skia_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'swash_render'", + "cargo": { + "args": [ + "build", + "--bin=swash_render", + "--package=swash_render" + ], + "filter": { + "name": "swash_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'swash_render'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=swash_render", + "--package=swash_render" + ], + "filter": { + "name": "swash_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'vello_render'", + "cargo": { + "args": [ + "build", + "--bin=vello_render", + "--package=vello_render" + ], + "filter": { + "name": "vello_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'vello_render'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=vello_render", + "--package=vello_render" + ], + "filter": { + "name": "vello_render", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3bfbdb82..44b3459c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "adler" version = "1.0.2" @@ -15,6 +31,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -26,6 +43,48 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.5.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arrayref" version = "0.3.7" @@ -38,12 +97,48 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -56,6 +151,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block2" version = "0.5.1" @@ -65,6 +166,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytemuck" version = "1.16.0" @@ -82,7 +189,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", ] [[package]] @@ -91,12 +198,133 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.5.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -167,6 +395,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "d3d12" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" +dependencies = [ + "bitflags 2.5.0", + "libloading 0.8.5", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.4" @@ -175,9 +432,39 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.5", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + [[package]] name = "dwrote" version = "0.11.0" @@ -192,6 +479,31 @@ dependencies = [ "wio", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "euclid" +version = "0.22.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" +dependencies = [ + "num-traits", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -213,9 +525,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf6aa1de86490d8e39e04589bd04eb5953cc2a5ef0c25e389e807f44fd24e41" +checksum = "34fd7136aca682873d859ef34494ab1a7d3f57ecd485ed40eb6437ee8c85aa29" dependencies = [ "bytemuck", ] @@ -272,7 +584,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", ] [[package]] @@ -282,114 +594,273 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] -name = "hashbrown" -version = "0.14.5" +name = "futures-core" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] -name = "icu_collections" -version = "1.5.0" +name = "futures-intrusive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", + "futures-core", + "lock_api", + "parking_lot", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "gethostname" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "libc", + "windows-targets 0.48.5", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +name = "getrandom" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", + "cfg-if", + "libc", + "wasi", ] [[package]] -name = "icu_locid_transform_data" -version = "1.5.0" +name = "gl_generator" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] [[package]] -name = "icu_properties" -version = "1.5.1" +name = "glow" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "icu_properties_data" -version = "1.5.0" +name = "glutin_wgl_sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] [[package]] -name = "icu_provider" -version = "1.5.0" +name = "gpu-alloc" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", + "bitflags 2.5.0", + "gpu-alloc-types", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "gpu-alloc-types" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bitflags 2.5.0", ] [[package]] -name = "image" -version = "0.25.1" +name = "gpu-allocator" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" +dependencies = [ + "bitflags 2.5.0", + "gpu-descriptor-types", + "hashbrown", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.5.0", + "com", + "libc", + "libloading 0.8.5", + "thiserror", + "widestring", + "winapi", +] + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", @@ -397,6 +868,73 @@ dependencies = [ "png", ] +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.5", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kurbo" version = "0.11.0" @@ -420,378 +958,1841 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "litemap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "metal" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" +dependencies = [ + "bitflags 2.5.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "naga" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.5.0", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.5.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.5.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.5.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.3", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parley" +version = "0.1.0" +dependencies = [ + "fontique", + "peniko", + "skrifa", + "swash", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peniko" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c28d7294093837856bb80ad191cc46a2fcec8a30b43b7a3b0285325f0a917a9" +dependencies = [ + "kurbo", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b8af39d1f23869711ad4cea5e7835a20daa987f80232f7f2a2374d648ca64d" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "skrifa" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab45fb68b53576a43d4fc0e9ec8ea64e29a4d2cc7f44506964cb75f288222e9" +dependencies = [ + "bytemuck", + "core_maths", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.5.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +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", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "svg_fmt" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" + +[[package]] +name = "swash" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682a612b50baf09e8a039547ecf49e6c155690dcb751b1bcb19c93cdeb3d42d4" +dependencies = [ + "read-fonts", + "yazi", + "zeno", +] + +[[package]] +name = "swash_render" +version = "0.1.0" +dependencies = [ + "image", + "parley", + "peniko", + "skrifa", + "swash", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny_skia_render" +version = "0.1.0" +dependencies = [ + "parley", + "peniko", + "skrifa", + "tiny-skia", +] + +[[package]] +name = "tinystr" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[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", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "ttf-parser" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-script" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "vello" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861c12258ed7e72762765e2c88a07bb528040ec4e5f87514d65b19b29a7cccf0" +dependencies = [ + "bytemuck", + "futures-intrusive", + "log", + "peniko", + "raw-window-handle", + "skrifa", + "static_assertions", + "thiserror", + "vello_encoding", + "vello_shaders", + "wgpu", +] + +[[package]] +name = "vello_editor" +version = "0.1.0" +dependencies = [ + "anyhow", + "parley", + "peniko", + "pollster", + "vello", + "winit", +] + +[[package]] +name = "vello_encoding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d73777327877fa824a45c7195f850390dd3f91feb15f47d331db1fc01abf6d" +dependencies = [ + "bytemuck", + "guillotiere", + "peniko", + "skrifa", + "smallvec", +] + +[[package]] +name = "vello_shaders" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ab6bcb2b079c3cf57e964d1ba0b1f08901284be1c7f5cba34d3e0e08154bce" +dependencies = [ + "bytemuck", + "naga", + "thiserror", + "vello_encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.65", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wayland-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.5.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.5.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.5", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" +dependencies = [ + "bitflags 2.5.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "log" -version = "0.4.21" +name = "windows" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] [[package]] -name = "memmap2" -version = "0.9.4" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "libc", + "windows-targets 0.52.6", ] [[package]] -name = "miniz_oxide" -version = "0.7.3" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "adler", - "simd-adler32", + "windows-targets 0.42.2", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "autocfg", + "windows-targets 0.52.6", ] [[package]] -name = "objc-sys" -version = "0.3.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] [[package]] -name = "objc2" -version = "0.5.2" +name = "windows-targets" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "objc-sys", - "objc2-encode", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] -name = "objc2-encode" -version = "4.0.3" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] [[package]] -name = "objc2-foundation" -version = "0.2.2" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "bitflags 2.5.0", - "block2", - "libc", - "objc2", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "once_cell" -version = "1.19.0" +name = "windows_aarch64_gnullvm" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] -name = "parley" -version = "0.1.0" -dependencies = [ - "fontique", - "peniko", - "skrifa", - "swash", -] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "peniko" -version = "0.1.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caaf7fec601d640555d9a4cab7343eba1e1c7a5a71c9993ff63b4c26bc5d50c5" -dependencies = [ - "kurbo", - "smallvec", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "png" -version = "0.17.13" +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] -name = "proc-macro2" -version = "1.0.83" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" -dependencies = [ - "unicode-ident", -] +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "quote" -version = "1.0.36" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -dependencies = [ - "proc-macro2", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "read-fonts" -version = "0.19.1" +name = "windows_i686_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4749db2bd1c853db31a7ae5ee2fc6c30bbddce353ea8fedf673fed187c68c7" -dependencies = [ - "bytemuck", - "font-types", -] +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] -name = "roxmltree" -version = "0.19.0" +name = "windows_i686_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "serde" -version = "1.0.202" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" -dependencies = [ - "serde_derive", -] +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "serde_derive" -version = "1.0.202" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "simd-adler32" -version = "0.3.7" +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] -name = "skrifa" -version = "0.19.1" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dbed6a0c9addb84c2c8f66f35f504bb875b901e2a022419173e6ee2adfd0fb4" -dependencies = [ - "bytemuck", - "core_maths", - "read-fonts", -] +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "smallvec" -version = "1.13.2" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "windows_x86_64_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] -name = "strict-num" -version = "0.1.1" +name = "windows_x86_64_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "swash" -version = "0.1.16" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682a612b50baf09e8a039547ecf49e6c155690dcb751b1bcb19c93cdeb3d42d4" -dependencies = [ - "read-fonts", - "yazi", - "zeno", -] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "swash_render" -version = "0.1.0" -dependencies = [ - "image", - "parley", - "peniko", - "skrifa", - "swash", -] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] -name = "syn" -version = "2.0.65" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "synstructure" -version = "0.13.1" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "thiserror" -version = "1.0.61" +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" -dependencies = [ - "thiserror-impl", -] +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] -name = "thiserror-impl" -version = "1.0.61" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "tiny-skia" -version = "0.11.4" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png", - "tiny-skia-path", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "tiny-skia-path" -version = "0.11.4" +name = "winit" +version = "0.30.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67" dependencies = [ - "arrayref", + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.5.0", + "block2", "bytemuck", - "strict-num", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", ] [[package]] -name = "tiny_skia_render" -version = "0.1.0" +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ - "parley", - "peniko", - "skrifa", - "tiny-skia", + "memchr", ] [[package]] -name = "tinystr" -version = "0.7.5" +name = "wio" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" dependencies = [ - "displaydoc", - "zerovec", + "winapi", ] [[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-script" -version = "0.5.6" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "version_check" -version = "0.9.4" +name = "x11-dl" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] [[package]] -name = "winapi" -version = "0.3.9" +name = "x11rb" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.5", + "once_cell", + "rustix", + "x11rb-protocol", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "x11rb-protocol" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "xcursor" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] -name = "wio" -version = "0.2.2" +name = "xkbcommon-dl" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "winapi", + "bitflags 2.5.0", + "dlib", + "log", + "once_cell", + "xkeysym", ] [[package]] -name = "writeable" -version = "0.5.5" +name = "xkeysym" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "yazi" @@ -819,7 +2820,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", "synstructure", ] @@ -846,7 +2847,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", ] [[package]] @@ -866,7 +2867,7 @@ checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", "synstructure", ] @@ -889,5 +2890,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", ] diff --git a/Cargo.toml b/Cargo.toml index 734f63c3..b96d2d42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "parley", "examples/tiny_skia_render", "examples/swash_render", + "examples/vello_editor", ] [workspace.package] diff --git a/examples/vello_editor/Cargo.toml b/examples/vello_editor/Cargo.toml new file mode 100644 index 00000000..c58cae96 --- /dev/null +++ b/examples/vello_editor/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "vello_editor" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +vello = "0.2.1" +anyhow = "1.0.86" +pollster = "0.3.0" +winit = "0.30.3" +parley = { workspace = true, default-features = true } +peniko = { workspace = true } + +[lints] +workspace = true diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs new file mode 100644 index 00000000..6d55208e --- /dev/null +++ b/examples/vello_editor/src/main.rs @@ -0,0 +1,223 @@ +// Copyright 2024 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::Result; +use std::num::NonZeroUsize; +use std::sync::Arc; +use vello::kurbo::{Affine, Circle, Ellipse, Line, RoundedRect, Stroke}; +use vello::peniko::Color; +use vello::util::{RenderContext, RenderSurface}; +use vello::wgpu; +use vello::{AaConfig, Renderer, RendererOptions, Scene}; +use winit::application::ApplicationHandler; +use winit::dpi::LogicalSize; +use winit::event::*; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::window::Window; + +mod text; + +// Simple struct to hold the state of the renderer +pub struct ActiveRenderState<'s> { + // The fields MUST be in this order, so that the surface is dropped before the window + surface: RenderSurface<'s>, + window: Arc, +} + +enum RenderState<'s> { + Active(ActiveRenderState<'s>), + // Cache a window so that it can be reused when the app is resumed after being suspended + Suspended(Option>), +} + +struct SimpleVelloApp<'s> { + // The vello RenderContext which is a global context that lasts for the + // lifetime of the application + context: RenderContext, + + // An array of renderers, one per wgpu device + renderers: Vec>, + + // State for our example where we store the winit Window and the wgpu Surface + state: RenderState<'s>, + + // A vello Scene which is a data structure which allows one to build up a + // description a scene to be drawn (with paths, fills, images, text, etc) + // which is then passed to a renderer for rendering + scene: Scene, + + // Our text state object + text: text::Text, +} + +impl<'s> ApplicationHandler for SimpleVelloApp<'s> { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let RenderState::Suspended(cached_window) = &mut self.state else { + return; + }; + + // Get the winit window cached in a previous Suspended event or else create a new window + let window = cached_window + .take() + .unwrap_or_else(|| create_winit_window(event_loop)); + + // Create a vello Surface + let size = window.inner_size(); + let surface_future = self.context.create_surface( + window.clone(), + size.width, + size.height, + wgpu::PresentMode::AutoVsync, + ); + let surface = pollster::block_on(surface_future).expect("Error creating surface"); + self.text.update_layout(size.width as _, 1.0); + + // Create a vello Renderer for the surface (using its device id) + self.renderers + .resize_with(self.context.devices.len(), || None); + self.renderers[surface.dev_id] + .get_or_insert_with(|| create_vello_renderer(&self.context, &surface)); + + // Save the Window and Surface to a state variable + self.state = RenderState::Active(ActiveRenderState { window, surface }); + + event_loop.set_control_flow(ControlFlow::Poll); + } + + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + if let RenderState::Active(state) = &self.state { + self.state = RenderState::Suspended(Some(state.window.clone())); + } + event_loop.set_control_flow(ControlFlow::Wait); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + // Ignore the event (return from the function) if + // - we have no render_state + // - OR the window id of the event doesn't match the window id of our render_state + // + // Else extract a mutable reference to the render state from its containing option for use below + let render_state = match &mut self.state { + RenderState::Active(state) if state.window.id() == window_id => state, + _ => return, + }; + + self.text.handle_event(&event); + render_state.window.request_redraw(); + render_state + .window + .set_cursor(winit::window::Cursor::Icon(winit::window::CursorIcon::Text)); + + match event { + // Exit the event loop when a close is requested (e.g. window's close button is pressed) + WindowEvent::CloseRequested => event_loop.exit(), + + // Resize the surface when the window is resized + WindowEvent::Resized(size) => { + self.context + .resize_surface(&mut render_state.surface, size.width, size.height); + render_state.window.request_redraw(); + self.text.update_layout(size.width as _, 1.0); + } + + // This is where all the rendering happens + WindowEvent::RedrawRequested => { + // Empty the scene of objects to draw. You could create a new Scene each time, but in this case + // the same Scene is reused so that the underlying memory allocation can also be reused. + self.scene.reset(); + + self.text.draw(&mut self.scene); + // Re-add the objects to draw to the scene. + // add_shapes_to_scene(&mut self.scene); + + // Get the RenderSurface (surface + config) + let surface = &render_state.surface; + + // Get the window size + let width = surface.config.width; + let height = surface.config.height; + + // Get a handle to the device + let device_handle = &self.context.devices[surface.dev_id]; + + // Get the surface's texture + let surface_texture = surface + .surface + .get_current_texture() + .expect("failed to get surface texture"); + + // Render to the surface's texture + self.renderers[surface.dev_id] + .as_mut() + .unwrap() + .render_to_surface( + &device_handle.device, + &device_handle.queue, + &self.scene, + &surface_texture, + &vello::RenderParams { + base_color: Color::rgb8(30, 30, 30), // Background color + width, + height, + antialiasing_method: AaConfig::Msaa16, + }, + ) + .expect("failed to render to surface"); + + // Queue the texture to be presented on the surface + surface_texture.present(); + + device_handle.device.poll(wgpu::Maintain::Poll); + } + _ => {} + } + } +} + +fn main() -> Result<()> { + // Setup a bunch of state: + let mut app = SimpleVelloApp { + context: RenderContext::new(), + renderers: vec![], + state: RenderState::Suspended(None), + scene: Scene::new(), + text: text::Text::default(), + }; + + app.text.set_text(text::LOREM); + + // Create and run a winit event loop + let event_loop = EventLoop::new()?; + event_loop + .run_app(&mut app) + .expect("Couldn't run event loop"); + Ok(()) +} + +/// Helper function that creates a Winit window and returns it (wrapped in an Arc for sharing between threads) +fn create_winit_window(event_loop: &ActiveEventLoop) -> Arc { + let attr = Window::default_attributes() + .with_inner_size(LogicalSize::new(1044, 800)) + .with_resizable(true) + .with_title("Vello Text Editor"); + Arc::new(event_loop.create_window(attr).unwrap()) +} + +/// Helper function that creates a vello `Renderer` for a given `RenderContext` and `RenderSurface` +fn create_vello_renderer(render_cx: &RenderContext, surface: &RenderSurface) -> Renderer { + Renderer::new( + &render_cx.devices[surface.dev_id].device, + RendererOptions { + surface_format: Some(surface.format), + use_cpu: false, + antialiasing_support: vello::AaSupport::all(), + num_init_threads: NonZeroUsize::new(1), + }, + ) + .expect("Couldn't create renderer") +} diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs new file mode 100644 index 00000000..f5dc858f --- /dev/null +++ b/examples/vello_editor/src/text.rs @@ -0,0 +1,249 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use parley::{layout::cursor::Selection, layout::PositionedLayoutItem, FontContext}; +use peniko::{ + kurbo::{Affine, Stroke}, + Color, Fill, +}; +use vello::Scene; +use winit::{ + event::{Modifiers, WindowEvent}, + keyboard::{KeyCode, PhysicalKey}, +}; + +type LayoutContext = parley::LayoutContext; +type Layout = parley::Layout; + +const INSET: f32 = 32.0; + +#[derive(Copy, Clone, Debug)] +pub enum ActiveText<'a> { + FocusedCluster(&'a str), + Selection(&'a str), +} + +#[derive(Default)] +pub struct Text { + font_cx: FontContext, + layout_cx: LayoutContext, + buffer: String, + layout: Layout, + selection: Selection, + pointer_down: bool, + cursor_pos: (f32, f32), + modifiers: Option, + width: f32, +} + +impl Text { + pub fn set_text(&mut self, text: &str) { + self.buffer.clear(); + self.buffer.push_str(text); + } + + pub fn update_layout(&mut self, width: f32, scale: f32) { + let mut builder = self + .layout_cx + .ranged_builder(&mut self.font_cx, &self.buffer, scale); + builder.push_default(&parley::style::StyleProperty::FontSize(32.0)); + builder.push_default(&parley::style::StyleProperty::LineHeight(1.2)); + builder.push_default(&parley::style::StyleProperty::FontStack( + parley::style::FontStack::Source("system-ui"), + )); + builder.build_into(&mut self.layout); + self.layout + .break_all_lines(Some(width - INSET * 2.0), parley::layout::Alignment::Start); + self.width = width; + } + + pub fn active_text(&self) -> ActiveText { + if self.selection.is_collapsed() { + let range = self.selection.focus().text_start..self.selection.focus().text_end; + ActiveText::FocusedCluster(&self.buffer[range]) + } else { + ActiveText::Selection(&self.buffer[self.selection.text_range()]) + } + } + + pub fn handle_event(&mut self, event: &WindowEvent) { + match event { + WindowEvent::ModifiersChanged(modifiers) => { + self.modifiers = Some(*modifiers); + } + WindowEvent::KeyboardInput { event, .. } => { + if !event.state.is_pressed() { + return; + } + let shift = self + .modifiers + .map(|mods| mods.state().shift_key()) + .unwrap_or_default(); + match event.physical_key { + PhysicalKey::Code(code) => match code { + KeyCode::ArrowLeft => { + self.selection = self.selection.prev_logical(&self.layout, shift); + } + KeyCode::ArrowRight => { + self.selection = self.selection.next_logical(&self.layout, shift); + } + KeyCode::ArrowUp => { + self.selection = self.selection.prev_line(&self.layout, shift); + } + KeyCode::ArrowDown => { + self.selection = self.selection.next_line(&self.layout, shift); + } + KeyCode::Home => { + self.selection = self.selection.line_start(&self.layout, shift); + } + KeyCode::End => { + self.selection = self.selection.line_end(&self.layout, shift); + } + KeyCode::Delete => { + let start = if self.selection.is_collapsed() { + let range = self.selection.focus().text_start + ..self.selection.focus().text_end; + let start = range.start; + self.buffer.replace_range(range, ""); + start + } else { + self.delete_current_selection().unwrap() + }; + self.update_layout(self.width, 1.0); + self.selection = Selection::from_byte_index(&self.layout, start); + } + KeyCode::Backspace => { + let start = if self.selection.is_collapsed() { + let end = self.selection.focus().text_start; + if let Some((start, _)) = + self.buffer[..end].char_indices().rev().next() + { + self.buffer.replace_range(start..end, ""); + self.update_layout(self.width, 1.0); + self.selection = + Selection::from_byte_index(&self.layout, start); + Some(start) + } else { + None + } + } else { + self.delete_current_selection() + }; + if let Some(start) = start { + self.update_layout(self.width, 1.0); + self.selection = Selection::from_byte_index(&self.layout, start); + } + } + _ => { + if let Some(text) = &event.text { + let start = self + .delete_current_selection() + .unwrap_or_else(|| self.selection.focus().text_start); + self.buffer.insert_str(start, text); + self.update_layout(self.width, 1.0); + self.selection = + Selection::from_byte_index(&self.layout, start + text.len()); + } + } + }, + _ => {} + } + + println!("Active text: {:?}", self.active_text()); + } + WindowEvent::MouseInput { state, button, .. } => { + if *button == winit::event::MouseButton::Left { + self.pointer_down = state.is_pressed(); + if self.pointer_down { + self.selection = Selection::from_point( + &self.layout, + self.cursor_pos.0, + self.cursor_pos.1, + ); + println!("Active text: {:?}", self.active_text()); + } + } + } + WindowEvent::CursorMoved { position, .. } => { + self.cursor_pos = (position.x as f32 - INSET, position.y as f32 - INSET); + if self.pointer_down { + self.selection = self.selection.extend_to_point( + &self.layout, + self.cursor_pos.0, + self.cursor_pos.1, + ); + println!("Active text: {:?}", self.active_text()); + } + } + _ => {} + } + } + + fn delete_current_selection(&mut self) -> Option { + if !self.selection.is_collapsed() { + let range = self.selection.text_range(); + let start = range.start; + self.buffer.replace_range(range, ""); + Some(start) + } else { + None + } + } + + pub fn draw(&self, scene: &mut Scene) { + let transform = Affine::translate((INSET as f64, INSET as f64)); + self.selection.visual_regions_with(&self.layout, |rect| { + scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); + }); + if let Some(cursor) = self.selection.visual_caret(&self.layout) { + scene.stroke(&Stroke::new(1.5), transform, Color::WHITE, None, &cursor); + }; + for line in self.layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { + continue; + }; + let mut x = glyph_run.offset(); + let y = glyph_run.baseline(); + let run = glyph_run.run(); + let font = run.font(); + let font_size = run.font_size(); + let synthesis = run.synthesis(); + let glyph_xform = synthesis + .skew() + .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0)); + let style = glyph_run.style(); + let coords = run + .normalized_coords() + .iter() + .map(|coord| vello::skrifa::instance::NormalizedCoord::from_bits(*coord)) + .collect::>(); + scene + .draw_glyphs(font) + .brush(Color::WHITE) + .hint(true) + .transform(transform) + .glyph_transform(glyph_xform) + .font_size(font_size) + .normalized_coords(&coords) + .draw( + Fill::NonZero, + glyph_run.glyphs().map(|glyph| { + let gx = x + glyph.x; + let gy = y - glyph.y; + x += glyph.advance; + vello::glyph::Glyph { + id: glyph.id as _, + x: gx, + y: gy, + } + }), + ); + } + } + } +} + +pub const LOREM: &str = r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl. + +Phasellus in viverra dolor, vitae facilisis est. Maecenas malesuada massa vel ultricies feugiat. Vivamus venenatis et התעשייה בנושא האינטרנט nibh nec pharetra. Phasellus vestibulum elit enim, nec scelerisque orci faucibus id. Vivamus consequat purus sit amet orci egestas, non iaculis massa porttitor. Vestibulum ut eros leo. In fermentum convallis magna in finibus. Donec justo leo, maximus ac laoreet id, volutpat ut elit. Mauris sed leo non neque laoreet faucibus. Aliquam orci arcu, faucibus in molestie eget, ornare non dui. Donec volutpat nulla in fringilla elementum. Aliquam vitae ante egestas ligula tempus vestibulum sit amet sed ante. "; diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index a91f6be8..c5dca5cf 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -3,10 +3,12 @@ //! Hit testing. +use peniko::kurbo::Rect; + use super::*; /// Represents a position within a layout. -#[derive(Copy, Clone, Default, Debug)] +#[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Cursor { /// Path to the target cluster. pub path: CursorPath, @@ -49,35 +51,30 @@ impl Cursor { } result.baseline = line_metrics.baseline; result.path.line_index = line_index; - let mut last_edge = line_metrics.offset; + result.path.visual_line_index = line_index; + let mut cur_edge = line_metrics.offset; + let last_run_ix = line.data.item_range.len().saturating_sub(1); for (run_index, run) in line.runs().enumerate() { result.path.run_index = run_index; + let last_cluster_ix = run.cluster_range().len().saturating_sub(1); for (cluster_index, cluster) in run.visual_clusters().enumerate() { let range = cluster.text_range(); - result.text_start = range.start; - result.text_end = range.end; - result.is_rtl = run.is_rtl(); - result.path.cluster_index = run.visual_to_logical(cluster_index).unwrap(); - if x >= last_edge { - let advance = cluster.advance(); - let next_edge = last_edge + advance; - result.offset = next_edge; - result.insert_point = range.end; - if x >= next_edge { - last_edge = next_edge; - continue; - } - result.advance = advance; - if x <= (last_edge + next_edge) * 0.5 { - result.insert_point = range.start; - result.offset = last_edge; - } - } else { - result.is_inside = false; - result.insert_point = range.start; - result.offset = line_metrics.offset; + let advance = cluster.advance(); + if x <= cur_edge + advance * 0.5 { + let index = if run.is_rtl() { range.end } else { range.start }; + return Self::from_byte_index(layout, index); + } else if run.is_rtl() && x < cur_edge + advance { + return Self::from_byte_index(layout, range.start); + } else if cluster_index == last_cluster_ix && run_index == last_run_ix { + let mut cursor = Self::from_byte_index(layout, range.start + 1); + cursor.baseline = line_metrics.baseline; + cursor.path.visual_line_index = line_index; + cursor.offset = line_metrics.offset + line_metrics.advance; + return cursor; + } else if x < cur_edge + advance { + return Self::from_byte_index(layout, range.end); } - return result; + cur_edge += advance; } } break; @@ -87,36 +84,41 @@ impl Cursor { } /// Creates a new cursor for the specified layout and text position. - pub fn from_position( - layout: &Layout, - mut position: usize, - is_leading: bool, - ) -> Self { + pub fn from_byte_index(layout: &Layout, mut index: usize) -> Self { let mut result = Self { is_inside: true, ..Default::default() }; - if position >= layout.data.text_len { + if index >= layout.data.text_len { result.is_inside = false; - position = layout.data.text_len.saturating_sub(1); + result.text_start = layout.data.text_len; + result.text_end = result.text_start; + index = layout.data.text_len; } let last_line = layout.data.lines.len().saturating_sub(1); for (line_index, line) in layout.lines().enumerate() { let line_metrics = line.metrics(); result.baseline = line_metrics.baseline; result.path.line_index = line_index; - if !line.text_range().contains(&position) && line_index != last_line { + result.path.visual_line_index = line_index; + if !line.text_range().contains(&index) && line_index != last_line { continue; } let mut last_edge = line_metrics.offset; result.offset = last_edge; + let mut last_is_rtl = false; + let mut last_run_end = 0.0; for (run_index, run) in line.runs().enumerate() { + let is_rtl = run.is_rtl(); result.path.run_index = run_index; - if !run.text_range().contains(&position) { + if !run.text_range().contains(&index) { last_edge += run.advance(); result.offset = last_edge; + last_is_rtl = is_rtl; + last_run_end = last_edge; continue; } + let last_cluster_ix = run.cluster_range().len().saturating_sub(1); for (cluster_index, cluster) in run.visual_clusters().enumerate() { let range = cluster.text_range(); result.text_start = range.start; @@ -124,13 +126,16 @@ impl Cursor { result.offset = last_edge; result.is_rtl = run.is_rtl(); result.path.cluster_index = run.visual_to_logical(cluster_index).unwrap(); + result.insert_point = range.start; let advance = cluster.advance(); - if range.contains(&position) { - if !is_leading || !result.is_inside { + result.advance = advance; + if range.contains(&index) { + if is_rtl && cluster_index == last_cluster_ix && !last_is_rtl { + result.offset = last_run_end; + } else if is_rtl || !result.is_inside { result.offset += advance; + result.advance = -advance; } - result.insert_point = if is_leading { range.start } else { range.end }; - result.advance = advance; return result; } last_edge += advance; @@ -144,6 +149,10 @@ impl Cursor { result } + pub fn text_range(&self) -> Range { + self.text_start..self.text_end + } + /// Returns `true` if the cursor is on the leading edge of the target /// cluster. pub fn is_leading(&self) -> bool { @@ -196,7 +205,7 @@ fn move_to_line( } /// Index based path to a cluster. -#[derive(Copy, Clone, Default, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] pub struct CursorPath { /// Index of the containing line. pub line_index: usize, @@ -204,6 +213,9 @@ pub struct CursorPath { pub run_index: usize, /// Index of the cluster within the containing run. pub cluster_index: usize, + /// Index of the line containing the visual representation of the + /// cursor. + pub visual_line_index: usize, } impl CursorPath { @@ -221,4 +233,250 @@ impl CursorPath { pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { self.run(layout)?.get(self.cluster_index) } + + pub fn visual_line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + layout.get(self.visual_line_index) + } +} + +/// Returns a point that is falls within the vertical bounds of the given line. +fn line_y(line: &Line) -> f32 { + line.metrics().baseline - line.metrics().ascent * 0.5 +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + pub fn from_byte_index(layout: &Layout, index: usize) -> Self { + Cursor::from_byte_index(layout, index).into() + } + + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + pub fn focus(&self) -> &Cursor { + &self.focus + } + + pub fn is_collapsed(&self) -> bool { + self.anchor.insert_point == self.focus.insert_point + } + + pub fn text_range(&self) -> Range { + if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start..self.focus.text_start + } else { + self.focus.text_start..self.anchor.text_start + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start + } + + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + Self { + anchor: self.anchor, + focus: Cursor::from_point(layout, x, y), + h_pos: None, + } + } + + pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(Cursor::from_byte_index(layout, self.focus.text_end), extend) + } + + pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend( + Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1)), + extend, + ) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some(y) = self + .focus + .path + .visual_line(layout) + .map(|line| line_y(&line)) + { + self.maybe_extend(Cursor::from_point(layout, 0.0, y), extend) + } else { + *self + } + } + + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some(y) = self + .focus + .path + .visual_line(layout) + .map(|line| line_y(&line)) + { + self.maybe_extend(Cursor::from_point(layout, f32::MAX, y), extend) + } else { + *self + } + } + + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + let line_index = self + .focus + .path + .visual_line_index + .saturating_add_signed(line_delta); + let line = layout.get(line_index)?; + let y = line.metrics().baseline - line.metrics().ascent * 0.5; + let h_pos = self.h_pos.unwrap_or(self.focus.offset); + let new_focus = Cursor::from_point(layout, h_pos, y); + let h_pos = Some(h_pos); + Some(if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + } + }) + } + + pub fn visual_caret(&self, layout: &Layout) -> Option { + self.focus.path.visual_line(layout).map(|line| { + let metrics = line.metrics(); + let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + let line_max = line_min + metrics.line_height as f64; + let line_x = self.focus.offset as f64; + peniko::kurbo::Line::new((line_x, line_min), (line_x, line_max)) + }) + } + + pub fn visual_anchor(&self, layout: &Layout) -> Option { + self.anchor.path.visual_line(layout).map(|line| { + let metrics = line.metrics(); + let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + let line_max = line_min + metrics.line_height as f64; + let line_x = self.anchor.offset as f64; + peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) + }) + } + + pub fn visual_regions(&self, layout: &Layout) -> Vec { + let mut rects = vec![]; + self.visual_regions_with(layout, |rect| rects.push(rect)); + rects + } + + pub fn visual_regions_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.path.visual_line_index; + let line_end_ix = end.path.visual_line_index; + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix) else { + continue; + }; + let metrics = line.metrics(); + let line_min = (metrics.baseline - metrics.ascent) as f64; + let line_max = (metrics.baseline + metrics.descent) as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&cluster.text_range().start) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } } diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index ffce5eb2..af902603 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -134,7 +134,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Computes the next line in the paragraph. Returns the advance and size /// (width and height for horizontal layouts) of the line. - pub fn break_next(&mut self, max_advance: f32) -> Option<(f32, f32)> { + pub fn break_next(&mut self, max_advance: f32, alignment: Alignment) -> Option<(f32, f32)> { // Maintain iterator state if self.done { return None; @@ -151,7 +151,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { &mut self.lines, &mut self.state.line, max_advance, - Alignment::Start, + alignment, $break_reason, ) }; @@ -394,7 +394,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Breaks all remaining lines with the specified maximum advance. This /// consumes the line breaker. - pub fn break_remaining(mut self, max_advance: f32) { + pub fn break_remaining(mut self, max_advance: f32, alignment: Alignment) { // println!("\nDEBUG ITEMS"); // for item in &self.layout.items { // match item.kind { @@ -408,7 +408,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { // println!("\nBREAK ALL"); - while self.break_next(max_advance).is_some() {} + while self.break_next(max_advance, alignment).is_some() {} self.finish(); } @@ -490,6 +490,10 @@ impl<'a, B: Brush> BreakLines<'a, B> { needs_reorder = true; } + let run = &self.layout.runs[line_item.index]; + let line_height = line_item.compute_line_height(self.layout); + line.metrics.line_height = line.metrics.line_height.max(line_height); + // Ignore trailing whitespace for metrics computation // (we are iterating backwards so trailing whitespace comes first) if !have_metrics && line_item.is_whitespace { @@ -503,9 +507,6 @@ impl<'a, B: Brush> BreakLines<'a, B> { .sum(); // Compute the run's vertical metrics - let run = &self.layout.runs[line_item.index]; - let line_height = line_item.compute_line_height(self.layout); - line.metrics.line_height = line.metrics.line_height.max(line_height); line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); line.metrics.descent = line.metrics.descent.max(run.metrics.descent); line.metrics.leading = line.metrics.leading.max(run.metrics.leading); diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index d868f995..7f17a664 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -115,9 +115,9 @@ impl Layout { } /// Breaks all lines with the specified maximum advance. - pub fn break_all_lines(&mut self, max_advance: Option) { + pub fn break_all_lines(&mut self, max_advance: Option, alignment: Alignment) { self.break_lines() - .break_remaining(max_advance.unwrap_or(f32::MAX)); + .break_remaining(max_advance.unwrap_or(f32::MAX), alignment); } // Apply to alignment to layout relative to the specified container width. If container_width is not From e8ab0ebd007ed8f915b21dee1f4b23e5e9fa8fd5 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 16 Aug 2024 12:42:15 -0400 Subject: [PATCH 04/18] fix ci --- examples/swash_render/src/main.rs | 1 - examples/tiny_skia_render/src/main.rs | 2 +- examples/vello_editor/src/main.rs | 1 - examples/vello_editor/src/text.rs | 11 +++++------ 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 74959d9d..2bbf2ec1 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -80,7 +80,6 @@ fn main() { let mut layout: Layout = builder.build(); // Perform layout (including bidi resolution and shaping) with start alignment - layout.break_all_lines(max_advance); layout.align(max_advance, Alignment::Start); // Create image to render into diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index 10ee4876..6b21fd22 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -75,7 +75,7 @@ fn main() { let mut layout: Layout = builder.build(); // Perform layout (including bidi resolution and shaping) with start alignment - layout.break_all_lines(max_advance); + layout.break_all_lines(max_advance, Alignment::Start); layout.align(max_advance, Alignment::Start); let width = layout.width().ceil() as u32; let height = layout.height().ceil() as u32; diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index 6d55208e..116e91bb 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -4,7 +4,6 @@ use anyhow::Result; use std::num::NonZeroUsize; use std::sync::Arc; -use vello::kurbo::{Affine, Circle, Ellipse, Line, RoundedRect, Stroke}; use vello::peniko::Color; use vello::util::{RenderContext, RenderSurface}; use vello::wgpu; diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index f5dc858f..1ff02d9d 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -17,6 +17,7 @@ type Layout = parley::Layout; const INSET: f32 = 32.0; +#[allow(dead_code)] #[derive(Copy, Clone, Debug)] pub enum ActiveText<'a> { FocusedCluster(&'a str), @@ -79,8 +80,8 @@ impl Text { .modifiers .map(|mods| mods.state().shift_key()) .unwrap_or_default(); - match event.physical_key { - PhysicalKey::Code(code) => match code { + if let PhysicalKey::Code(code) = event.physical_key { + match code { KeyCode::ArrowLeft => { self.selection = self.selection.prev_logical(&self.layout, shift); } @@ -116,7 +117,7 @@ impl Text { let start = if self.selection.is_collapsed() { let end = self.selection.focus().text_start; if let Some((start, _)) = - self.buffer[..end].char_indices().rev().next() + self.buffer[..end].char_indices().next_back() { self.buffer.replace_range(start..end, ""); self.update_layout(self.width, 1.0); @@ -145,8 +146,7 @@ impl Text { Selection::from_byte_index(&self.layout, start + text.len()); } } - }, - _ => {} + } } println!("Active text: {:?}", self.active_text()); @@ -212,7 +212,6 @@ impl Text { let glyph_xform = synthesis .skew() .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0)); - let style = glyph_run.style(); let coords = run .normalized_coords() .iter() From 1d2402e5d5de3c2bfb36a5b5b1eb6caf67a8cd7a Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 16 Aug 2024 12:43:27 -0400 Subject: [PATCH 05/18] copyright header --- examples/vello_editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index 116e91bb..a1f4bebf 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -1,4 +1,4 @@ -// Copyright 2024 the Vello Authors +// Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT use anyhow::Result; From 0938434c3189c9d906d6474a53aee7781f12dec3 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 16 Aug 2024 12:49:47 -0400 Subject: [PATCH 06/18] no_std --- parley/src/layout/cursor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index c5dca5cf..bc73fd13 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -3,9 +3,9 @@ //! Hit testing. -use peniko::kurbo::Rect; - use super::*; +use alloc::vec::Vec; +use peniko::kurbo::Rect; /// Represents a position within a layout. #[derive(Copy, Clone, PartialEq, Default, Debug)] @@ -421,7 +421,7 @@ impl Selection { } pub fn visual_regions(&self, layout: &Layout) -> Vec { - let mut rects = vec![]; + let mut rects = Vec::new(); self.visual_regions_with(layout, |rect| rects.push(rect)); rects } From ee7f063edbd4d957f83d8c3c6bef76313cee29ba Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Sat, 17 Aug 2024 20:08:02 -0400 Subject: [PATCH 07/18] checkpoint bidi matches chrome impl --- Cargo.lock | 62 +++++ examples/vello_editor/Cargo.toml | 1 + examples/vello_editor/src/main.rs | 6 +- examples/vello_editor/src/text.rs | 41 ++- parley/src/layout/cursor.rs | 420 +++++++++++++++++++++--------- parley/src/layout/line/greedy.rs | 2 + parley/src/layout/line/mod.rs | 10 + parley/src/layout/mod.rs | 46 +++- 8 files changed, 457 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44b3459c..73316a69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1177,6 +1199,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -1380,6 +1413,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -2035,6 +2077,7 @@ name = "vello_editor" version = "0.1.0" dependencies = [ "anyhow", + "clipboard", "parley", "peniko", "pollster", @@ -2731,6 +2774,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -2763,6 +2815,16 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + [[package]] name = "xcursor" version = "0.3.8" diff --git a/examples/vello_editor/Cargo.toml b/examples/vello_editor/Cargo.toml index c58cae96..8c251b51 100644 --- a/examples/vello_editor/Cargo.toml +++ b/examples/vello_editor/Cargo.toml @@ -13,6 +13,7 @@ pollster = "0.3.0" winit = "0.30.3" parley = { workspace = true, default-features = true } peniko = { workspace = true } +clipboard = "0.5.0" [lints] workspace = true diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index a1f4bebf..be5564e3 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -108,9 +108,9 @@ impl<'s> ApplicationHandler for SimpleVelloApp<'s> { self.text.handle_event(&event); render_state.window.request_redraw(); - render_state - .window - .set_cursor(winit::window::Cursor::Icon(winit::window::CursorIcon::Text)); + // render_state + // .window + // .set_cursor(winit::window::Cursor::Icon(winit::window::CursorIcon::Text)); match event { // Exit the event loop when a close is requested (e.g. window's close button is pressed) diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 1ff02d9d..91be01b3 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -1,6 +1,7 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +use clipboard::ClipboardProvider; use parley::{layout::cursor::Selection, layout::PositionedLayoutItem, FontContext}; use peniko::{ kurbo::{Affine, Stroke}, @@ -60,7 +61,7 @@ impl Text { pub fn active_text(&self) -> ActiveText { if self.selection.is_collapsed() { - let range = self.selection.focus().text_start..self.selection.focus().text_end; + let range = self.selection.text_range(); ActiveText::FocusedCluster(&self.buffer[range]) } else { ActiveText::Selection(&self.buffer[self.selection.text_range()]) @@ -69,6 +70,10 @@ impl Text { pub fn handle_event(&mut self, event: &WindowEvent) { match event { + WindowEvent::Resized(size) => { + self.update_layout(size.width as f32, 1.0); + self.selection = self.selection.refresh(&self.layout); + } WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = Some(*modifiers); } @@ -76,12 +81,30 @@ impl Text { if !event.state.is_pressed() { return; } - let shift = self + let (shift, ctrl) = self .modifiers - .map(|mods| mods.state().shift_key()) + .map(|mods| (mods.state().shift_key(), mods.state().control_key())) .unwrap_or_default(); if let PhysicalKey::Code(code) = event.physical_key { match code { + KeyCode::KeyC if ctrl => { + let text = &self.buffer[self.selection.text_range()]; + let mut cb: clipboard::ClipboardContext = + ClipboardProvider::new().unwrap(); + cb.set_contents(text.to_owned()).ok(); + } + KeyCode::KeyV if ctrl => { + let mut cb: clipboard::ClipboardContext = + ClipboardProvider::new().unwrap(); + let text = cb.get_contents().unwrap_or_default(); + let start = self + .delete_current_selection() + .unwrap_or_else(|| self.selection.focus().text_start as usize); + self.buffer.insert_str(start, &text); + self.update_layout(self.width, 1.0); + self.selection = + Selection::from_byte_index(&self.layout, start + text.len()); + } KeyCode::ArrowLeft => { self.selection = self.selection.prev_logical(&self.layout, shift); } @@ -102,8 +125,7 @@ impl Text { } KeyCode::Delete => { let start = if self.selection.is_collapsed() { - let range = self.selection.focus().text_start - ..self.selection.focus().text_end; + let range = self.selection.focus().text_range(); let start = range.start; self.buffer.replace_range(range, ""); start @@ -115,7 +137,7 @@ impl Text { } KeyCode::Backspace => { let start = if self.selection.is_collapsed() { - let end = self.selection.focus().text_start; + let end = self.selection.focus().text_start as usize; if let Some((start, _)) = self.buffer[..end].char_indices().next_back() { @@ -139,7 +161,7 @@ impl Text { if let Some(text) = &event.text { let start = self .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_start); + .unwrap_or_else(|| self.selection.focus().text_start as usize); self.buffer.insert_str(start, text); self.update_layout(self.width, 1.0); self.selection = @@ -195,9 +217,12 @@ impl Text { self.selection.visual_regions_with(&self.layout, |rect| { scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); }); - if let Some(cursor) = self.selection.visual_caret(&self.layout) { + if let Some(cursor) = self.selection.visual_focus(&self.layout) { scene.stroke(&Stroke::new(1.5), transform, Color::WHITE, None, &cursor); }; + if let Some(alt_cursor) = self.selection.visual_alternate_focus(&self.layout) { + scene.stroke(&Stroke::new(1.5), transform, Color::RED, None, &alt_cursor); + }; for line in self.layout.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index bc73fd13..fe6052da 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -19,15 +19,15 @@ pub struct Cursor { /// Advance of the target cluster. pub advance: f32, /// Start of the target cluster. - pub text_start: usize, + pub text_start: u32, /// End of the target cluster. - pub text_end: usize, - /// Insert point of the cursor (leading or trailing). - pub insert_point: usize, + pub text_end: u32, /// `true` if the target cluster is in a right-to-left run. pub is_rtl: bool, - /// `true` if the cursor was created from a point or position inside the layout + /// `true` if the cursor was created from a point or position inside the layout. pub is_inside: bool, + /// Possible visual positions of the cursor. + pub placement: CursorPlacement, } impl Cursor { @@ -37,25 +37,30 @@ impl Cursor { is_inside: x >= 0. && y >= 0., ..Default::default() }; - let last_line = layout.data.lines.len().saturating_sub(1); - for (line_index, line) in layout.lines().enumerate() { + let last_line_index = layout.data.lines.len().saturating_sub(1); + if let Some((line_index, line)) = layout + .line_for_offset(y) + .or_else(|| Some((last_line_index, layout.get(last_line_index)?))) + { + let line_index = line_index as u32; let line_metrics = line.metrics(); - if y > line_metrics.baseline + line_metrics.descent + line_metrics.leading * 0.5 { - if line_index != last_line { - continue; - } + if y > line_metrics.max_coord { result.is_inside = false; x = f32::MAX; - } else if y < 0. { - x = 0.; + } else if y < 0.0 { + x = 0.0; } result.baseline = line_metrics.baseline; result.path.line_index = line_index; result.path.visual_line_index = line_index; + result.placement = CursorPlacement::Single(CursorPosition { + line_index: line_index as u32, + offset: 0.0, + }); let mut cur_edge = line_metrics.offset; let last_run_ix = line.data.item_range.len().saturating_sub(1); for (run_index, run) in line.runs().enumerate() { - result.path.run_index = run_index; + result.path.run_index = run_index as u32; let last_cluster_ix = run.cluster_range().len().saturating_sub(1); for (cluster_index, cluster) in run.visual_clusters().enumerate() { let range = cluster.text_range(); @@ -67,9 +72,21 @@ impl Cursor { return Self::from_byte_index(layout, range.start); } else if cluster_index == last_cluster_ix && run_index == last_run_ix { let mut cursor = Self::from_byte_index(layout, range.start + 1); + let offset = cursor.placement.primary_position().offset; cursor.baseline = line_metrics.baseline; cursor.path.visual_line_index = line_index; cursor.offset = line_metrics.offset + line_metrics.advance; + cursor.placement = CursorPlacement::LineBoundary { + prefer_end: true, + end: CursorPosition { + line_index, + offset: line_metrics.offset + line_metrics.advance, + }, + start: CursorPosition { + line_index: cursor.path.line_index, + offset, + }, + }; return cursor; } else if x < cur_edge + advance { return Self::from_byte_index(layout, range.end); @@ -77,7 +94,6 @@ impl Cursor { cur_edge += advance; } } - break; } result.is_inside = false; result @@ -91,151 +107,248 @@ impl Cursor { }; if index >= layout.data.text_len { result.is_inside = false; - result.text_start = layout.data.text_len; + result.text_start = layout.data.text_len as u32; result.text_end = result.text_start; index = layout.data.text_len; } - let last_line = layout.data.lines.len().saturating_sub(1); - for (line_index, line) in layout.lines().enumerate() { + let last_line_index = layout.data.lines.len().saturating_sub(1); + if let Some((line_index, line)) = layout + .line_for_byte_index(index) + .or_else(|| Some((last_line_index, layout.get(last_line_index)?))) + { + let line_index = line_index as u32; let line_metrics = line.metrics(); result.baseline = line_metrics.baseline; result.path.line_index = line_index; result.path.visual_line_index = line_index; - if !line.text_range().contains(&index) && line_index != last_line { - continue; - } let mut last_edge = line_metrics.offset; result.offset = last_edge; - let mut last_is_rtl = false; + let mut last_is_rtl = None; let mut last_run_end = 0.0; + let mut last_run_start = 0.0; for (run_index, run) in line.runs().enumerate() { let is_rtl = run.is_rtl(); - result.path.run_index = run_index; + result.path.run_index = run_index as u32; if !run.text_range().contains(&index) { + last_run_start = last_edge; last_edge += run.advance(); result.offset = last_edge; - last_is_rtl = is_rtl; + last_is_rtl = Some(is_rtl); last_run_end = last_edge; continue; } let last_cluster_ix = run.cluster_range().len().saturating_sub(1); for (cluster_index, cluster) in run.visual_clusters().enumerate() { let range = cluster.text_range(); - result.text_start = range.start; - result.text_end = range.end; + result.text_start = range.start as u32; + result.text_end = range.end as u32; result.offset = last_edge; + result.placement = CursorPlacement::Single(CursorPosition { + line_index: line_index as u32, + offset: last_edge, + }); result.is_rtl = run.is_rtl(); - result.path.cluster_index = run.visual_to_logical(cluster_index).unwrap(); - result.insert_point = range.start; + result.path.cluster_index = + run.visual_to_logical(cluster_index).unwrap() as u32; let advance = cluster.advance(); result.advance = advance; if range.contains(&index) { - if is_rtl && cluster_index == last_cluster_ix && !last_is_rtl { - result.offset = last_run_end; + if cluster_index == last_cluster_ix + && last_is_rtl.is_some() + && last_is_rtl != Some(is_rtl) + { + if is_rtl { + result.placement = CursorPlacement::DirectionalBoundary { + primary: CursorPosition { + line_index, + offset: last_run_end, + }, + secondary: CursorPosition { + line_index, + offset: last_edge + advance, + }, + }; + } else { + result.placement = CursorPlacement::DirectionalBoundary { + primary: CursorPosition { + line_index, + offset: last_edge, + }, + secondary: CursorPosition { + line_index, + offset: last_run_start, + }, + }; + } } else if is_rtl || !result.is_inside { - result.offset += advance; - result.advance = -advance; + result.placement = CursorPlacement::Single(CursorPosition { + line_index, + offset: last_edge + advance, + }); } + // if is_rtl && cluster_index == last_cluster_ix && !last_is_rtl { + // result.offset = last_run_end; + // } else if is_rtl || !result.is_inside { + // result.offset += advance; + // result.advance = -advance; + // } return result; } last_edge += advance; } } - result.offset = last_edge; - break; + result.placement = CursorPlacement::Single(CursorPosition { + line_index, + offset: last_edge, + }); } - result.insert_point = result.text_end; result.is_inside = false; result } pub fn text_range(&self) -> Range { - self.text_start..self.text_end - } - - /// Returns `true` if the cursor is on the leading edge of the target - /// cluster. - pub fn is_leading(&self) -> bool { - self.text_start == self.insert_point - } - - /// Returns `true` if the cursor is on the trailing edge of the target - /// cluster. - pub fn is_trailing(&self) -> bool { - self.text_end == self.insert_point + self.text_start as usize..self.text_end as usize } - /// Given the layout that generated this cursor, return a new cursor - /// for the corresponding position on the next line. - /// - /// If `h_pos` is provided, then it will be used as the horizontal offset - /// for computing the position on the next line. - /// - /// Returns `None` if the cursor should remain in its current position. - pub fn next_line(&self, layout: &Layout, h_pos: Option) -> Option { - move_to_line(layout, self, h_pos, self.path.line_index.checked_add(1)?) - } - - /// Given the layout that generated this cursor, return a new cursor - /// for the corresponding position on the previous line. - /// - /// If `h_pos` is provided, then it will be used as the horizontal offset - /// for computing the position on the previous line. - /// - /// Returns `None` if the cursor should remain in its current position. - pub fn prev_line(&self, layout: &Layout, h_pos: Option) -> Option { - move_to_line(layout, self, h_pos, self.path.line_index.checked_sub(1)?) + fn at_end_of_line(&self) -> bool { + match self.placement { + CursorPlacement::LineBoundary { prefer_end, .. } => prefer_end, + _ => false, + } } } -fn move_to_line( - layout: &Layout, - cursor: &Cursor, - h_pos: Option, - line_index: usize, -) -> Option { - let line = layout.get(line_index)?; - let metrics = line.metrics(); - let y = metrics.baseline - metrics.line_height * 0.5; - Some(Cursor::from_point( - layout, - h_pos.unwrap_or(cursor.offset), - y, - )) -} - /// Index based path to a cluster. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] pub struct CursorPath { /// Index of the containing line. - pub line_index: usize, + pub line_index: u32, /// Index of the run within the containing line. - pub run_index: usize, + pub run_index: u32, /// Index of the cluster within the containing run. - pub cluster_index: usize, + pub cluster_index: u32, /// Index of the line containing the visual representation of the /// cursor. - pub visual_line_index: usize, + pub visual_line_index: u32, } impl CursorPath { /// Returns the line for this path and the specified layout. pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.line_index) + layout.get(self.line_index as usize) } /// Returns the run for this path and the specified layout. pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.line(layout)?.run(self.run_index) + self.line(layout)?.run(self.run_index as usize) } /// Returns the cluster for this path and the specified layout. pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.run(layout)?.get(self.cluster_index) + self.run(layout)?.get(self.cluster_index as usize) } pub fn visual_line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.visual_line_index) + layout.get(self.visual_line_index as usize) + } +} + +/// Describes the possible visual positions where a [`Cursor`] may be rendered. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum CursorPlacement { + /// A cursor with a single visual position. + Single(CursorPosition), + /// A cursor that sits on a boundary where the text direction changes. + DirectionalBoundary { + /// Indicates the visual position where left-to-right text would be + /// inserted. + /// + /// This is considered the primary position because it is the one + /// used by all text editors. + primary: CursorPosition, + /// Indicates the visual position where right-to-left text would be + /// inserted. + secondary: CursorPosition, + }, + /// A cursor that sits on a line boundary. + LineBoundary { + /// True when the `end` position is preferred. This is set when the + /// cursor is generated as the result of a hit test that explicitly + /// targeted the end of the line. + prefer_end: bool, + /// The visual position at the end of previous line. + end: CursorPosition, + /// The visual position at the start of the current line. This always + /// represents the logical position of the cursor. + start: CursorPosition, + }, +} + +impl CursorPlacement { + /// Returns the primary position for the cursor. + /// + /// This is the generally preferred position that matches what the majority + /// of text editors would display. + pub fn primary_position(&self) -> CursorPosition { + match *self { + Self::Single(pos) => pos, + Self::DirectionalBoundary { primary, .. } => primary, + Self::LineBoundary { + prefer_end, + end, + start, + } => { + if prefer_end { + end + } else { + start + } + } + } + } + + /// Returns the alternate position fo the cursor, if one is available. + /// + /// Some text editors will use this to display the two potential insertion + /// points for mixed-direction text. + pub fn alternate_position(&self) -> Option { + match *self { + Self::Single(_) => None, + Self::DirectionalBoundary { secondary, .. } => Some(secondary), + Self::LineBoundary { + prefer_end, + end, + start, + } => { + if prefer_end { + Some(start) + } else { + Some(end) + } + } + } + } +} + +impl Default for CursorPlacement { + fn default() -> Self { + Self::Single(CursorPosition::default()) + } +} + +/// Visual position of a [`Cursor`]. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct CursorPosition { + /// Index of the line containing the cursor. + pub line_index: u32, + /// Offset of the cursor along the direction of the line. + pub offset: f32, +} + +impl CursorPosition { + /// Returns the line for this position and the specified layout. + pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + layout.get(self.line_index as usize) } } @@ -244,10 +357,26 @@ fn line_y(line: &Line) -> f32 { line.metrics().baseline - line.metrics().ascent * 0.5 } +fn visual_for_cursor( + layout: &Layout, + cursor_pos: Option, +) -> Option { + let pos = cursor_pos?; + pos.line(layout).map(|line| { + let metrics = line.metrics(); + let line_x = pos.offset as f64; + peniko::kurbo::Line::new( + (line_x, metrics.min_coord as f64), + (line_x, metrics.max_coord as f64), + ) + }) +} + #[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Selection { anchor: Cursor, focus: Cursor, + focus_at_end: bool, h_pos: Option, } @@ -256,6 +385,7 @@ impl From for Selection { Self { anchor: value, focus: value, + focus_at_end: value.at_end_of_line(), h_pos: None, } } @@ -279,46 +409,87 @@ impl Selection { } pub fn is_collapsed(&self) -> bool { - self.anchor.insert_point == self.focus.insert_point + self.anchor.text_start == self.focus.text_start } pub fn text_range(&self) -> Range { if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start..self.focus.text_start + self.anchor.text_start as usize..self.focus.text_start as usize } else { - self.focus.text_start..self.anchor.text_start + self.focus.text_start as usize..self.anchor.text_start as usize } } /// Returns the index where text should be inserted based on this /// selection. pub fn insertion_index(&self) -> usize { - self.focus.text_start + self.focus.text_start as usize } + #[must_use] pub fn collapse(&self) -> Self { Self { anchor: self.focus, focus: self.focus, + focus_at_end: self.focus_at_end, h_pos: self.h_pos, } } + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = Cursor::from_byte_index(layout, self.anchor.text_start as usize); + let focus = Cursor::from_byte_index(layout, self.focus.text_start as usize); + let focus = + if self.focus_at_end && focus.path.run_index == 0 && focus.path.cluster_index == 0 { + // Hack! + // On resize, keep track of cursor positions that were set at end + // of the line. + if let Some(prev_line) = focus + .path + .line_index + .checked_sub(1) + .and_then(|line_ix| layout.get(line_ix as usize)) + { + let y = prev_line.metrics().baseline; + Cursor::from_point(layout, f32::MAX, y) + } else { + focus + } + } else { + focus + }; + Self { + anchor, + focus, + focus_at_end: self.focus_at_end, + h_pos: None, + } + } + + #[must_use] pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, x, y); Self { anchor: self.anchor, - focus: Cursor::from_point(layout, x, y), + focus, + focus_at_end: focus.at_end_of_line(), h_pos: None, } } + #[must_use] pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(Cursor::from_byte_index(layout, self.focus.text_end), extend) + self.maybe_extend( + Cursor::from_byte_index(layout, self.focus.text_end as usize), + extend, + ) } + #[must_use] pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { self.maybe_extend( - Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1)), + Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), extend, ) } @@ -328,6 +499,7 @@ impl Selection { Self { anchor: self.anchor, focus, + focus_at_end: focus.at_end_of_line(), h_pos: None, } } else { @@ -335,11 +507,13 @@ impl Selection { } } + #[must_use] pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { if let Some(y) = self .focus - .path - .visual_line(layout) + .placement + .primary_position() + .line(layout) .map(|line| line_y(&line)) { self.maybe_extend(Cursor::from_point(layout, 0.0, y), extend) @@ -348,11 +522,13 @@ impl Selection { } } + #[must_use] pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { if let Some(y) = self .focus - .path - .visual_line(layout) + .placement + .primary_position() + .line(layout) .map(|line| line_y(&line)) { self.maybe_extend(Cursor::from_point(layout, f32::MAX, y), extend) @@ -361,10 +537,12 @@ impl Selection { } } + #[must_use] pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { self.move_line(layout, 1, extend).unwrap_or(*self) } + #[must_use] pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { self.move_line(layout, -1, extend).unwrap_or(*self) } @@ -372,15 +550,16 @@ impl Selection { fn move_line( &self, layout: &Layout, - line_delta: isize, + line_delta: i32, extend: bool, ) -> Option { let line_index = self .focus - .path - .visual_line_index + .placement + .primary_position() + .line_index .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; + let line = layout.get(line_index as usize)?; let y = line.metrics().baseline - line.metrics().ascent * 0.5; let h_pos = self.h_pos.unwrap_or(self.focus.offset); let new_focus = Cursor::from_point(layout, h_pos, y); @@ -389,25 +568,28 @@ impl Selection { Self { anchor: self.anchor, focus: new_focus, + focus_at_end: new_focus.at_end_of_line(), h_pos, } } else { Self { anchor: new_focus, focus: new_focus, + focus_at_end: new_focus.at_end_of_line(), h_pos, } }) } - pub fn visual_caret(&self, layout: &Layout) -> Option { - self.focus.path.visual_line(layout).map(|line| { - let metrics = line.metrics(); - let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - let line_max = line_min + metrics.line_height as f64; - let line_x = self.focus.offset as f64; - peniko::kurbo::Line::new((line_x, line_min), (line_x, line_max)) - }) + pub fn visual_focus(&self, layout: &Layout) -> Option { + visual_for_cursor(layout, Some(self.focus.placement.primary_position())) + } + + pub fn visual_alternate_focus( + &self, + layout: &Layout, + ) -> Option { + visual_for_cursor(layout, self.focus.placement.alternate_position()) } pub fn visual_anchor(&self, layout: &Layout) -> Option { @@ -442,12 +624,12 @@ impl Selection { let line_start_ix = start.path.visual_line_index; let line_end_ix = end.path.visual_line_index; for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix) else { + let Some(line) = layout.get(line_ix as usize) else { continue; }; let metrics = line.metrics(); - let line_min = (metrics.baseline - metrics.ascent) as f64; - let line_max = (metrics.baseline + metrics.descent) as f64; + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; if line_ix == line_start_ix || line_ix == line_end_ix { // We only need to run the expensive logic on the first and // last lines @@ -456,7 +638,7 @@ impl Selection { for run in line.runs() { for cluster in run.visual_clusters() { let advance = cluster.advance() as f64; - if text_range.contains(&cluster.text_range().start) { + if text_range.contains(&(cluster.text_range().start as u32)) { cur_x += advance; } else { if cur_x != start_x { diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index af902603..3bd72a62 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -566,8 +566,10 @@ impl<'a, B: Brush> BreakLines<'a, B> { // Compute let above = (line.metrics.ascent + line.metrics.leading * 0.5).round(); let below = (line.metrics.descent + line.metrics.leading * 0.5).round(); + line.metrics.min_coord = y; line.metrics.baseline = y + above; y = line.metrics.baseline + below; + line.metrics.max_coord = y; } } } diff --git a/parley/src/layout/line/mod.rs b/parley/src/layout/line/mod.rs index 9f1e3349..f62c085c 100644 --- a/parley/src/layout/line/mod.rs +++ b/parley/src/layout/line/mod.rs @@ -101,6 +101,16 @@ pub struct LineMetrics { pub advance: f32, /// Advance of trailing whitespace. pub trailing_whitespace: f32, + /// Minimum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the top of the line. + pub min_coord: f32, + /// Maximum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the bottom of the line. + pub max_coord: f32, } impl LineMetrics { diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 7f17a664..3aaf3875 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -16,7 +16,7 @@ use self::alignment::align; use super::style::Brush; use crate::{Font, InlineBox}; -use core::ops::Range; +use core::{cmp::Ordering, ops::Range}; use data::*; use swash::text::cluster::{Boundary, ClusterInfo}; use swash::{GlyphId, NormalizedCoord, Synthesis}; @@ -134,6 +134,50 @@ impl Layout { line_data: None, }) } + + /// Returns the index and `Line` object for the line containing the + /// given byte `index` in the source text. + pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line)> { + let line_index = self + .data + .lines + .binary_search_by(|line| { + if index < line.text_range.start { + Ordering::Greater + } else if index >= line.text_range.end { + Ordering::Less + } else { + Ordering::Equal + } + }) + .ok()?; + Some((line_index, self.get(line_index)?)) + } + + /// Returns the index and `Line` object for the line containing the + /// given `offset`. + /// + /// The offset is specified in the direction orthogonal to line direction. + /// For horizontal text, this is a vertical or y offset. + pub(crate) fn line_for_offset(&self, offset: f32) -> Option<(usize, Line)> { + if offset < 0.0 { + return Some((0, self.get(0)?)); + } + let maybe_line_index = self.data.lines.binary_search_by(|line| { + if offset < line.metrics.min_coord { + Ordering::Greater + } else if offset > line.metrics.max_coord { + Ordering::Less + } else { + Ordering::Equal + } + }); + let line_index = match maybe_line_index { + Ok(index) => index, + Err(index) => index.saturating_sub(1), + }; + Some((line_index, self.get(line_index)?)) + } } impl Default for Layout { From 596a2afd626caaf956c8eda959d667b2b65e41a9 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 20 Aug 2024 22:43:05 -0400 Subject: [PATCH 08/18] checkpoint visual movement --- .vscode/launch.json | 8 +- examples/vello_editor/src/main.rs | 15 +- examples/vello_editor/src/text.rs | 134 +++-- parley/src/layout/cluster.rs | 482 +++++++++++++++++ parley/src/layout/cursor.rs | 869 ++++++++++++++++-------------- parley/src/layout/cursor2.rs | 527 ++++++++++++++++++ parley/src/layout/cursor3.rs | 641 ++++++++++++++++++++++ parley/src/layout/mod.rs | 6 + parley/src/layout/run.rs | 16 + parley/src/layout/select.rs | 700 ++++++++++++++++++++++++ parley/src/layout/select2.rs | 491 +++++++++++++++++ parley/src/layout/selection.rs | 413 ++++++++++++++ 12 files changed, 3833 insertions(+), 469 deletions(-) create mode 100644 parley/src/layout/cursor2.rs create mode 100644 parley/src/layout/cursor3.rs create mode 100644 parley/src/layout/select.rs create mode 100644 parley/src/layout/select2.rs create mode 100644 parley/src/layout/selection.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 0a2ffce1..7ff31f9f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -119,15 +119,15 @@ { "type": "lldb", "request": "launch", - "name": "Debug executable 'vello_render'", + "name": "Debug executable 'vello_editor'", "cargo": { "args": [ "build", - "--bin=vello_render", - "--package=vello_render" + "--bin=vello_editor", + "--package=vello_editor" ], "filter": { - "name": "vello_render", + "name": "vello_editor", "kind": "bin" } }, diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index be5564e3..4fc2e071 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -14,6 +14,7 @@ use winit::event::*; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::Window; +// #[path = "text2.rs"] mod text; // Simple struct to hold the state of the renderer @@ -46,7 +47,7 @@ struct SimpleVelloApp<'s> { scene: Scene, // Our text state object - text: text::Text, + editor: text::Editor, } impl<'s> ApplicationHandler for SimpleVelloApp<'s> { @@ -69,7 +70,7 @@ impl<'s> ApplicationHandler for SimpleVelloApp<'s> { wgpu::PresentMode::AutoVsync, ); let surface = pollster::block_on(surface_future).expect("Error creating surface"); - self.text.update_layout(size.width as _, 1.0); + self.editor.update_layout(size.width as _, 1.0); // Create a vello Renderer for the surface (using its device id) self.renderers @@ -106,7 +107,7 @@ impl<'s> ApplicationHandler for SimpleVelloApp<'s> { _ => return, }; - self.text.handle_event(&event); + self.editor.handle_event(&event); render_state.window.request_redraw(); // render_state // .window @@ -121,7 +122,7 @@ impl<'s> ApplicationHandler for SimpleVelloApp<'s> { self.context .resize_surface(&mut render_state.surface, size.width, size.height); render_state.window.request_redraw(); - self.text.update_layout(size.width as _, 1.0); + self.editor.update_layout(size.width as _, 1.0); } // This is where all the rendering happens @@ -130,7 +131,7 @@ impl<'s> ApplicationHandler for SimpleVelloApp<'s> { // the same Scene is reused so that the underlying memory allocation can also be reused. self.scene.reset(); - self.text.draw(&mut self.scene); + self.editor.draw(&mut self.scene); // Re-add the objects to draw to the scene. // add_shapes_to_scene(&mut self.scene); @@ -185,10 +186,10 @@ fn main() -> Result<()> { renderers: vec![], state: RenderState::Suspended(None), scene: Scene::new(), - text: text::Text::default(), + editor: text::Editor::default(), }; - app.text.set_text(text::LOREM); + app.editor.set_text(text::LOREM); // Create and run a winit event loop let event_loop = EventLoop::new()?; diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 91be01b3..8ccc1eed 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use clipboard::ClipboardProvider; -use parley::{layout::cursor::Selection, layout::PositionedLayoutItem, FontContext}; -use peniko::{ - kurbo::{Affine, Stroke}, - Color, Fill, -}; +use parley::layout::cursor::{Selection, VisualCursorMode}; +use parley::layout::Affinity; +use parley::{layout::PositionedLayoutItem, FontContext}; +use peniko::{kurbo::Affine, Color, Fill}; +use std::time::Instant; use vello::Scene; use winit::{ event::{Modifiers, WindowEvent}, @@ -21,24 +21,27 @@ const INSET: f32 = 32.0; #[allow(dead_code)] #[derive(Copy, Clone, Debug)] pub enum ActiveText<'a> { - FocusedCluster(&'a str), + FocusedCluster(Affinity, &'a str), Selection(&'a str), } #[derive(Default)] -pub struct Text { +pub struct Editor { font_cx: FontContext, layout_cx: LayoutContext, buffer: String, layout: Layout, selection: Selection, + cursor_mode: VisualCursorMode, + last_click_time: Option, + click_count: u32, pointer_down: bool, cursor_pos: (f32, f32), modifiers: Option, width: f32, } -impl Text { +impl Editor { pub fn set_text(&mut self, text: &str) { self.buffer.clear(); self.buffer.push_str(text); @@ -61,8 +64,14 @@ impl Text { pub fn active_text(&self) -> ActiveText { if self.selection.is_collapsed() { - let range = self.selection.text_range(); - ActiveText::FocusedCluster(&self.buffer[range]) + let range = self + .selection + .focus() + .cluster_path() + .cluster(&self.layout) + .unwrap() + .text_range(); + ActiveText::FocusedCluster(self.selection.focus().affinity(), &self.buffer[range]) } else { ActiveText::Selection(&self.buffer[self.selection.text_range()]) } @@ -99,20 +108,36 @@ impl Text { let text = cb.get_contents().unwrap_or_default(); let start = self .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_start as usize); + .unwrap_or_else(|| self.selection.focus().text_range().start); self.buffer.insert_str(start, &text); self.update_layout(self.width, 1.0); - self.selection = - Selection::from_byte_index(&self.layout, start + text.len()); + self.selection = Selection::from_index( + &self.layout, + start + text.len(), + Affinity::default(), + ); } KeyCode::ArrowLeft => { - self.selection = self.selection.prev_logical(&self.layout, shift); + self.selection = if ctrl { + self.selection.previous_word(&self.layout, shift) + } else { + self.selection.previous_visual( + &self.layout, + self.cursor_mode, + shift, + ) + }; } KeyCode::ArrowRight => { - self.selection = self.selection.next_logical(&self.layout, shift); + self.selection = if ctrl { + self.selection.next_word(&self.layout, shift) + } else { + self.selection + .next_visual(&self.layout, self.cursor_mode, shift) + }; } KeyCode::ArrowUp => { - self.selection = self.selection.prev_line(&self.layout, shift); + self.selection = self.selection.previous_line(&self.layout, shift); } KeyCode::ArrowDown => { self.selection = self.selection.next_line(&self.layout, shift); @@ -124,27 +149,23 @@ impl Text { self.selection = self.selection.line_end(&self.layout, shift); } KeyCode::Delete => { - let start = if self.selection.is_collapsed() { + if self.selection.is_collapsed() { let range = self.selection.focus().text_range(); - let start = range.start; self.buffer.replace_range(range, ""); - start } else { - self.delete_current_selection().unwrap() + self.delete_current_selection(); }; self.update_layout(self.width, 1.0); - self.selection = Selection::from_byte_index(&self.layout, start); + self.selection = self.selection.refresh(&self.layout); + // Selection::from_byte_index(&self.layout, start, Default::default()); } KeyCode::Backspace => { let start = if self.selection.is_collapsed() { - let end = self.selection.focus().text_start as usize; + let end = self.selection.focus().text_range().start as usize; if let Some((start, _)) = self.buffer[..end].char_indices().next_back() { self.buffer.replace_range(start..end, ""); - self.update_layout(self.width, 1.0); - self.selection = - Selection::from_byte_index(&self.layout, start); Some(start) } else { None @@ -154,18 +175,27 @@ impl Text { }; if let Some(start) = start { self.update_layout(self.width, 1.0); - self.selection = Selection::from_byte_index(&self.layout, start); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = + Selection::from_index(&self.layout, start, affinity); } } _ => { if let Some(text) = &event.text { let start = self .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_start as usize); + .unwrap_or_else(|| self.selection.focus().text_range().start); self.buffer.insert_str(start, text); self.update_layout(self.width, 1.0); - self.selection = - Selection::from_byte_index(&self.layout, start + text.len()); + self.selection = Selection::from_index( + &self.layout, + start + text.len() - 1, + Affinity::Upstream, + ); } } } @@ -177,11 +207,33 @@ impl Text { if *button == winit::event::MouseButton::Left { self.pointer_down = state.is_pressed(); if self.pointer_down { - self.selection = Selection::from_point( - &self.layout, - self.cursor_pos.0, - self.cursor_pos.1, - ); + let now = Instant::now(); + if let Some(last) = self.last_click_time.take() { + if now.duration_since(last).as_secs_f64() < 0.25 { + self.click_count = (self.click_count + 1) % 3; + } else { + self.click_count = 1; + } + } + self.last_click_time = Some(now); + match self.click_count { + 2 => { + println!("SELECTING WORD"); + self.selection = Selection::word_from_point( + &self.layout, + self.cursor_pos.0, + self.cursor_pos.1, + ); + } + // TODO: handle line + _ => { + self.selection = Selection::from_point( + &self.layout, + self.cursor_pos.0, + self.cursor_pos.1, + ); + } + } println!("Active text: {:?}", self.active_text()); } } @@ -214,14 +266,14 @@ impl Text { pub fn draw(&self, scene: &mut Scene) { let transform = Affine::translate((INSET as f64, INSET as f64)); - self.selection.visual_regions_with(&self.layout, |rect| { + self.selection.geometry_with(&self.layout, |rect| { scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); }); - if let Some(cursor) = self.selection.visual_focus(&self.layout) { - scene.stroke(&Stroke::new(1.5), transform, Color::WHITE, None, &cursor); + if let Some(cursor) = self.selection.focus().strong_geometry(&self.layout, 1.5) { + scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor); }; - if let Some(alt_cursor) = self.selection.visual_alternate_focus(&self.layout) { - scene.stroke(&Stroke::new(1.5), transform, Color::RED, None, &alt_cursor); + if let Some(cursor) = self.selection.focus().weak_geometry(&self.layout, 1.5) { + scene.fill(Fill::NonZero, transform, Color::YELLOW, None, &cursor); }; for line in self.layout.lines() { for item in line.items() { @@ -268,6 +320,6 @@ impl Text { } } -pub const LOREM: &str = r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl. +pub const LOREM: &str = r" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl. -Phasellus in viverra dolor, vitae facilisis est. Maecenas malesuada massa vel ultricies feugiat. Vivamus venenatis et התעשייה בנושא האינטרנט nibh nec pharetra. Phasellus vestibulum elit enim, nec scelerisque orci faucibus id. Vivamus consequat purus sit amet orci egestas, non iaculis massa porttitor. Vestibulum ut eros leo. In fermentum convallis magna in finibus. Donec justo leo, maximus ac laoreet id, volutpat ut elit. Mauris sed leo non neque laoreet faucibus. Aliquam orci arcu, faucibus in molestie eget, ornare non dui. Donec volutpat nulla in fringilla elementum. Aliquam vitae ante egestas ligula tempus vestibulum sit amet sed ante. "; +Phasellus in viverra dolor, vitae facilisis est. Maecenas malesuada massa vel ultricies feugiat. Vivamus venenatis et gהתעשייה בנושא האינטרנטa nibh nec pharetra. Phasellus vestibulum elit enim, nec scelerisque orci faucibus id. Vivamus consequat purus sit amet orci egestas, non iaculis massa porttitor. Vestibulum ut eros leo. In fermentum convallis magna in finibus. Donec justo leo, maximus ac laoreet id, volutpat ut elit. Mauris sed leo non neque laoreet faucibus. Aliquam orci arcu, faucibus in molestie eget, ornare non dui. Donec volutpat nulla in fringilla elementum. Aliquam vitae ante egestas ligula tempus vestibulum sit amet sed ante. "; diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 98650540..4b59ccab 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -14,6 +14,11 @@ impl<'a, B: Brush> Cluster<'a, B> { self.data.advance } + /// Returns true if this is a right-to-left cluster. + pub fn is_rtl(&self) -> bool { + self.run.is_rtl() + } + /// Returns `true` if the cluster is the beginning of a ligature. pub fn is_ligature_start(&self) -> bool { self.data.is_ligature_start() @@ -67,6 +72,483 @@ impl<'a, B: Brush> Cluster<'a, B> { } } +/// Determines how a cursor attaches to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub enum Affinity { + /// Left side for LTR clusters and right side for RTL clusters. + #[default] + Downstream = 0, + /// Right side for LTR clusters and left side for RTL clusters. + Upstream = 1, +} + +impl Affinity { + pub(crate) fn new(is_rtl: bool, is_leading: bool) -> Self { + match (is_rtl, is_leading) { + // trailing edge of RTL and leading edge of LTR + (true, false) | (false, true) => Affinity::Downstream, + // leading edge of RTL and trailing edge of LTR + (true, true) | (false, false) => Affinity::Upstream, + } + } + + pub fn invert(&self) -> Self { + match self { + Self::Downstream => Self::Upstream, + Self::Upstream => Self::Downstream, + } + } + + /// Returns true if the cursor should be placed on the leading edge. + pub fn is_visually_leading(&self, is_rtl: bool) -> bool { + match (*self, is_rtl) { + (Self::Upstream, true) | (Self::Downstream, false) => true, + (Self::Upstream, false) | (Self::Downstream, true) => false, + } + } + + /// Returns true if the cursor should be placed on the trailing edge. + pub fn is_visually_trailing(&self, is_rtl: bool) -> bool { + !self.is_visually_leading(is_rtl) + } +} + +/// Index based path to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct ClusterPath { + line_index: u32, + run_index: u32, + logical_index: u32, +} + +impl ClusterPath { + /// Returns the path to the cluster for the given layout and byte index. + pub fn from_byte_index(layout: &Layout, byte_index: usize) -> Self { + let mut path = Self::default(); + if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { + path.line_index = line_index as u32; + for (run_index, run) in line.runs().enumerate() { + path.run_index = run_index as u32; + if !run.text_range().contains(&byte_index) { + continue; + } + for (cluster_index, cluster) in run.clusters().enumerate() { + path.logical_index = cluster_index as u32; + if cluster.text_range().contains(&byte_index) { + return path; + } + } + } + } + path + } + + /// Returns the path to the cluster and the clicked side for the given layout + /// and point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> (Self, Affinity) { + let mut path = Self::default(); + if let Some((line_index, line)) = layout.line_for_offset(y) { + path.line_index = line_index as u32; + let mut offset = 0.0; + let last_run_index = line.len().saturating_sub(1); + for (run_index, run) in line.runs().enumerate() { + let is_last_run = run_index == last_run_index; + let run_advance = run.advance(); + path.run_index = run_index as u32; + path.logical_index = 0; + if x > offset + run_advance && !is_last_run { + offset += run_advance; + continue; + } + let last_cluster_index = run.cluster_range().len().saturating_sub(1); + for (visual_index, cluster) in run.visual_clusters().enumerate() { + let is_last_cluster = is_last_run && visual_index == last_cluster_index; + path.logical_index = + run.visual_to_logical(visual_index).unwrap_or_default() as u32; + let cluster_advance = cluster.advance(); + let edge = offset; + offset += cluster_advance; + if x > offset && !is_last_cluster { + continue; + } + let affinity = + Affinity::new(cluster.is_rtl(), x <= edge + cluster_advance * 0.5); + return (path, affinity); + } + } + } + (path, Affinity::default()) + } + + /// Returns the index of the line containing this cluster. + pub fn line_index(&self) -> usize { + self.line_index as usize + } + + /// Returns the index of the run (within the owning line) containing this + /// cluster. + pub fn run_index(&self) -> usize { + self.run_index as usize + } + + /// Returns the logical index of the cluster within the owning run. + pub fn logical_index(&self) -> usize { + self.logical_index as usize + } + + /// Returns the line for this path and the specified layout. + pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + layout.get(self.line_index()) + } + + /// Returns the run for this path and the specified layout. + pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.line(layout)?.run(self.run_index()) + } + + /// Returns the cluster for this path and the specified layout. + pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.run(layout)?.get(self.logical_index()) + } + + /// Returns true if this cluster is at the beginning of a line. + pub fn is_start_of_line(&self, layout: &Layout) -> bool { + self.run_index == 0 + && self + .run(layout) + .and_then(|run| run.logical_to_visual(self.logical_index())) + == Some(0) + } + + /// Returns true if this cluster is at the end of a line. + pub fn is_end_of_line(&self, layout: &Layout) -> bool { + self.line(layout).map(|line| line.len().saturating_sub(1)) == Some(self.run_index()) + && self + .run(layout) + .map(|run| { + run.logical_to_visual(self.logical_index()) + == Some(run.cluster_range().len().saturating_sub(1)) + }) + .unwrap_or_default() + } + + /// If this cluster, combined with the given affinity, sits on a + /// directional boundary, returns the cluster that represents the alternate + /// insertion position. + /// + /// For example, if this cluster is a left-to-right cluster, then this + /// will return the cluster that represents the position where a + /// right-to-left character would be inserted, and vice versa. + pub fn bidi_link_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + affinity: Affinity, + ) -> Option<(Self, Cluster<'a, B>)> { + let run = self.run(layout)?; + let run_end = run.len().checked_sub(1)?; + let visual_index = run.logical_to_visual(self.logical_index())?; + let cluster = self.cluster(layout)?; + let is_rtl = cluster.is_rtl(); + let is_leading = affinity.is_visually_leading(is_rtl); + let at_start = visual_index == 0 && is_leading; + let at_end = visual_index == run_end && !is_leading; + let other_path = if (at_start && !is_rtl) || (at_end && is_rtl) { + let line = self.line(layout)?; + let prev_run_index = self.run_index().checked_sub(1)?; + let prev_run = line.run(prev_run_index)?; + ClusterPath { + line_index: self.line_index, + run_index: prev_run_index as u32, + logical_index: prev_run.len().checked_sub(1)? as u32, + } + } else if (at_end && !is_rtl) || (at_start && is_rtl) { + ClusterPath { + line_index: self.line_index, + run_index: self.run_index() as u32 + 1, + logical_index: 0, + } + } else { + return None; + }; + let other_cluster = other_path.cluster(layout)?; + if other_cluster.is_rtl() == is_rtl { + return None; + } + Some((other_path, other_path.cluster(layout)?)) + } + + /// Returns the path of the cluster that follows this one in visual order. + pub fn next_visual<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + let line = self.line(layout)?; + let run = line.run(self.run_index())?; + let visual_index = run.logical_to_visual(self.logical_index())?; + if let Some(cluster_index) = run.visual_to_logical(visual_index + 1) { + // Easy mode: next visual cluster is in the same run + Some(Self { + line_index: self.line_index, + run_index: self.run_index, + logical_index: cluster_index as u32, + }) + } else { + // We just want to find the first line/run following this one that + // contains any cluster. + let mut run_index = self.run_index() + 1; + for line_index in self.line_index()..layout.len() { + let line = layout.get(line_index)?; + for run_index in run_index..line.len() { + if let Some(run) = line.run(run_index) { + if !run.cluster_range().is_empty() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: run.visual_to_logical(0)? as u32, + }); + } + } + } + // Restart at first run on next line + run_index = 0; + } + None + } + } + + pub fn next_visual_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.next_visual(layout) + .and_then(|path| Some((path, path.cluster(layout)?))) + } + + /// Returns the path of the cluster that follows this one in logical order. + pub fn next_logical<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + let line = self.line(layout)?; + let run = line.run(self.run_index())?; + if self.logical_index() + 1 < run.cluster_range().len() { + // Easy mode: next cluster is in the same run + Some(Self { + line_index: self.line_index, + run_index: self.run_index, + logical_index: self.logical_index + 1, + }) + } else { + // We just want to find the first line/run following this one that + // contains any cluster. + let mut run_index = self.run_index() + 1; + for line_index in self.line_index()..layout.len() { + let line = layout.get(line_index)?; + for run_index in run_index..line.len() { + if let Some(run) = line.run(run_index) { + if !run.cluster_range().is_empty() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: 0, + }); + } + } + } + // Restart at first run on next line + run_index = 0; + } + None + } + } + + pub fn next_logical_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.next_logical(layout) + .and_then(|path| Some((path, path.cluster(layout)?))) + } + + /// Returns the path of the cluster that precedes this one in visual order. + pub fn previous_visual<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + let line = self.line(layout)?; + let run = line.run(self.run_index())?; + let visual_index = run.logical_to_visual(self.logical_index())?; + if let Some(cluster_index) = visual_index + .checked_sub(1) + .and_then(|visual_index| run.visual_to_logical(visual_index)) + { + // Easy mode: previous visual cluster is in the same run + Some(Self { + line_index: self.line_index, + run_index: self.run_index, + logical_index: cluster_index as u32, + }) + } else { + // We just want to find the first line/run preceding this one that + // contains any cluster. + let mut run_index = Some(self.run_index()); + for line_index in (0..=self.line_index()).rev() { + let line = layout.get(line_index)?; + let first_run = run_index.unwrap_or(line.len()); + for run_index in (0..first_run).rev() { + if let Some(run) = line.run(run_index) { + let range = run.cluster_range(); + if !range.is_empty() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: run.visual_to_logical(range.len() - 1)? as u32, + }); + } + } + } + // Restart at last run + run_index = None; + } + None + } + } + + pub fn previous_visual_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.previous_visual(layout) + .and_then(|path| Some((path, path.cluster(layout)?))) + } + + /// Returns the path of the cluster that precedes this one in logical + /// order. + pub fn previous_logical<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + if self.logical_index > 0 { + // Easy mode: previous cluster is in the same run + Some(Self { + line_index: self.line_index, + run_index: self.run_index, + logical_index: self.logical_index - 1, + }) + } else { + // We just want to find the first line/run preceding this one that + // contains any cluster. + let mut run_index = Some(self.run_index()); + for line_index in (0..=self.line_index()).rev() { + let line = layout.get(line_index)?; + let first_run = run_index.unwrap_or(line.len()); + for run_index in (0..first_run).rev() { + if let Some(run) = line.run(run_index) { + let range = run.cluster_range(); + if !range.is_empty() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: (range.len() - 1) as u32, + }); + } + } + } + // Restart at last run + run_index = None; + } + None + } + } + + pub fn previous_logical_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.previous_logical(layout) + .and_then(|path| Some((path, path.cluster(layout)?))) + } + + pub fn move_lines(&self, layout: &Layout, delta: i32) -> Self { + *self + } + + pub fn next_word(&self, layout: &Layout) -> Option { + let line_start = self.line_index(); + let mut run_start = self.run_index(); + let mut cluster_start = self.logical_index() + 1; + for line_index in line_start..layout.len() { + let line = layout.get(line_index)?; + for run_index in run_start..line.len() { + let run = line.run(run_index)?; + for cluster_index in cluster_start..run.len() { + let cluster = run.get(cluster_index)?; + if cluster.is_word_boundary() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: cluster_index as u32, + }); + } + } + cluster_start = 0; + } + run_start = 0; + } + None + } + + pub fn next_word_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.next_word(layout) + .and_then(|p| Some((p, p.cluster(layout)?))) + } + + pub fn previous_word(&self, layout: &Layout) -> Option { + let line_start = self.line_index(); + let mut run_start = Some(self.run_index() + 1); + let mut cluster_start = Some(self.logical_index()); + for line_index in (0..=line_start).rev() { + let line = layout.get(line_index)?; + let run_start = run_start.take().unwrap_or(line.len()); + for run_index in (0..run_start).rev() { + let run = line.run(run_index)?; + let cluster_start = cluster_start.take().unwrap_or(run.len()); + for cluster_index in (0..cluster_start).rev() { + let cluster = run.get(cluster_index)?; + if cluster.is_word_boundary() { + return Some(Self { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: cluster_index as u32, + }); + } + } + } + } + None + } + + pub fn previous_word_cluster<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> Option<(Self, Cluster<'a, B>)> { + self.previous_word(layout) + .and_then(|p| Some((p, p.cluster(layout)?))) + } + + /// Returns the visual offset of this cluster along direction of text flow. + /// + /// This cost of this function is roughly linear in the number of clusters + /// on the containing line. + pub fn visual_offset(&self, layout: &Layout) -> Option { + let line = self.line(layout)?; + let mut offset = 0.0; + for run_index in 0..=self.run_index() { + let run = line.run(run_index)?; + if run_index != self.run_index() { + offset += run.advance(); + } else { + let visual_index = run.logical_to_visual(self.logical_index())?; + for cluster in run.visual_clusters().take(visual_index) { + offset += cluster.advance(); + } + } + } + Some(offset) + } +} + #[derive(Clone)] enum GlyphIter<'a> { Single(Option), diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index fe6052da..4007ad21 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -1,382 +1,359 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Hit testing. +//! Text selection support. -use super::*; -use alloc::vec::Vec; +use super::{Affinity, Brush, ClusterPath, Layout}; +use core::ops::Range; use peniko::kurbo::Rect; -/// Represents a position within a layout. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub enum VisualCursorMode { + /// During cursor motion, affinity is adjusted to prioritize the dominant + /// direction of the layout. + /// + /// That is, if the base direction of the layout is left-to-right, then + /// the visual cursor will represent the position where the next + /// left-to-right character would be inserted, and vice versa. + /// + /// This matches the behavior of Pango's strong cursor. + #[default] + Strong, + /// During cursor motion, affinity is adjusted to prioritize the non-dominant + /// direction of the layout. + /// + /// That is, if the base direction of the layout is left-to-right, then + /// the visual cursor will represent the position where the next + /// right-to-left character would be inserted, and vice versa. + /// + /// This matches the behavior of Pango's weak cursor. + Weak, + /// During cursor motion, affinity is adjusted based on the directionality + /// of the incoming position. + /// + /// That is, if a directional boundary is entered from a left-to-right run + /// of text, then the cursor will represent the position where the next + /// left-to-right character would be inserted, and vice versa. + /// + /// This matches the behavior of Firefox. + Adaptive, +} + +impl VisualCursorMode { + /// Returns the preferred RTL state for the given layout. + /// + /// This is used to handle cursor modes when moving visually + /// by cluster. + fn prefer_rtl(self, layout: &Layout) -> Option { + match self { + Self::Strong => Some(layout.is_rtl()), + Self::Weak => Some(!layout.is_rtl()), + Self::Adaptive => None, + } + } +} + #[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Cursor { - /// Path to the target cluster. - pub path: CursorPath, - /// Offset to the baseline. - pub baseline: f32, - /// Offset to the target cluster along the baseline. - pub offset: f32, - /// Advance of the target cluster. - pub advance: f32, - /// Start of the target cluster. - pub text_start: u32, - /// End of the target cluster. - pub text_end: u32, - /// `true` if the target cluster is in a right-to-left run. - pub is_rtl: bool, - /// `true` if the cursor was created from a point or position inside the layout. - pub is_inside: bool, - /// Possible visual positions of the cursor. - pub placement: CursorPlacement, + path: ClusterPath, + index: u32, + text_start: u32, + text_end: u32, + visual_offset: f32, + is_rtl: bool, + affinity: Affinity, } impl Cursor { - /// Creates a new cursor from the specified layout and point. - pub fn from_point(layout: &Layout, mut x: f32, y: f32) -> Self { - let mut result = Self { - is_inside: x >= 0. && y >= 0., - ..Default::default() - }; - let last_line_index = layout.data.lines.len().saturating_sub(1); - if let Some((line_index, line)) = layout - .line_for_offset(y) - .or_else(|| Some((last_line_index, layout.get(last_line_index)?))) - { - let line_index = line_index as u32; - let line_metrics = line.metrics(); - if y > line_metrics.max_coord { - result.is_inside = false; - x = f32::MAX; - } else if y < 0.0 { - x = 0.0; - } - result.baseline = line_metrics.baseline; - result.path.line_index = line_index; - result.path.visual_line_index = line_index; - result.placement = CursorPlacement::Single(CursorPosition { - line_index: line_index as u32, - offset: 0.0, - }); - let mut cur_edge = line_metrics.offset; - let last_run_ix = line.data.item_range.len().saturating_sub(1); - for (run_index, run) in line.runs().enumerate() { - result.path.run_index = run_index as u32; - let last_cluster_ix = run.cluster_range().len().saturating_sub(1); - for (cluster_index, cluster) in run.visual_clusters().enumerate() { - let range = cluster.text_range(); - let advance = cluster.advance(); - if x <= cur_edge + advance * 0.5 { - let index = if run.is_rtl() { range.end } else { range.start }; - return Self::from_byte_index(layout, index); - } else if run.is_rtl() && x < cur_edge + advance { - return Self::from_byte_index(layout, range.start); - } else if cluster_index == last_cluster_ix && run_index == last_run_ix { - let mut cursor = Self::from_byte_index(layout, range.start + 1); - let offset = cursor.placement.primary_position().offset; - cursor.baseline = line_metrics.baseline; - cursor.path.visual_line_index = line_index; - cursor.offset = line_metrics.offset + line_metrics.advance; - cursor.placement = CursorPlacement::LineBoundary { - prefer_end: true, - end: CursorPosition { - line_index, - offset: line_metrics.offset + line_metrics.advance, - }, - start: CursorPosition { - line_index: cursor.path.line_index, - offset, - }, - }; - return cursor; - } else if x < cur_edge + advance { - return Self::from_byte_index(layout, range.end); - } - cur_edge += advance; - } - } - } - result.is_inside = false; - result + /// Creates a new cursor for the given layout and point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (path, affinity) = ClusterPath::from_point(layout, x, y); + Self::from_cluster_path(layout, path, affinity) } - /// Creates a new cursor for the specified layout and text position. - pub fn from_byte_index(layout: &Layout, mut index: usize) -> Self { - let mut result = Self { - is_inside: true, - ..Default::default() - }; - if index >= layout.data.text_len { - result.is_inside = false; - result.text_start = layout.data.text_len as u32; - result.text_end = result.text_start; - index = layout.data.text_len; - } - let last_line_index = layout.data.lines.len().saturating_sub(1); - if let Some((line_index, line)) = layout - .line_for_byte_index(index) - .or_else(|| Some((last_line_index, layout.get(last_line_index)?))) - { - let line_index = line_index as u32; - let line_metrics = line.metrics(); - result.baseline = line_metrics.baseline; - result.path.line_index = line_index; - result.path.visual_line_index = line_index; - let mut last_edge = line_metrics.offset; - result.offset = last_edge; - let mut last_is_rtl = None; - let mut last_run_end = 0.0; - let mut last_run_start = 0.0; - for (run_index, run) in line.runs().enumerate() { - let is_rtl = run.is_rtl(); - result.path.run_index = run_index as u32; - if !run.text_range().contains(&index) { - last_run_start = last_edge; - last_edge += run.advance(); - result.offset = last_edge; - last_is_rtl = Some(is_rtl); - last_run_end = last_edge; - continue; - } - let last_cluster_ix = run.cluster_range().len().saturating_sub(1); - for (cluster_index, cluster) in run.visual_clusters().enumerate() { - let range = cluster.text_range(); - result.text_start = range.start as u32; - result.text_end = range.end as u32; - result.offset = last_edge; - result.placement = CursorPlacement::Single(CursorPosition { - line_index: line_index as u32, - offset: last_edge, - }); - result.is_rtl = run.is_rtl(); - result.path.cluster_index = - run.visual_to_logical(cluster_index).unwrap() as u32; - let advance = cluster.advance(); - result.advance = advance; - if range.contains(&index) { - if cluster_index == last_cluster_ix - && last_is_rtl.is_some() - && last_is_rtl != Some(is_rtl) - { - if is_rtl { - result.placement = CursorPlacement::DirectionalBoundary { - primary: CursorPosition { - line_index, - offset: last_run_end, - }, - secondary: CursorPosition { - line_index, - offset: last_edge + advance, - }, - }; - } else { - result.placement = CursorPlacement::DirectionalBoundary { - primary: CursorPosition { - line_index, - offset: last_edge, - }, - secondary: CursorPosition { - line_index, - offset: last_run_start, - }, - }; - } - } else if is_rtl || !result.is_inside { - result.placement = CursorPlacement::Single(CursorPosition { - line_index, - offset: last_edge + advance, - }); - } - // if is_rtl && cluster_index == last_cluster_ix && !last_is_rtl { - // result.offset = last_run_end; - // } else if is_rtl || !result.is_inside { - // result.offset += advance; - // result.advance = -advance; - // } - return result; + /// Returns a new cursor for the given layout, byte index and affinity. + pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + let path = ClusterPath::from_byte_index(layout, index); + Self::from_cluster_path(layout, path, affinity) + } + + fn from_cluster_path( + layout: &Layout, + path: ClusterPath, + affinity: Affinity, + ) -> Self { + let (index, text_start, text_end, visual_offset, is_rtl) = + if let Some(cluster) = path.cluster(layout) { + let mut range = cluster.text_range(); + let index = range.start as u32; + let mut offset = path.visual_offset(layout).unwrap_or_default(); + let is_rtl = cluster.is_rtl(); + let is_left_side = affinity.is_visually_leading(is_rtl); + if !is_left_side { + offset += cluster.advance(); + if !is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); } - last_edge += advance; + } else if is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); } - } - result.placement = CursorPlacement::Single(CursorPosition { - line_index, - offset: last_edge, - }); + ( + index, + range.start as u32, + range.end as u32, + offset, + cluster.is_rtl(), + ) + } else { + Default::default() + }; + Self { + path, + index, + text_start, + text_end, + visual_offset, + is_rtl, + affinity, } - result.is_inside = false; - result } + /// Returns a new cursor with internal state recomputed to match the given + /// layout. + /// + /// This should be called whenever the layout is rebuilt or resized. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + Self::from_index(layout, self.index as usize, self.affinity) + } + + /// Returns the path to the target cluster. + pub fn cluster_path(&self) -> ClusterPath { + self.path + } + + /// Returns the text range of the target cluster. pub fn text_range(&self) -> Range { self.text_start as usize..self.text_end as usize } - fn at_end_of_line(&self) -> bool { - match self.placement { - CursorPlacement::LineBoundary { prefer_end, .. } => prefer_end, - _ => false, - } + /// Returns the visual offset of the target cluster along the direction of + /// text flow. + pub fn visual_offset(&self) -> f32 { + self.visual_offset } -} - -/// Index based path to a cluster. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] -pub struct CursorPath { - /// Index of the containing line. - pub line_index: u32, - /// Index of the run within the containing line. - pub run_index: u32, - /// Index of the cluster within the containing run. - pub cluster_index: u32, - /// Index of the line containing the visual representation of the - /// cursor. - pub visual_line_index: u32, -} -impl CursorPath { - /// Returns the line for this path and the specified layout. - pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.line_index as usize) + /// Returns the byte index associated with the cursor. + pub fn index(&self) -> usize { + self.index as usize } - /// Returns the run for this path and the specified layout. - pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.line(layout)?.run(self.run_index as usize) + /// Returns the associated affinity for this cursor. + pub fn affinity(&self) -> Affinity { + self.affinity } - /// Returns the cluster for this path and the specified layout. - pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.run(layout)?.get(self.cluster_index as usize) + /// Returns the visual geometry of the cursor where the next character + /// matching the base direction of the layout would be inserted. + /// + /// If the current cursor is not on a directional boundary, this is also + /// the location where characters opposite the base direction would be + /// inserted. + pub fn strong_geometry(&self, layout: &Layout, size: f32) -> Option { + if self.is_rtl == layout.is_rtl() { + self.geometry(layout, size) + } else { + self.bidi_link_geometry(layout, size) + .or_else(|| self.geometry(layout, size)) + } } - pub fn visual_line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.visual_line_index as usize) + /// Returns the visual geometry of the cursor where the next character + /// that is opposite the base direction of the layout would be inserted. + /// + /// This returns `None` when the current cursor is not on a directional + /// boundary. + pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { + // Weak cursor only exists if we're on a directional boundary + let bidi_link = self.bidi_link_geometry(layout, size)?; + if self.is_rtl == layout.is_rtl() { + Some(bidi_link) + } else { + self.geometry(layout, size) + } } -} -/// Describes the possible visual positions where a [`Cursor`] may be rendered. -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum CursorPlacement { - /// A cursor with a single visual position. - Single(CursorPosition), - /// A cursor that sits on a boundary where the text direction changes. - DirectionalBoundary { - /// Indicates the visual position where left-to-right text would be - /// inserted. - /// - /// This is considered the primary position because it is the one - /// used by all text editors. - primary: CursorPosition, - /// Indicates the visual position where right-to-left text would be - /// inserted. - secondary: CursorPosition, - }, - /// A cursor that sits on a line boundary. - LineBoundary { - /// True when the `end` position is preferred. This is set when the - /// cursor is generated as the result of a hit test that explicitly - /// targeted the end of the line. - prefer_end: bool, - /// The visual position at the end of previous line. - end: CursorPosition, - /// The visual position at the start of the current line. This always - /// represents the logical position of the cursor. - start: CursorPosition, - }, -} + fn geometry(&self, layout: &Layout, size: f32) -> Option { + let metrics = *self.path.line(layout)?.metrics(); + let line_x = self.visual_offset as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + size as f64, + metrics.max_coord as f64, + )) + } -impl CursorPlacement { - /// Returns the primary position for the cursor. - /// - /// This is the generally preferred position that matches what the majority - /// of text editors would display. - pub fn primary_position(&self) -> CursorPosition { - match *self { - Self::Single(pos) => pos, - Self::DirectionalBoundary { primary, .. } => primary, - Self::LineBoundary { - prefer_end, - end, - start, - } => { - if prefer_end { - end - } else { - start + fn bidi_link_geometry(&self, layout: &Layout, size: f32) -> Option { + let (path, cluster) = self.path.bidi_link_cluster(layout, self.affinity)?; + let mut line_x = path.visual_offset(layout)? as f64; + let run = path.run(layout)?; + if run.logical_to_visual(path.logical_index())? != 0 { + line_x += cluster.advance() as f64; + } + let metrics = *path.line(layout)?.metrics(); + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + size as f64, + metrics.max_coord as f64, + )) + } + + pub fn next_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + let prefer_rtl = mode.prefer_rtl(layout); + if self.affinity.is_visually_leading(self.is_rtl) { + // Check for directional boundary condition + if let Some((next_path, next_cluster)) = self.path.next_visual_cluster(layout) { + if next_cluster.is_rtl() != self.is_rtl { + println!("MOVING RIGHT INTO BIDI BOUNDARY"); + if let Some(prefer_rtl) = prefer_rtl { + if self.is_rtl != prefer_rtl { + return Self::from_cluster_path( + layout, + next_path, + self.affinity.invert(), + ); + } + } } } + // We're moving right so we want to track right-side affinity; + // let's swap. + Self::from_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { + let next_rtl = next_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { + if next_next_cluster.is_rtl() != next_rtl { + println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if next_rtl != prefer_rtl { + return Self::from_cluster_path(layout, next_next, self.affinity); + } else { + return Self::from_cluster_path(layout, next, self.affinity); + } + } + } + } + let affinity = if self.is_rtl != next_rtl { + // println!("MOVING INTO BIDI BOUNDARY"); + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, next, affinity) + } else { + *self + } } } - /// Returns the alternate position fo the cursor, if one is available. - /// - /// Some text editors will use this to display the two potential insertion - /// points for mixed-direction text. - pub fn alternate_position(&self) -> Option { - match *self { - Self::Single(_) => None, - Self::DirectionalBoundary { secondary, .. } => Some(secondary), - Self::LineBoundary { - prefer_end, - end, - start, - } => { - if prefer_end { - Some(start) - } else { - Some(end) + pub fn previous_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + let prefer_rtl = mode.prefer_rtl(layout); + if !self.affinity.is_visually_leading(self.is_rtl) { + // Check for directional boundary condition + if let Some((prev_path, prev_cluster)) = self.path.previous_visual_cluster(layout) { + if prev_cluster.is_rtl() != self.is_rtl { + println!("MOVING LEFT INTO BIDI BOUNDARY"); + if let Some(prefer_rtl) = prefer_rtl { + if self.is_rtl != prefer_rtl { + return Self::from_cluster_path( + layout, + prev_path, + self.affinity.invert(), + ); + } + } } } + // We're moving left so we want to track left-side affinity; + // let's swap + Self::from_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { + let prev_rtl = prev_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { + if prev_prev_cluster.is_rtl() != prev_rtl { + println!("MOVING LEFT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if prev_rtl != prefer_rtl { + return Self::from_cluster_path(layout, prev_prev, self.affinity); + } else { + return Self::from_cluster_path(layout, prev, self.affinity); + } + } + } + } + let affinity = if self.is_rtl != prev_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, prev, affinity) + } else { + *self + } } } -} -impl Default for CursorPlacement { - fn default() -> Self { - Self::Single(CursorPosition::default()) + pub fn next_word(&self, layout: &Layout) -> Self { + let mut next_path = if self.affinity == Affinity::Upstream { + self.path.next_logical(layout).unwrap_or(self.path) + } else { + self.path + }; + while let Some((path, cluster)) = next_path.next_word_cluster(layout) { + next_path = path; + if !cluster.is_space_or_nbsp() { + break; + } + } + Self::from_cluster_path(layout, next_path, Affinity::default()) } -} -/// Visual position of a [`Cursor`]. -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct CursorPosition { - /// Index of the line containing the cursor. - pub line_index: u32, - /// Offset of the cursor along the direction of the line. - pub offset: f32, -} - -impl CursorPosition { - /// Returns the line for this position and the specified layout. - pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.line_index as usize) + pub fn previous_word(&self, layout: &Layout) -> Self { + let mut next_path = if self.affinity == Affinity::Upstream { + self.path.next_logical(layout).unwrap_or(self.path) + } else { + self.path + }; + // let mut next_path = self.path; + while let Some((path, cluster)) = next_path.previous_word_cluster(layout) { + next_path = path; + if !cluster.is_space_or_nbsp() { + break; + } + } + Self::from_cluster_path(layout, next_path, Affinity::default()) } } -/// Returns a point that is falls within the vertical bounds of the given line. -fn line_y(line: &Line) -> f32 { - line.metrics().baseline - line.metrics().ascent * 0.5 -} - -fn visual_for_cursor( - layout: &Layout, - cursor_pos: Option, -) -> Option { - let pos = cursor_pos?; - pos.line(layout).map(|line| { - let metrics = line.metrics(); - let line_x = pos.offset as f64; - peniko::kurbo::Line::new( - (line_x, metrics.min_coord as f64), - (line_x, metrics.max_coord as f64), - ) - }) -} - #[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Selection { anchor: Cursor, focus: Cursor, - focus_at_end: bool, + /// Current horizontal position. Used for tracking line movement. h_pos: Option, } @@ -385,35 +362,74 @@ impl From for Selection { Self { anchor: value, focus: value, - focus_at_end: value.at_end_of_line(), h_pos: None, } } } impl Selection { + /// Creates a collapsed selection with the anchor and focus set to the + /// position associated with the given point. pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { Cursor::from_point(layout, x, y).into() } - pub fn from_byte_index(layout: &Layout, index: usize) -> Self { - Cursor::from_byte_index(layout, index).into() + /// Creates a collapsed selection with the anchor and focus set to the + /// position associated with the given byte index and affinity. + pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + Cursor::from_index(layout, index, affinity).into() } + /// Creates a new selection bounding the word at the given coordinates. + pub fn word_from_point(layout: &Layout, x: f32, y: f32) -> Self { + let mut anchor = Cursor::from_point(layout, x, y); + if !(anchor.affinity == Affinity::Downstream + && anchor + .cluster_path() + .cluster(layout) + .map(|cluster| cluster.is_word_boundary()) + .unwrap_or_default()) + { + anchor = anchor.previous_word(layout); + } + let mut focus = anchor.next_word(layout); + if anchor.is_rtl { + core::mem::swap(&mut anchor, &mut focus); + } + Self { + anchor, + focus, + h_pos: None, + } + } + + /// Returns the anchor point of the selection. + /// + /// This represents the location where the selection was initiated. pub fn anchor(&self) -> &Cursor { &self.anchor } + /// Returns the focus point of the selection. + /// + /// This represents the current location of the selection. pub fn focus(&self) -> &Cursor { &self.focus } + /// Returns true when the anchor and focus are at the same position. pub fn is_collapsed(&self) -> bool { self.anchor.text_start == self.focus.text_start } + /// Returns the range of text bounded by this selection. + /// + /// This is equivalent to the text that would be removed when pressing the + /// delete key. pub fn text_range(&self) -> Range { - if self.anchor.text_start < self.focus.text_start { + if self.is_collapsed() { + self.focus.text_range() + } else if self.anchor.text_start < self.focus.text_start { self.anchor.text_start as usize..self.focus.text_start as usize } else { self.focus.text_start as usize..self.anchor.text_start as usize @@ -426,72 +442,88 @@ impl Selection { self.focus.text_start as usize } + /// Returns a new collapsed selection at the position of the current + /// focus. #[must_use] pub fn collapse(&self) -> Self { Self { anchor: self.focus, focus: self.focus, - focus_at_end: self.focus_at_end, h_pos: self.h_pos, } } + /// Refreshes the internal cursor state to match the the given layout. + /// + /// This should be called whenever the layout is rebuilt or resized. #[must_use] pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = Cursor::from_byte_index(layout, self.anchor.text_start as usize); - let focus = Cursor::from_byte_index(layout, self.focus.text_start as usize); - let focus = - if self.focus_at_end && focus.path.run_index == 0 && focus.path.cluster_index == 0 { - // Hack! - // On resize, keep track of cursor positions that were set at end - // of the line. - if let Some(prev_line) = focus - .path - .line_index - .checked_sub(1) - .and_then(|line_ix| layout.get(line_ix as usize)) - { - let y = prev_line.metrics().baseline; - Cursor::from_point(layout, f32::MAX, y) - } else { - focus - } - } else { - focus - }; + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); Self { anchor, focus, - focus_at_end: self.focus_at_end, h_pos: None, } } + /// Returns a new selection with the focus extended to the given point. #[must_use] pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { let focus = Cursor::from_point(layout, x, y); Self { anchor: self.anchor, focus, - focus_at_end: focus.at_end_of_line(), h_pos: None, } } + /// Returns a new selection with the focus moved to the next cluster in + /// visual order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] - pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend( - Cursor::from_byte_index(layout, self.focus.text_end as usize), - extend, - ) + pub fn next_visual( + &self, + layout: &Layout, + mode: VisualCursorMode, + extend: bool, + ) -> Self { + self.maybe_extend(self.focus.next_visual(layout, mode), extend) } + /// Returns a new selection with the focus moved to the previous cluster in + /// visual order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] - pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend( - Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), - extend, - ) + pub fn previous_visual( + &self, + layout: &Layout, + mode: VisualCursorMode, + extend: bool, + ) -> Self { + self.maybe_extend(self.focus.previous_visual(layout, mode), extend) + } + + /// Returns a new selection with the focus moved to the next word. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_word(layout), extend) + } + + /// Returns a new selection with the focus moved to the previous word. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_word(layout), extend) } fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { @@ -499,7 +531,6 @@ impl Selection { Self { anchor: self.anchor, focus, - focus_at_end: focus.at_end_of_line(), h_pos: None, } } else { @@ -507,110 +538,114 @@ impl Selection { } } + /// Returns a new selection with the focus moved to the start of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - if let Some(y) = self - .focus - .placement - .primary_position() - .line(layout) - .map(|line| line_y(&line)) - { - self.maybe_extend(Cursor::from_point(layout, 0.0, y), extend) + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_index(layout, line.text_range().start, Affinity::Downstream), + extend, + ) } else { *self } } + /// Returns a new selection with the focus moved to the end of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - if let Some(y) = self - .focus - .placement - .primary_position() - .line(layout) - .map(|line| line_y(&line)) - { - self.maybe_extend(Cursor::from_point(layout, f32::MAX, y), extend) + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_index( + layout, + line.text_range().end.saturating_sub(1), + Affinity::Upstream, + ), + extend, + ) } else { *self } } + /// Returns a new selection with the focus moved to the next line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { self.move_line(layout, 1, extend).unwrap_or(*self) } + /// Returns a new selection with the focus moved to the previous line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. #[must_use] - pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + pub fn previous_line(&self, layout: &Layout, extend: bool) -> Self { self.move_line(layout, -1, extend).unwrap_or(*self) } fn move_line( &self, layout: &Layout, - line_delta: i32, + line_delta: isize, extend: bool, ) -> Option { let line_index = self .focus - .placement - .primary_position() - .line_index + .path + .line_index() .saturating_add_signed(line_delta); - let line = layout.get(line_index as usize)?; + let line = layout.get(line_index)?; let y = line.metrics().baseline - line.metrics().ascent * 0.5; - let h_pos = self.h_pos.unwrap_or(self.focus.offset); + let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); let new_focus = Cursor::from_point(layout, h_pos, y); let h_pos = Some(h_pos); Some(if extend { Self { anchor: self.anchor, focus: new_focus, - focus_at_end: new_focus.at_end_of_line(), h_pos, } } else { Self { anchor: new_focus, focus: new_focus, - focus_at_end: new_focus.at_end_of_line(), h_pos, } }) } - pub fn visual_focus(&self, layout: &Layout) -> Option { - visual_for_cursor(layout, Some(self.focus.placement.primary_position())) - } - - pub fn visual_alternate_focus( - &self, - layout: &Layout, - ) -> Option { - visual_for_cursor(layout, self.focus.placement.alternate_position()) - } - - pub fn visual_anchor(&self, layout: &Layout) -> Option { - self.anchor.path.visual_line(layout).map(|line| { - let metrics = line.metrics(); - let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - let line_max = line_min + metrics.line_height as f64; - let line_x = self.anchor.offset as f64; - peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) - }) - } - - pub fn visual_regions(&self, layout: &Layout) -> Vec { + /// Returns a vector containing the rectangles which represent the visual + /// geometry of this selection for the given layout. + /// + /// This is a convenience method built on [`geometry_with`](Self::geometry_with). + pub fn geometry(&self, layout: &Layout) -> Vec { let mut rects = Vec::new(); - self.visual_regions_with(layout, |rect| rects.push(rect)); + self.geometry_with(layout, |rect| rects.push(rect)); rects } - pub fn visual_regions_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + /// Invokes `f` with the sequence of rectangles which represent the visual + /// geometry of this selection for the given layout. + /// + /// This avoids allocation if the intent is to render the rectangles + /// immediately. + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { // Ensure we add some visual indicator for selected empty // lines. + // Make this configurable? const MIN_RECT_WIDTH: f64 = 4.0; if self.is_collapsed() { return; @@ -621,8 +656,8 @@ impl Selection { core::mem::swap(&mut start, &mut end); } let text_range = start.text_start..end.text_start; - let line_start_ix = start.path.visual_line_index; - let line_end_ix = end.path.visual_line_index; + let line_start_ix = start.path.line_index(); + let line_end_ix = end.path.line_index(); for line_ix in line_start_ix..=line_end_ix { let Some(line) = layout.get(line_ix as usize) else { continue; diff --git a/parley/src/layout/cursor2.rs b/parley/src/layout/cursor2.rs new file mode 100644 index 00000000..691d44e7 --- /dev/null +++ b/parley/src/layout/cursor2.rs @@ -0,0 +1,527 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +use super::{Affinity, Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; +use core::ops::Range; + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub enum CursorMode { + #[default] + Strong, + Weak, +} + +fn resolve_cursor_mode( + layout: &Layout, + path: ClusterPath, + mode: Option, +) -> CursorMode { + mode.unwrap_or_else(|| { + let layout_rtl = layout.data.base_level & 1 != 0; + let cluster_rtl = path + .cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(); + if layout_rtl == cluster_rtl { + CursorMode::Strong + } else { + CursorMode::Weak + } + }) +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Cursor { + path: ClusterPath, + index: u32, + text_start: u32, + text_end: u32, + visual_offset: f32, + mode: CursorMode, + is_rtl: bool, + affinity: Affinity, +} + +impl Cursor { + pub fn from_point( + layout: &Layout, + mode: Option, + x: f32, + y: f32, + ) -> Self { + let (path, affinity) = ClusterPath::from_point(layout, x, y); + Self::from_cluster_path( + layout, + path, + resolve_cursor_mode(layout, path, mode), + affinity, + ) + } + + pub fn from_byte_index( + layout: &Layout, + mode: Option, + byte_index: usize, + affinity: Affinity, + ) -> Self { + let path = ClusterPath::from_byte_index(layout, byte_index); + Self::from_cluster_path( + layout, + path, + resolve_cursor_mode(layout, path, mode), + affinity, + ) + } + + fn from_cluster_path( + layout: &Layout, + path: ClusterPath, + mode: CursorMode, + mut affinity: Affinity, + ) -> Self { + let (index, text_start, text_end, visual_offset, is_rtl, affinity) = + if let Some(cluster) = path.cluster(layout) { + let mut range = cluster.text_range(); + let index = range.start as u32; + let mut offset = path.visual_offset(layout).unwrap_or_default(); + let layout_is_rtl = layout.data.base_level & 1 != 0; + let is_rtl = cluster.is_rtl(); + let is_left_side = affinity.is_visually_leading(is_rtl); + if !is_left_side { + offset += cluster.advance(); + if !is_rtl { + range = next_logical_range(layout, path).unwrap_or(range.end..range.end); + } + } else if is_rtl { + range = next_logical_range(layout, path).unwrap_or(range.end..range.end); + } + ( + index, + range.start as u32, + range.end as u32, + offset, + cluster.is_rtl(), + affinity, + ) + } else { + Default::default() + }; + Self { + path, + index, + text_start, + text_end, + visual_offset, + mode, + is_rtl, + affinity, + } + } + + #[must_use] + fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, Some(self.mode), self.index as usize, self.affinity) + } + + /// Returns the path to the target cluster. + pub fn cluster_path(&self) -> ClusterPath { + self.path + } + + /// Returns the text range of the target cluster. + pub fn text_range(&self) -> Range { + self.text_start as usize..self.text_end as usize + } + + /// Returns the visual offset of the target cluster along the direction of + /// text flow. + pub fn visual_offset(&self) -> f32 { + self.visual_offset + } + + /// Returns the byte index associated with the cursor. + pub fn index(&self) -> usize { + self.index as usize + } + + pub fn affinity(&self) -> Affinity { + self.affinity + } + + #[must_use] + pub fn swap_mode(&self, layout: &Layout) -> Self { + let new_mode = match self.mode { + CursorMode::Strong => CursorMode::Weak, + CursorMode::Weak => CursorMode::Strong, + }; + Self::from_cluster_path(layout, self.path, new_mode, self.affinity) + } + + pub fn geometry(&self, layout: &Layout, width: f32) -> Option { + let metrics = *self.path.line(layout)?.metrics(); + let line_x = self.visual_offset as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + width as f64, + metrics.max_coord as f64, + )) + } + + pub fn next_visual(&self, layout: &Layout) -> Self { + if self.affinity.is_visually_leading(self.is_rtl) { + // We're moving right so we want to track right-side affinity; + // let's swap + Self::from_byte_index( + layout, + Some(self.mode), + self.index as usize, + self.affinity.invert(), + ) + } else { + if let Some(path) = self.path.next_visual(layout) { + let next_rtl = path + .cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(); + let affinity = if self.is_rtl != next_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, path, self.mode, affinity) + } else { + *self + } + } + } + + pub fn previous_visual(&self, layout: &Layout) -> Self { + if !self.affinity.is_visually_leading(self.is_rtl) { + // We're moving left so we want to track left-side affinity; + // let's swap + Self::from_byte_index( + layout, + Some(self.mode), + self.index as usize, + self.affinity.invert(), + ) + } else { + if let Some(path) = self.path.previous_visual(layout) { + let next_rtl = path + .cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(); + let affinity = if self.is_rtl != next_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, path, self.mode, affinity) + } else { + *self + } + } + } +} + +fn next_logical_range(layout: &Layout, path: ClusterPath) -> Option> { + Some(path.next_logical(layout)?.cluster(layout)?.text_range()) +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + pub fn from_point( + layout: &Layout, + mode: Option, + x: f32, + y: f32, + ) -> Self { + Cursor::from_point(layout, mode, x, y).into() + } + + pub fn from_byte_index( + layout: &Layout, + mode: Option, + index: usize, + affinity: Affinity, + ) -> Self { + Cursor::from_byte_index(layout, mode, index, affinity).into() + } + + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + pub fn focus(&self) -> &Cursor { + &self.focus + } + + pub fn is_collapsed(&self) -> bool { + self.anchor.text_start == self.focus.text_start + } + + pub fn text_range(&self) -> Range { + if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start as usize..self.focus.text_start as usize + } else { + self.focus.text_start as usize..self.anchor.text_start as usize + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start as usize + } + + #[must_use] + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + Self { + anchor, + focus, + h_pos: None, + } + } + + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, Some(self.focus.mode), x, y); + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } + + // #[must_use] + // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_end as usize), + // extend, + // ) + // } + + // #[must_use] + // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), + // extend, + // ) + // } + + #[must_use] + pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_visual(layout), extend) + } + + #[must_use] + pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_visual(layout), extend) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_byte_index( + layout, + Some(self.focus.mode), + line.text_range().start, + Affinity::Downstream, + ), + extend, + ) + } else { + *self + } + } + + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_byte_index( + layout, + Some(self.focus.mode), + line.text_range().end.saturating_sub(1), + Affinity::Upstream, + ), + extend, + ) + } else { + *self + } + } + + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + #[must_use] + pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + // let line_index = self + // .focus + // .placement + // .primary_position() + // .line_index + // .saturating_add_signed(line_delta); + let line_index = self + .focus + .path + .line_index() + .saturating_add_signed(line_delta); + let line = layout.get(line_index)?; + let y = line.metrics().baseline - line.metrics().ascent * 0.5; + let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); + let new_focus = Cursor::from_point(layout, Some(self.focus.mode), h_pos, y); + let h_pos = Some(h_pos); + Some(if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + } + }) + } + + pub fn visual_focus(&self, layout: &Layout) -> Option { + self.focus.geometry(layout, 1.5) + } + + // pub fn visual_alternate_focus( + // &self, + // layout: &Layout, + // ) -> Option { + // visual_for_cursor(layout, self.focus.placement.alternate_position()) + // } + + // pub fn visual_anchor(&self, layout: &Layout) -> Option { + // self.anchor.path.visual_line(layout).map(|line| { + // let metrics = line.metrics(); + // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + // let line_max = line_min + metrics.line_height as f64; + // let line_x = self.anchor.offset as f64; + // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) + // }) + // } + + pub fn geometry(&self, layout: &Layout) -> Vec { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect| rects.push(rect)); + rects + } + + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.path.line_index(); + let line_end_ix = end.path.line_index(); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix as usize) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&(cluster.text_range().start as u32)) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } +} diff --git a/parley/src/layout/cursor3.rs b/parley/src/layout/cursor3.rs new file mode 100644 index 00000000..6e6691c9 --- /dev/null +++ b/parley/src/layout/cursor3.rs @@ -0,0 +1,641 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +use super::{Affinity, Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; +use core::ops::Range; + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub enum CursorMode { + #[default] + Strong, + Weak, +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Cursor { + pub index: CursorIndex, + text_start: u32, + text_end: u32, +} + +impl Cursor { + pub fn from_point( + layout: &Layout, + mode: Option, + x: f32, + y: f32, + ) -> Self { + let (path, affinity) = ClusterPath::from_point(layout, x, y); + if let Some(cluster) = path.cluster(layout) { + let index = if affinity.is_visually_leading(cluster.is_rtl()) { + cluster.text_range().start + } else { + // path.next_logical(layout).and_then(|p| p.cluster(layout)).and_then(|c| c.text_range().start)) + cluster.text_range().end + }; + Self::from_byte_index(layout, mode, index, Affinity::Downstream) + } else { + Self::default() + } + } + + pub fn from_byte_index( + layout: &Layout, + mode: Option, + index: usize, + affinity: Affinity, + ) -> Self { + Self::from_cursor_index(layout, CursorIndex::new(layout, index, affinity)) + } + + fn from_cursor_index(layout: &Layout, index: CursorIndex) -> Self { + let range = index.text_range(layout); + Self { + index, + text_start: range.start as u32, + text_end: range.end as u32, + } + } + + pub fn index(&self) -> usize { + self.index.index as usize + } + + pub fn affinity(&self) -> Affinity { + self.index.affinity + } + + pub fn text_range(&self) -> Range { + self.text_start as usize..self.text_end as usize + } + + #[must_use] + fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, None, self.index.index as usize, self.index.affinity) + } + + pub fn geometry(&self, layout: &Layout, size: f32) -> Option { + self.index.geometry(layout, size) + } + + pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { + self.index.weak_geometry(layout, size) + } + + pub fn next_visual(&self, layout: &Layout) -> Self { + self.index + .next_visual(layout) + .map(|ix| Self::from_cursor_index(layout, ix)) + .unwrap_or(*self) + } + + pub fn previous_visual(&self, layout: &Layout) -> Self { + self.index + .previous_visual(layout) + .map(|ix| Self::from_cursor_index(layout, ix)) + .unwrap_or(*self) + } +} + +fn next_logical_range(layout: &Layout, path: ClusterPath) -> Option> { + Some(path.next_logical(layout)?.cluster(layout)?.text_range()) +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub enum ClusterPartition { + /// No partition. + #[default] + None, + /// Text direction changes between two clusters. + TextDirection, + /// Soft line break between two clusters. + SoftLine, + /// Hard line break between two clusters. + HardLine, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct CursorIndex { + pub index: u32, + pub affinity: Affinity, + pub kind: CursorIndexKind, +} + +impl Default for CursorIndex { + fn default() -> Self { + Self { + index: 0, + affinity: Default::default(), + kind: CursorIndexKind::Start(Default::default()), + } + } +} + +impl CursorIndex { + pub fn new(layout: &Layout, index: usize, affinity: Affinity) -> Self { + let index = index.min(layout.data.text_len); + let kind = CursorIndexKind::new(layout, index); + Self { + index: index as u32, + affinity, + kind, + } + } + + pub fn next_visual(&self, layout: &Layout) -> Option { + match self.kind { + CursorIndexKind::Start(path) => { + let next = path.next_visual(layout)?; + let index = next.cluster(layout)?.text_range().start; + Some(Self::new(layout, index, self.affinity)) + } + CursorIndexKind::Between(prev, next, _partition) => { + let cluster = prev.cluster(layout)?; + let new = prev.next_visual(layout)?.next_visual(layout)?; + let new_cluster = new.cluster(layout)?; + let range = new_cluster.text_range(); + let index = if new_cluster.is_rtl() { + range.end + } else { + range.start + }; + let affinity = if cluster.is_rtl() != new_cluster.is_rtl() { + self.affinity.invert() + } else { + self.affinity + }; + Some(Self::new(layout, index, affinity)) + } + CursorIndexKind::End(_) => None, + } + } + + pub fn previous_visual(&self, layout: &Layout) -> Option { + match self.kind { + CursorIndexKind::Start(_) => None, + CursorIndexKind::Between(prev, _next, _partition) => { + let cluster = prev.cluster(layout)?; + let new = prev.previous_visual(layout)?; + let new_cluster = new.cluster(layout)?; + let range = new_cluster.text_range(); + let index = if new_cluster.is_rtl() { + range.end + } else { + range.start + }; + let affinity = if cluster.is_rtl() != new_cluster.is_rtl() { + self.affinity.invert() + } else { + self.affinity + }; + Some(Self::new(layout, index, affinity)) + } + CursorIndexKind::End(path) => { + let prev = path.previous_visual(layout)?; + let index = prev.cluster(layout)?.text_range().end; + Some(Self::new(layout, index, self.affinity)) + } + } + } + + pub fn line_index(&self) -> usize { + match self.kind { + CursorIndexKind::Start(path) | CursorIndexKind::End(path) => path.line_index(), + CursorIndexKind::Between(prev, next, _partition) => { + let path = match self.affinity { + Affinity::Upstream => prev, + Affinity::Downstream => next, + }; + path.line_index() + } + } + } + + pub fn text_range(&self, layout: &Layout) -> Range { + match self.kind { + CursorIndexKind::Start(path) => path + .cluster(layout) + .map(|c| c.text_range()) + .unwrap_or_default(), + CursorIndexKind::Between(prev, next, _partition) => { + let path = match self.affinity { + Affinity::Downstream => prev, + Affinity::Upstream => next, + }; + path.cluster(layout) + .map(|c| c.text_range()) + .unwrap_or_default() + } + CursorIndexKind::End(_path) => layout.data.text_len..layout.data.text_len, + } + } + + pub fn geometry(&self, layout: &Layout, size: f32) -> Option { + let (line_index, offset) = match self.kind { + CursorIndexKind::Start(path) => { + let cluster = path.cluster(layout)?; + let line_index = path.line_index(); + let mut offset = path.visual_offset(layout)?; + if cluster.is_rtl() { + offset += cluster.advance(); + } + (line_index, offset) + } + CursorIndexKind::Between(path, _next, _partition) => { + let cluster = path.cluster(layout)?; + let line_index = path.line_index(); + let mut offset = path.visual_offset(layout)?; + if self.affinity.is_visually_leading(cluster.is_rtl()) { + offset += cluster.advance(); + } + (line_index, offset) + } + CursorIndexKind::End(path) => { + let cluster = path.cluster(layout)?; + let line_index = path.line_index(); + let mut offset = path.visual_offset(layout)?; + if !cluster.is_rtl() { + offset += cluster.advance(); + } + (line_index, offset) + } + }; + let line = layout.get(line_index)?; + let metrics = line.metrics(); + Some(Rect::new( + offset as f64, + metrics.min_coord as f64, + offset as f64 + size as f64, + metrics.max_coord as f64, + )) + } + + pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { + match self.kind { + CursorIndexKind::Start(_) + | CursorIndexKind::End(_) + | CursorIndexKind::Between( + _, + _, + ClusterPartition::None | ClusterPartition::SoftLine | ClusterPartition::HardLine, + ) => None, + CursorIndexKind::Between(prev, next, ClusterPartition::TextDirection) => { + let path = match self.affinity { + Affinity::Downstream => next, + Affinity::Upstream => prev, + }; + let cluster = path.cluster(layout)?; + let line_index = path.line_index(); + let mut offset = path.visual_offset(layout)? + cluster.advance(); + // if cluster.is_rtl() { + // offset += cluster.advance(); + // } + let line = layout.get(line_index)?; + let metrics = line.metrics(); + Some(Rect::new( + offset as f64, + metrics.min_coord as f64, + offset as f64 + size as f64, + metrics.max_coord as f64, + )) + } + } + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum CursorIndexKind { + /// Index is at the start of the text. + Start(ClusterPath), + /// Index is between two clusters in logical order. + Between(ClusterPath, ClusterPath, ClusterPartition), + /// Index is at the end of the text. + End(ClusterPath), +} + +impl CursorIndexKind { + pub fn new(layout: &Layout, index: usize) -> Self { + let path = ClusterPath::from_byte_index(layout, index); + if index >= layout.data.text_len { + Self::End(path) + } else if let Some(prev_path) = path.previous_logical(layout) { + let partition = if let Some((cluster, prev_cluster)) = + path.cluster(layout).zip(prev_path.cluster(layout)) + { + if prev_path.line_index() != path.line_index() { + if prev_cluster.is_hard_line_break() { + ClusterPartition::HardLine + } else { + ClusterPartition::SoftLine + } + } else if cluster.is_rtl() != prev_cluster.is_rtl() { + ClusterPartition::TextDirection + } else { + ClusterPartition::None + } + } else { + ClusterPartition::None + }; + Self::Between(prev_path, path, partition) + } else { + Self::Start(path) + } + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + pub fn from_point( + layout: &Layout, + mode: Option, + x: f32, + y: f32, + ) -> Self { + Cursor::from_point(layout, mode, x, y).into() + } + + pub fn from_byte_index( + layout: &Layout, + mode: Option, + index: usize, + affinity: Affinity, + ) -> Self { + Cursor::from_byte_index(layout, mode, index, affinity).into() + } + + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + pub fn focus(&self) -> &Cursor { + &self.focus + } + + pub fn is_collapsed(&self) -> bool { + self.anchor.text_start == self.focus.text_start + } + + pub fn text_range(&self) -> Range { + if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start as usize..self.focus.text_start as usize + } else { + self.focus.text_start as usize..self.anchor.text_start as usize + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start as usize + } + + #[must_use] + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + Self { + anchor, + focus, + h_pos: None, + } + } + + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, None, x, y); + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } + + // #[must_use] + // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_end as usize), + // extend, + // ) + // } + + // #[must_use] + // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), + // extend, + // ) + // } + + #[must_use] + pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_visual(layout), extend) + } + + #[must_use] + pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_visual(layout), extend) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + // if let Some(line) = self.focus.path.line(layout) { + // self.maybe_extend( + // Cursor::from_byte_index( + // layout, + // Some(self.focus.mode), + // line.text_range().start, + // Affinity::Downstream, + // ), + // extend, + // ) + // } else { + // *self + // } + *self + } + + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + // if let Some(line) = self.focus.path.line(layout) { + // self.maybe_extend( + // Cursor::from_byte_index( + // layout, + // Some(self.focus.mode), + // line.text_range().end.saturating_sub(1), + // Affinity::Upstream, + // ), + // extend, + // ) + // } else { + // *self + // } + *self + } + + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + #[must_use] + pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + // let line_index = self + // .focus + // .placement + // .primary_position() + // .line_index + // .saturating_add_signed(line_delta); + + // let line_index = self + // .focus + // .path + // .line_index() + // .saturating_add_signed(line_delta); + // let line = layout.get(line_index)?; + // let y = line.metrics().baseline - line.metrics().ascent * 0.5; + // let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); + // let new_focus = Cursor::from_point(layout, Some(self.focus.mode), h_pos, y); + // let h_pos = Some(h_pos); + // Some(if extend { + // Self { + // anchor: self.anchor, + // focus: new_focus, + // h_pos, + // } + // } else { + // Self { + // anchor: new_focus, + // focus: new_focus, + // h_pos, + // } + // }) + None + } + + // pub fn visual_alternate_focus( + // &self, + // layout: &Layout, + // ) -> Option { + // visual_for_cursor(layout, self.focus.placement.alternate_position()) + // } + + // pub fn visual_anchor(&self, layout: &Layout) -> Option { + // self.anchor.path.visual_line(layout).map(|line| { + // let metrics = line.metrics(); + // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + // let line_max = line_min + metrics.line_height as f64; + // let line_x = self.anchor.offset as f64; + // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) + // }) + // } + + pub fn geometry(&self, layout: &Layout) -> Vec { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect| rects.push(rect)); + rects + } + + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.index.line_index(); + let line_end_ix = end.index.line_index(); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix as usize) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&(cluster.text_range().start as u32)) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } +} diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 3aaf3875..96d09bb5 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -21,6 +21,7 @@ use data::*; use swash::text::cluster::{Boundary, ClusterInfo}; use swash::{GlyphId, NormalizedCoord, Synthesis}; +pub use cluster::{Affinity, ClusterPath}; pub use cursor::Cursor; pub use line::greedy::BreakLines; pub use line::{GlyphRun, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; @@ -93,6 +94,11 @@ impl Layout { }) } + /// Returns true if the dominant direction of the layout is right-to-left. + pub fn is_rtl(&self) -> bool { + self.data.base_level & 1 != 0 + } + pub fn inline_boxes(&self) -> &[InlineBox] { &self.data.inline_boxes } diff --git a/parley/src/layout/run.rs b/parley/src/layout/run.rs index 43c8305c..9e7f3b6a 100644 --- a/parley/src/layout/run.rs +++ b/parley/src/layout/run.rs @@ -106,6 +106,22 @@ impl<'a, B: Brush> Run<'a, B> { } } + /// Returns the visual cluster index for the specified logical cluster index. + pub fn logical_to_visual(&self, logical_index: usize) -> Option { + let num_clusters = self.len(); + if logical_index >= num_clusters { + return None; + } + + let visual_index = if self.is_rtl() { + num_clusters - 1 - logical_index + } else { + logical_index + }; + + Some(visual_index) + } + /// Returns the logical cluster index for the specified visual cluster index. pub fn visual_to_logical(&self, visual_index: usize) -> Option { let num_clusters = self.len(); diff --git a/parley/src/layout/select.rs b/parley/src/layout/select.rs new file mode 100644 index 00000000..61cd4501 --- /dev/null +++ b/parley/src/layout/select.rs @@ -0,0 +1,700 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +use super::{Affinity, Brush, ClusterPath, Layout}; +use core::ops::Range; + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub enum VisualCursorMode { + /// During cursor motion, affinity is adjusted to prioritize the dominant + /// direction of the layout. + /// + /// That is, if the base direction of the layout is left-to-right, then + /// the visual cursor will represent the position where the next + /// left-to-right character would be inserted, and vice versa. + /// + /// This matches the behavior of Pango's strong cursor. + #[default] + Strong, + /// During cursor motion, affinity is adjusted to prioritize the non-dominant + /// direction of the layout. + /// + /// That is, if the base direction of the layout is left-to-right, then + /// the visual cursor will represent the position where the next + /// right-to-left character would be inserted, and vice versa. + /// + /// This matches the behavior of Pango's weak cursor. + Weak, + /// During cursor motion, affinity is adjusted based on the directionality + /// of the incoming position. + /// + /// That is, if a directional boundary is entered from a left-to-right run + /// of text, then the cursor will represent the position where the next + /// left-to-right character would be inserted, and vice versa. + /// + /// This matches the behavior of Firefox. + Adaptive, +} + +impl VisualCursorMode { + /// Returns the preferred RTL state for the given layout. + /// + /// This is used to handle cursor modes when moving visually + /// by cluster. + fn prefer_rtl(self, layout: &Layout) -> Option { + match self { + Self::Strong => Some(layout.is_rtl()), + Self::Weak => Some(!layout.is_rtl()), + Self::Adaptive => None, + } + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Cursor { + path: ClusterPath, + index: u32, + text_start: u32, + text_end: u32, + visual_offset: f32, + is_rtl: bool, + affinity: Affinity, +} + +impl Cursor { + /// Creates a new cursor for the given layout and point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (path, affinity) = ClusterPath::from_point(layout, x, y); + Self::from_cluster_path(layout, path, affinity) + } + + /// Returns a new cursor for the given layout, byte index and affinity. + pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + let path = ClusterPath::from_byte_index(layout, index); + Self::from_cluster_path(layout, path, affinity) + } + + fn from_cluster_path( + layout: &Layout, + path: ClusterPath, + affinity: Affinity, + ) -> Self { + let (index, text_start, text_end, visual_offset, is_rtl) = + if let Some(cluster) = path.cluster(layout) { + let mut range = cluster.text_range(); + let index = range.start as u32; + let mut offset = path.visual_offset(layout).unwrap_or_default(); + let is_rtl = cluster.is_rtl(); + let is_left_side = affinity.is_visually_leading(is_rtl); + if !is_left_side { + offset += cluster.advance(); + if !is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } + } else if is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } + ( + index, + range.start as u32, + range.end as u32, + offset, + cluster.is_rtl(), + ) + } else { + Default::default() + }; + Self { + path, + index, + text_start, + text_end, + visual_offset, + is_rtl, + affinity, + } + } + + /// Returns a new cursor with internal state recomputed to match the given + /// layout. + /// + /// This should be called whenever the layout is rebuilt or resized. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + Self::from_index(layout, self.index as usize, self.affinity) + } + + /// Returns the path to the target cluster. + pub fn cluster_path(&self) -> ClusterPath { + self.path + } + + /// Returns the text range of the target cluster. + pub fn text_range(&self) -> Range { + self.text_start as usize..self.text_end as usize + } + + /// Returns the visual offset of the target cluster along the direction of + /// text flow. + pub fn visual_offset(&self) -> f32 { + self.visual_offset + } + + /// Returns the byte index associated with the cursor. + pub fn index(&self) -> usize { + self.index as usize + } + + /// Returns the associated affinity for this cursor. + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Returns the visual geometry of the cursor where the next character + /// matching the base direction of the layout would be inserted. + /// + /// If the current cursor is not on a directional boundary, this is also + /// the location where characters opposite the base direction would be + /// inserted. + pub fn strong_geometry(&self, layout: &Layout, size: f32) -> Option { + if self.is_rtl == layout.is_rtl() { + self.geometry(layout, size) + } else { + self.bidi_link_geometry(layout, size) + .or_else(|| self.geometry(layout, size)) + } + } + + /// Returns the visual geometry of the cursor where the next character + /// that is opposite the base direction of the layout would be inserted. + /// + /// This returns `None` when the current cursor is not on a directional + /// boundary. + pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { + // Weak cursor only exists if we're on a directional boundary + let bidi_link = self.bidi_link_geometry(layout, size)?; + if self.is_rtl == layout.is_rtl() { + Some(bidi_link) + } else { + self.geometry(layout, size) + } + } + + fn geometry(&self, layout: &Layout, size: f32) -> Option { + let metrics = *self.path.line(layout)?.metrics(); + let line_x = self.visual_offset as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + size as f64, + metrics.max_coord as f64, + )) + } + + fn bidi_link_geometry(&self, layout: &Layout, size: f32) -> Option { + let (path, cluster) = self.path.bidi_link_cluster(layout, self.affinity)?; + let mut line_x = path.visual_offset(layout)? as f64; + let run = path.run(layout)?; + if run.logical_to_visual(path.logical_index())? != 0 { + line_x += cluster.advance() as f64; + } + let metrics = *path.line(layout)?.metrics(); + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + size as f64, + metrics.max_coord as f64, + )) + } + + pub fn next_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + let prefer_rtl = mode.prefer_rtl(layout); + if self.affinity.is_visually_leading(self.is_rtl) { + // Check for directional boundary condition + if let Some((next_path, next_cluster)) = self.path.next_visual_cluster(layout) { + if next_cluster.is_rtl() != self.is_rtl { + println!("MOVING RIGHT INTO BIDI BOUNDARY"); + if let Some(prefer_rtl) = prefer_rtl { + if self.is_rtl != prefer_rtl { + return Self::from_cluster_path( + layout, + next_path, + self.affinity.invert(), + ); + } + } + } + } + // We're moving right so we want to track right-side affinity; + // let's swap. + Self::from_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { + let next_rtl = next_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { + if next_next_cluster.is_rtl() != next_rtl { + println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if next_rtl != prefer_rtl { + return Self::from_cluster_path(layout, next_next, self.affinity); + } else { + return Self::from_cluster_path(layout, next, self.affinity); + } + } + } + } + let affinity = if self.is_rtl != next_rtl { + // println!("MOVING INTO BIDI BOUNDARY"); + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, next, affinity) + } else { + *self + } + } + } + + pub fn previous_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + let prefer_rtl = mode.prefer_rtl(layout); + if !self.affinity.is_visually_leading(self.is_rtl) { + // Check for directional boundary condition + if let Some((prev_path, prev_cluster)) = self.path.previous_visual_cluster(layout) { + if prev_cluster.is_rtl() != self.is_rtl { + println!("MOVING LEFT INTO BIDI BOUNDARY"); + if let Some(prefer_rtl) = prefer_rtl { + if self.is_rtl != prefer_rtl { + return Self::from_cluster_path( + layout, + prev_path, + self.affinity.invert(), + ); + } + } + } + } + // We're moving left so we want to track left-side affinity; + // let's swap + Self::from_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { + let prev_rtl = prev_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { + if prev_prev_cluster.is_rtl() != prev_rtl { + println!("MOVING LEFT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if prev_rtl != prefer_rtl { + return Self::from_cluster_path(layout, prev_prev, self.affinity); + } else { + return Self::from_cluster_path(layout, prev, self.affinity); + } + } + } + } + let affinity = if self.is_rtl != prev_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, prev, affinity) + } else { + *self + } + } + } + + pub fn next_word(&self, layout: &Layout) -> Self { + let mut next_path = if self.affinity == Affinity::Upstream { + self.path.next_logical(layout).unwrap_or(self.path) + } else { + self.path + }; + while let Some((path, cluster)) = next_path.next_word_cluster(layout) { + next_path = path; + if !cluster.is_space_or_nbsp() { + break; + } + } + Self::from_cluster_path(layout, next_path, Affinity::default()) + } + + pub fn previous_word(&self, layout: &Layout) -> Self { + let mut next_path = if self.affinity == Affinity::Upstream { + self.path.next_logical(layout).unwrap_or(self.path) + } else { + self.path + }; + // let mut next_path = self.path; + while let Some((path, cluster)) = next_path.previous_word_cluster(layout) { + next_path = path; + if !cluster.is_space_or_nbsp() { + break; + } + } + Self::from_cluster_path(layout, next_path, Affinity::default()) + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + /// Current horizontal position. Used for tracking line movement. + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + /// Creates a collapsed selection with the anchor and focus set to the + /// position associated with the given point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + /// Creates a collapsed selection with the anchor and focus set to the + /// position associated with the given byte index and affinity. + pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + Cursor::from_index(layout, index, affinity).into() + } + + /// Creates a new selection bounding the word at the given coordinates. + pub fn word_from_point(layout: &Layout, x: f32, y: f32) -> Self { + let mut anchor = Cursor::from_point(layout, x, y); + if !(anchor.affinity == Affinity::Downstream + && anchor + .cluster_path() + .cluster(layout) + .map(|cluster| cluster.is_word_boundary()) + .unwrap_or_default()) + { + anchor = anchor.previous_word(layout); + } + let mut focus = anchor.next_word(layout); + if anchor.is_rtl { + core::mem::swap(&mut anchor, &mut focus); + } + Self { + anchor, + focus, + h_pos: None, + } + } + + /// Returns the anchor point of the selection. + /// + /// This represents the location where the selection was initiated. + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + /// Returns the focus point of the selection. + /// + /// This represents the current location of the selection. + pub fn focus(&self) -> &Cursor { + &self.focus + } + + /// Returns true when the anchor and focus are at the same position. + pub fn is_collapsed(&self) -> bool { + self.anchor.text_start == self.focus.text_start + } + + /// Returns the range of text bounded by this selection. + /// + /// This is equivalent to the text that would be removed when pressing the + /// delete key. + pub fn text_range(&self) -> Range { + if self.is_collapsed() { + self.focus.text_range() + } else if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start as usize..self.focus.text_start as usize + } else { + self.focus.text_start as usize..self.anchor.text_start as usize + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start as usize + } + + /// Returns a new collapsed selection at the position of the current + /// focus. + #[must_use] + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + /// Refreshes the internal cursor state to match the the given layout. + /// + /// This should be called whenever the layout is rebuilt or resized. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + Self { + anchor, + focus, + h_pos: None, + } + } + + /// Returns a new selection with the focus extended to the given point. + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, x, y); + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } + + /// Returns a new selection with the focus moved to the next cluster in + /// visual order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_visual( + &self, + layout: &Layout, + mode: VisualCursorMode, + extend: bool, + ) -> Self { + self.maybe_extend(self.focus.next_visual(layout, mode), extend) + } + + /// Returns a new selection with the focus moved to the previous cluster in + /// visual order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_visual( + &self, + layout: &Layout, + mode: VisualCursorMode, + extend: bool, + ) -> Self { + self.maybe_extend(self.focus.previous_visual(layout, mode), extend) + } + + /// Returns a new selection with the focus moved to the next word. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_word(layout), extend) + } + + /// Returns a new selection with the focus moved to the previous word. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_word(layout), extend) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + /// Returns a new selection with the focus moved to the start of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_index(layout, line.text_range().start, Affinity::Downstream), + extend, + ) + } else { + *self + } + } + + /// Returns a new selection with the focus moved to the end of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_index( + layout, + line.text_range().end.saturating_sub(1), + Affinity::Upstream, + ), + extend, + ) + } else { + *self + } + } + + /// Returns a new selection with the focus moved to the next line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + /// Returns a new selection with the focus moved to the previous line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + let line_index = self + .focus + .path + .line_index() + .saturating_add_signed(line_delta); + let line = layout.get(line_index)?; + let y = line.metrics().baseline - line.metrics().ascent * 0.5; + let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); + let new_focus = Cursor::from_point(layout, h_pos, y); + let h_pos = Some(h_pos); + Some(if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + } + }) + } + + /// Returns a vector containing the rectangles which represent the visual + /// geometry of this selection for the given layout. + /// + /// This is a convenience method built on [`geometry_with`](Self::geometry_with). + pub fn geometry(&self, layout: &Layout) -> Vec { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect| rects.push(rect)); + rects + } + + /// Invokes `f` with the sequence of rectangles which represent the visual + /// geometry of this selection for the given layout. + /// + /// This avoids allocation if the intent is to render the rectangles + /// immediately. + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + // Make this configurable? + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.path.line_index(); + let line_end_ix = end.path.line_index(); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix as usize) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&(cluster.text_range().start as u32)) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } +} diff --git a/parley/src/layout/select2.rs b/parley/src/layout/select2.rs new file mode 100644 index 00000000..7e0007f1 --- /dev/null +++ b/parley/src/layout/select2.rs @@ -0,0 +1,491 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +use super::{Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; +use core::ops::Range; + +/// Determines how a cursor attaches to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub enum Affinity { + /// Left side for LTR clusters and right side for RTL clusters. + #[default] + Downstream = 0, + /// Right side for LTR clusters and left side for RTL clusters. + Upstream = 1, +} + +impl Affinity { + pub fn from_rtl_and_side(is_rtl: bool, side: ClusterSide) -> Self { + match (is_rtl, side) { + // right edge of RTL and left edge of LTR + (true, ClusterSide::Trailing) | (false, ClusterSide::Leading) => Affinity::Downstream, + // left edge of RTL and right edge of LTR + (true, ClusterSide::Leading) | (false, ClusterSide::Trailing) => Affinity::Upstream, + } + } + + pub fn invert(&self) -> Self { + match self { + Self::Downstream => Self::Upstream, + Self::Upstream => Self::Downstream, + } + } + + /// Returns true if the cursor should be placed on the left side. + pub fn is_left_side(&self, is_rtl: bool) -> bool { + match (*self, is_rtl) { + (Self::Upstream, true) | (Self::Downstream, false) => true, + (Self::Upstream, false) | (Self::Downstream, true) => false, + } + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Cursor { + path: ClusterPath, + index: u32, + text_start: u32, + text_end: u32, + visual_offset: f32, + is_rtl: bool, + affinity: Affinity, +} + +impl Cursor { + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (path, side) = ClusterPath::from_point(layout, x, y); + let affinity = Affinity::from_rtl_and_side( + path.cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(), + side, + ); + Self::from_cluster_path(layout, path, affinity) + } + + pub fn from_byte_index( + layout: &Layout, + byte_index: usize, + affinity: Affinity, + ) -> Self { + let path = ClusterPath::from_byte_index(layout, byte_index); + Self::from_cluster_path(layout, path, affinity) + } + + fn from_cluster_path( + layout: &Layout, + path: ClusterPath, + affinity: Affinity, + ) -> Self { + let (index, text_start, text_end, visual_offset, is_rtl) = + if let Some(cluster) = path.cluster(layout) { + let mut range = cluster.text_range(); + let index = range.start as u32; + let mut offset = path.visual_offset(layout).unwrap_or_default(); + let is_rtl = cluster.is_rtl(); + let is_left_side = affinity.is_left_side(is_rtl); + if !is_left_side { + offset += cluster.advance(); + if !is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } + } else if is_rtl { + range = path + .next_logical(layout) + .and_then(|path| path.cluster(layout)) + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } + ( + index, + range.start as u32, + range.end as u32, + offset, + cluster.is_rtl(), + ) + } else { + Default::default() + }; + Self { + path, + index, + text_start, + text_end, + visual_offset, + is_rtl, + affinity, + } + } + + #[must_use] + fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, self.index as usize, self.affinity) + } + + /// Returns the path to the target cluster. + pub fn cluster_path(&self) -> ClusterPath { + self.path + } + + /// Returns the text range of the target cluster. + pub fn text_range(&self) -> Range { + self.text_start as usize..self.text_end as usize + } + + /// Returns the visual offset of the target cluster along the direction of + /// text flow. + pub fn visual_offset(&self) -> f32 { + self.visual_offset + } + + /// Returns the byte index associated with the cursor. + pub fn index(&self) -> usize { + self.index as usize + } + + pub fn affinity(&self) -> Affinity { + self.affinity + } + + pub fn geometry(&self, layout: &Layout, width: f32) -> Option { + let metrics = *self.path.line(layout)?.metrics(); + let line_x = self.visual_offset as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + width as f64, + metrics.max_coord as f64, + )) + } + + pub fn next_visual(&self, layout: &Layout) -> Self { + if self.affinity.is_left_side(self.is_rtl) { + // We're moving right so we want to track right-side affinity; + // let's swap + Self::from_byte_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some(path) = self.path.next_visual(layout) { + let next_rtl = path + .cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(); + let affinity = if self.is_rtl != next_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, path, affinity) + } else { + *self + } + } + } + + pub fn previous_visual(&self, layout: &Layout) -> Self { + if !self.affinity.is_left_side(self.is_rtl) { + // We're moving left so we want to track left-side affinity; + // let's swap + Self::from_byte_index(layout, self.index as usize, self.affinity.invert()) + } else { + if let Some(path) = self.path.previous_visual(layout) { + let next_rtl = path + .cluster(layout) + .map(|cluster| cluster.is_rtl()) + .unwrap_or_default(); + let affinity = if self.is_rtl != next_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, path, affinity) + } else { + *self + } + } + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + Cursor::from_byte_index(layout, index, affinity).into() + } + + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + pub fn focus(&self) -> &Cursor { + &self.focus + } + + pub fn is_collapsed(&self) -> bool { + self.anchor.text_start == self.focus.text_start + } + + pub fn text_range(&self) -> Range { + if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start as usize..self.focus.text_start as usize + } else { + self.focus.text_start as usize..self.anchor.text_start as usize + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start as usize + } + + #[must_use] + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + Self { + anchor, + focus, + h_pos: None, + } + } + + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, x, y); + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } + + // #[must_use] + // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_end as usize), + // extend, + // ) + // } + + // #[must_use] + // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), + // extend, + // ) + // } + + #[must_use] + pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_visual(layout), extend) + } + + #[must_use] + pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_visual(layout), extend) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_byte_index(layout, line.text_range().start, Affinity::Downstream), + extend, + ) + } else { + *self + } + } + + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some(line) = self.focus.path.line(layout) { + self.maybe_extend( + Cursor::from_byte_index( + layout, + line.text_range().end.saturating_sub(1), + Affinity::Upstream, + ), + extend, + ) + } else { + *self + } + } + + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + #[must_use] + pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + // let line_index = self + // .focus + // .placement + // .primary_position() + // .line_index + // .saturating_add_signed(line_delta); + let line_index = self + .focus + .path + .line_index() + .saturating_add_signed(line_delta); + let line = layout.get(line_index)?; + let y = line.metrics().baseline - line.metrics().ascent * 0.5; + let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); + let new_focus = Cursor::from_point(layout, h_pos, y); + let h_pos = Some(h_pos); + Some(if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + } + }) + } + + pub fn visual_focus(&self, layout: &Layout) -> Option { + self.focus.geometry(layout, 1.5) + } + + // pub fn visual_alternate_focus( + // &self, + // layout: &Layout, + // ) -> Option { + // visual_for_cursor(layout, self.focus.placement.alternate_position()) + // } + + // pub fn visual_anchor(&self, layout: &Layout) -> Option { + // self.anchor.path.visual_line(layout).map(|line| { + // let metrics = line.metrics(); + // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + // let line_max = line_min + metrics.line_height as f64; + // let line_x = self.anchor.offset as f64; + // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) + // }) + // } + + pub fn geometry(&self, layout: &Layout) -> Vec { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect| rects.push(rect)); + rects + } + + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.path.line_index(); + let line_end_ix = end.path.line_index(); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix as usize) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&(cluster.text_range().start as u32)) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } +} diff --git a/parley/src/layout/selection.rs b/parley/src/layout/selection.rs new file mode 100644 index 00000000..087b57b0 --- /dev/null +++ b/parley/src/layout/selection.rs @@ -0,0 +1,413 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +use super::{Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; +use core::ops::Range; + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Cursor { + path: ClusterPath, + index: u32, + text_start: u32, + text_end: u32, + visual_offset: f32, + is_rtl: bool, +} + +impl Cursor { + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (mut path, side) = ClusterPath::from_point(layout, x, y); + if side == ClusterSide::Trailing { + path = path.next_visual(layout).unwrap_or(path); + } + Self::from_cluster_path(layout, path) + } + + pub fn from_byte_index(layout: &Layout, byte_index: usize) -> Self { + let path = ClusterPath::from_byte_index(layout, byte_index); + Self::from_cluster_path(layout, path) + } + + fn from_cluster_path( + layout: &Layout, + mut path: ClusterPath, + ) -> Self { + // if side == ClusterSide::Trailing { + // path = path.next_visual(layout).unwrap_or(path) + // }; + let (index, text_start, text_end, visual_offset, is_rtl) = + if let Some(cluster) = path.cluster(layout) { + let range = cluster.text_range(); + let index = range.start as u32; + let mut offset = path.visual_offset(layout).unwrap_or_default(); + if cluster.is_rtl() { + //offset += cluster.advance(); + } + ( + index, + range.start as u32, + range.end as u32, + offset, + cluster.is_rtl(), + ) + } else { + Default::default() + }; + Self { + path, + index, + text_start, + text_end, + visual_offset, + is_rtl, + } + } + + #[must_use] + fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, self.index as usize) + } + + /// Returns the path to the target cluster. + pub fn cluster_path(&self) -> ClusterPath { + self.path + } + + /// Returns the text range of the target cluster. + pub fn text_range(&self) -> Range { + self.text_start as usize..self.text_end as usize + } + + /// Returns the visual offset of the target cluster along the direction of + /// text flow. + pub fn visual_offset(&self) -> f32 { + self.visual_offset + } + + /// Returns the byte index associated with the cursor. + pub fn index(&self) -> usize { + self.index as usize + } + + pub fn geometry(&self, layout: &Layout, width: f32) -> Option { + let metrics = *self.path.line(layout)?.metrics(); + let line_x = self.visual_offset as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + width as f64, + metrics.max_coord as f64, + )) + } + + pub fn weak_geometry(&self, layout: &Layout, width: f32) -> Option { + let alternate = self.path.alternate_path(layout)?; + let metrics = *alternate.line(layout)?.metrics(); + let line_x = alternate.visual_offset(layout)? as f64; + Some(Rect::new( + line_x, + metrics.min_coord as f64, + line_x + width as f64, + metrics.max_coord as f64, + )) + } + + pub fn next_visual(&self, layout: &Layout) -> Self { + if let Some(path) = self.path.next_visual(layout) { + Self::from_cluster_path(layout, path) + } else { + *self + } + } + + pub fn previous_visual(&self, layout: &Layout) -> Self { + if let Some(path) = self.path.previous_visual(layout) { + Self::from_cluster_path(layout, path) + } else { + *self + } + } +} + +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + h_pos: Option, +} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self { + anchor: value, + focus: value, + h_pos: None, + } + } +} + +impl Selection { + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + pub fn from_byte_index(layout: &Layout, index: usize) -> Self { + Cursor::from_byte_index(layout, index).into() + } + + pub fn anchor(&self) -> &Cursor { + &self.anchor + } + + pub fn focus(&self) -> &Cursor { + &self.focus + } + + pub fn is_collapsed(&self) -> bool { + self.anchor.text_start == self.focus.text_start + } + + pub fn text_range(&self) -> Range { + if self.anchor.text_start < self.focus.text_start { + self.anchor.text_start as usize..self.focus.text_start as usize + } else { + self.focus.text_start as usize..self.anchor.text_start as usize + } + } + + /// Returns the index where text should be inserted based on this + /// selection. + pub fn insertion_index(&self) -> usize { + self.focus.text_start as usize + } + + #[must_use] + pub fn collapse(&self) -> Self { + Self { + anchor: self.focus, + focus: self.focus, + h_pos: self.h_pos, + } + } + + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + Self { + anchor, + focus, + h_pos: None, + } + } + + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + let focus = Cursor::from_point(layout, x, y); + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } + + // #[must_use] + // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_end as usize), + // extend, + // ) + // } + + // #[must_use] + // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { + // self.maybe_extend( + // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), + // extend, + // ) + // } + + #[must_use] + pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_visual(layout), extend) + } + + #[must_use] + pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_visual(layout), extend) + } + + fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self { + anchor: self.anchor, + focus, + h_pos: None, + } + } else { + focus.into() + } + } + + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + // if let Some(line) = self.focus.path.line(layout) { + // self.maybe_extend( + // Cursor::from_byte_index(layout, line.text_range().start, Affinity::Downstream), + // extend, + // ) + // } else { + // *self + // } + *self + } + + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + // if let Some(line) = self.focus.path.line(layout) { + // self.maybe_extend( + // Cursor::from_byte_index( + // layout, + // line.text_range().end.saturating_sub(1), + // Affinity::Upstream, + // ), + // extend, + // ) + // } else { + // *self + // } + *self + } + + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, 1, extend).unwrap_or(*self) + } + + #[must_use] + pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_line(layout, -1, extend).unwrap_or(*self) + } + + fn move_line( + &self, + layout: &Layout, + line_delta: isize, + extend: bool, + ) -> Option { + // let line_index = self + // .focus + // .placement + // .primary_position() + // .line_index + // .saturating_add_signed(line_delta); + let line_index = self + .focus + .path + .line_index() + .saturating_add_signed(line_delta); + let line = layout.get(line_index)?; + let y = line.metrics().baseline - line.metrics().ascent * 0.5; + let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); + let new_focus = Cursor::from_point(layout, h_pos, y); + let h_pos = Some(h_pos); + Some(if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + } + }) + } + + pub fn visual_focus(&self, layout: &Layout) -> Option { + self.focus.geometry(layout, 1.5) + } + + // pub fn visual_alternate_focus( + // &self, + // layout: &Layout, + // ) -> Option { + // visual_for_cursor(layout, self.focus.placement.alternate_position()) + // } + + // pub fn visual_anchor(&self, layout: &Layout) -> Option { + // self.anchor.path.visual_line(layout).map(|line| { + // let metrics = line.metrics(); + // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; + // let line_max = line_min + metrics.line_height as f64; + // let line_x = self.anchor.offset as f64; + // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) + // }) + // } + + pub fn geometry(&self, layout: &Layout) -> Vec { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect| rects.push(rect)); + rects + } + + pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { + // Ensure we add some visual indicator for selected empty + // lines. + const MIN_RECT_WIDTH: f64 = 4.0; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.text_start > end.text_start { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.text_start..end.text_start; + let line_start_ix = start.path.line_index(); + let line_end_ix = end.path.line_index(); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix as usize) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.min_coord as f64; + let line_max = metrics.max_coord as f64; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64; + let mut cur_x = start_x; + for run in line.runs() { + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64; + if text_range.contains(&(cluster.text_range().start as u32)) { + cur_x += advance; + } else { + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x as _, line_min, start_x + width, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + if cur_x != start_x { + let width = (cur_x - start_x).max(MIN_RECT_WIDTH); + f(Rect::new(start_x, line_min, start_x + width, line_max)); + } + } else { + let x = metrics.offset as f64; + let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); + f(Rect::new(x, line_min, x + width, line_max)); + } + } + } +} From f380d80f34d8c85ae4d723ba569769391e75bc39 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 20 Aug 2024 22:46:53 -0400 Subject: [PATCH 09/18] remove experimental code; make clippy happy --- examples/vello_editor/src/text.rs | 2 +- parley/src/layout/cluster.rs | 12 +- parley/src/layout/cursor.rs | 84 ++-- parley/src/layout/cursor2.rs | 527 ---------------------- parley/src/layout/cursor3.rs | 641 --------------------------- parley/src/layout/select.rs | 700 ------------------------------ parley/src/layout/select2.rs | 491 --------------------- parley/src/layout/selection.rs | 413 ------------------ 8 files changed, 45 insertions(+), 2825 deletions(-) delete mode 100644 parley/src/layout/cursor2.rs delete mode 100644 parley/src/layout/cursor3.rs delete mode 100644 parley/src/layout/select.rs delete mode 100644 parley/src/layout/select2.rs delete mode 100644 parley/src/layout/selection.rs diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 8ccc1eed..3af08e33 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -161,7 +161,7 @@ impl Editor { } KeyCode::Backspace => { let start = if self.selection.is_collapsed() { - let end = self.selection.focus().text_range().start as usize; + let end = self.selection.focus().text_range().start; if let Some((start, _)) = self.buffer[..end].char_indices().next_back() { diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 4b59ccab..96a384f7 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -278,7 +278,7 @@ impl ClusterPath { } /// Returns the path of the cluster that follows this one in visual order. - pub fn next_visual<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + pub fn next_visual(&self, layout: &Layout) -> Option { let line = self.line(layout)?; let run = line.run(self.run_index())?; let visual_index = run.logical_to_visual(self.logical_index())?; @@ -322,7 +322,7 @@ impl ClusterPath { } /// Returns the path of the cluster that follows this one in logical order. - pub fn next_logical<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + pub fn next_logical(&self, layout: &Layout) -> Option { let line = self.line(layout)?; let run = line.run(self.run_index())?; if self.logical_index() + 1 < run.cluster_range().len() { @@ -365,7 +365,7 @@ impl ClusterPath { } /// Returns the path of the cluster that precedes this one in visual order. - pub fn previous_visual<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + pub fn previous_visual(&self, layout: &Layout) -> Option { let line = self.line(layout)?; let run = line.run(self.run_index())?; let visual_index = run.logical_to_visual(self.logical_index())?; @@ -415,7 +415,7 @@ impl ClusterPath { /// Returns the path of the cluster that precedes this one in logical /// order. - pub fn previous_logical<'a, B: Brush>(&self, layout: &'a Layout) -> Option { + pub fn previous_logical(&self, layout: &Layout) -> Option { if self.logical_index > 0 { // Easy mode: previous cluster is in the same run Some(Self { @@ -457,10 +457,6 @@ impl ClusterPath { .and_then(|path| Some((path, path.cluster(layout)?))) } - pub fn move_lines(&self, layout: &Layout, delta: i32) -> Self { - *self - } - pub fn next_word(&self, layout: &Layout) -> Option { let line_start = self.line_index(); let mut run_start = self.run_index(); diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index 4007ad21..f5610871 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -239,32 +239,30 @@ impl Cursor { // We're moving right so we want to track right-side affinity; // let's swap. Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { - let next_rtl = next_cluster.is_rtl(); - // Check for directional boundary condition - if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { - if next_next_cluster.is_rtl() != next_rtl { - println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); - if let Some(prefer_rtl) = prefer_rtl { - if next_rtl != prefer_rtl { - return Self::from_cluster_path(layout, next_next, self.affinity); - } else { - return Self::from_cluster_path(layout, next, self.affinity); - } + } else if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { + let next_rtl = next_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { + if next_next_cluster.is_rtl() != next_rtl { + println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if next_rtl != prefer_rtl { + return Self::from_cluster_path(layout, next_next, self.affinity); + } else { + return Self::from_cluster_path(layout, next, self.affinity); } } } - let affinity = if self.is_rtl != next_rtl { - // println!("MOVING INTO BIDI BOUNDARY"); - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, next, affinity) - } else { - *self } + let affinity = if self.is_rtl != next_rtl { + // println!("MOVING INTO BIDI BOUNDARY"); + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, next, affinity) + } else { + *self } } @@ -289,31 +287,29 @@ impl Cursor { // We're moving left so we want to track left-side affinity; // let's swap Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { - let prev_rtl = prev_cluster.is_rtl(); - // Check for directional boundary condition - if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { - if prev_prev_cluster.is_rtl() != prev_rtl { - println!("MOVING LEFT INTO BIDI BOUNDARY 2"); - if let Some(prefer_rtl) = prefer_rtl { - if prev_rtl != prefer_rtl { - return Self::from_cluster_path(layout, prev_prev, self.affinity); - } else { - return Self::from_cluster_path(layout, prev, self.affinity); - } + } else if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { + let prev_rtl = prev_cluster.is_rtl(); + // Check for directional boundary condition + if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { + if prev_prev_cluster.is_rtl() != prev_rtl { + println!("MOVING LEFT INTO BIDI BOUNDARY 2"); + if let Some(prefer_rtl) = prefer_rtl { + if prev_rtl != prefer_rtl { + return Self::from_cluster_path(layout, prev_prev, self.affinity); + } else { + return Self::from_cluster_path(layout, prev, self.affinity); } } } - let affinity = if self.is_rtl != prev_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, prev, affinity) - } else { - *self } + let affinity = if self.is_rtl != prev_rtl { + self.affinity.invert() + } else { + self.affinity + }; + Self::from_cluster_path(layout, prev, affinity) + } else { + *self } } @@ -659,7 +655,7 @@ impl Selection { let line_start_ix = start.path.line_index(); let line_end_ix = end.path.line_index(); for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { + let Some(line) = layout.get(line_ix) else { continue; }; let metrics = line.metrics(); diff --git a/parley/src/layout/cursor2.rs b/parley/src/layout/cursor2.rs deleted file mode 100644 index 691d44e7..00000000 --- a/parley/src/layout/cursor2.rs +++ /dev/null @@ -1,527 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Text selection support. - -use peniko::kurbo::Rect; - -use super::{Affinity, Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; -use core::ops::Range; - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub enum CursorMode { - #[default] - Strong, - Weak, -} - -fn resolve_cursor_mode( - layout: &Layout, - path: ClusterPath, - mode: Option, -) -> CursorMode { - mode.unwrap_or_else(|| { - let layout_rtl = layout.data.base_level & 1 != 0; - let cluster_rtl = path - .cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(); - if layout_rtl == cluster_rtl { - CursorMode::Strong - } else { - CursorMode::Weak - } - }) -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Cursor { - path: ClusterPath, - index: u32, - text_start: u32, - text_end: u32, - visual_offset: f32, - mode: CursorMode, - is_rtl: bool, - affinity: Affinity, -} - -impl Cursor { - pub fn from_point( - layout: &Layout, - mode: Option, - x: f32, - y: f32, - ) -> Self { - let (path, affinity) = ClusterPath::from_point(layout, x, y); - Self::from_cluster_path( - layout, - path, - resolve_cursor_mode(layout, path, mode), - affinity, - ) - } - - pub fn from_byte_index( - layout: &Layout, - mode: Option, - byte_index: usize, - affinity: Affinity, - ) -> Self { - let path = ClusterPath::from_byte_index(layout, byte_index); - Self::from_cluster_path( - layout, - path, - resolve_cursor_mode(layout, path, mode), - affinity, - ) - } - - fn from_cluster_path( - layout: &Layout, - path: ClusterPath, - mode: CursorMode, - mut affinity: Affinity, - ) -> Self { - let (index, text_start, text_end, visual_offset, is_rtl, affinity) = - if let Some(cluster) = path.cluster(layout) { - let mut range = cluster.text_range(); - let index = range.start as u32; - let mut offset = path.visual_offset(layout).unwrap_or_default(); - let layout_is_rtl = layout.data.base_level & 1 != 0; - let is_rtl = cluster.is_rtl(); - let is_left_side = affinity.is_visually_leading(is_rtl); - if !is_left_side { - offset += cluster.advance(); - if !is_rtl { - range = next_logical_range(layout, path).unwrap_or(range.end..range.end); - } - } else if is_rtl { - range = next_logical_range(layout, path).unwrap_or(range.end..range.end); - } - ( - index, - range.start as u32, - range.end as u32, - offset, - cluster.is_rtl(), - affinity, - ) - } else { - Default::default() - }; - Self { - path, - index, - text_start, - text_end, - visual_offset, - mode, - is_rtl, - affinity, - } - } - - #[must_use] - fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, Some(self.mode), self.index as usize, self.affinity) - } - - /// Returns the path to the target cluster. - pub fn cluster_path(&self) -> ClusterPath { - self.path - } - - /// Returns the text range of the target cluster. - pub fn text_range(&self) -> Range { - self.text_start as usize..self.text_end as usize - } - - /// Returns the visual offset of the target cluster along the direction of - /// text flow. - pub fn visual_offset(&self) -> f32 { - self.visual_offset - } - - /// Returns the byte index associated with the cursor. - pub fn index(&self) -> usize { - self.index as usize - } - - pub fn affinity(&self) -> Affinity { - self.affinity - } - - #[must_use] - pub fn swap_mode(&self, layout: &Layout) -> Self { - let new_mode = match self.mode { - CursorMode::Strong => CursorMode::Weak, - CursorMode::Weak => CursorMode::Strong, - }; - Self::from_cluster_path(layout, self.path, new_mode, self.affinity) - } - - pub fn geometry(&self, layout: &Layout, width: f32) -> Option { - let metrics = *self.path.line(layout)?.metrics(); - let line_x = self.visual_offset as f64; - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + width as f64, - metrics.max_coord as f64, - )) - } - - pub fn next_visual(&self, layout: &Layout) -> Self { - if self.affinity.is_visually_leading(self.is_rtl) { - // We're moving right so we want to track right-side affinity; - // let's swap - Self::from_byte_index( - layout, - Some(self.mode), - self.index as usize, - self.affinity.invert(), - ) - } else { - if let Some(path) = self.path.next_visual(layout) { - let next_rtl = path - .cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(); - let affinity = if self.is_rtl != next_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, path, self.mode, affinity) - } else { - *self - } - } - } - - pub fn previous_visual(&self, layout: &Layout) -> Self { - if !self.affinity.is_visually_leading(self.is_rtl) { - // We're moving left so we want to track left-side affinity; - // let's swap - Self::from_byte_index( - layout, - Some(self.mode), - self.index as usize, - self.affinity.invert(), - ) - } else { - if let Some(path) = self.path.previous_visual(layout) { - let next_rtl = path - .cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(); - let affinity = if self.is_rtl != next_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, path, self.mode, affinity) - } else { - *self - } - } - } -} - -fn next_logical_range(layout: &Layout, path: ClusterPath) -> Option> { - Some(path.next_logical(layout)?.cluster(layout)?.text_range()) -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Selection { - anchor: Cursor, - focus: Cursor, - h_pos: Option, -} - -impl From for Selection { - fn from(value: Cursor) -> Self { - Self { - anchor: value, - focus: value, - h_pos: None, - } - } -} - -impl Selection { - pub fn from_point( - layout: &Layout, - mode: Option, - x: f32, - y: f32, - ) -> Self { - Cursor::from_point(layout, mode, x, y).into() - } - - pub fn from_byte_index( - layout: &Layout, - mode: Option, - index: usize, - affinity: Affinity, - ) -> Self { - Cursor::from_byte_index(layout, mode, index, affinity).into() - } - - pub fn anchor(&self) -> &Cursor { - &self.anchor - } - - pub fn focus(&self) -> &Cursor { - &self.focus - } - - pub fn is_collapsed(&self) -> bool { - self.anchor.text_start == self.focus.text_start - } - - pub fn text_range(&self) -> Range { - if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start as usize..self.focus.text_start as usize - } else { - self.focus.text_start as usize..self.anchor.text_start as usize - } - } - - /// Returns the index where text should be inserted based on this - /// selection. - pub fn insertion_index(&self) -> usize { - self.focus.text_start as usize - } - - #[must_use] - pub fn collapse(&self) -> Self { - Self { - anchor: self.focus, - focus: self.focus, - h_pos: self.h_pos, - } - } - - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = self.anchor.refresh(layout); - let focus = self.focus.refresh(layout); - Self { - anchor, - focus, - h_pos: None, - } - } - - #[must_use] - pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { - let focus = Cursor::from_point(layout, Some(self.focus.mode), x, y); - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } - - // #[must_use] - // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_end as usize), - // extend, - // ) - // } - - // #[must_use] - // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), - // extend, - // ) - // } - - #[must_use] - pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.next_visual(layout), extend) - } - - #[must_use] - pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.previous_visual(layout), extend) - } - - fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { - if extend { - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } else { - focus.into() - } - } - - #[must_use] - pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_byte_index( - layout, - Some(self.focus.mode), - line.text_range().start, - Affinity::Downstream, - ), - extend, - ) - } else { - *self - } - } - - #[must_use] - pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_byte_index( - layout, - Some(self.focus.mode), - line.text_range().end.saturating_sub(1), - Affinity::Upstream, - ), - extend, - ) - } else { - *self - } - } - - #[must_use] - pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) - } - - #[must_use] - pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) - } - - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { - // let line_index = self - // .focus - // .placement - // .primary_position() - // .line_index - // .saturating_add_signed(line_delta); - let line_index = self - .focus - .path - .line_index() - .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; - let y = line.metrics().baseline - line.metrics().ascent * 0.5; - let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); - let new_focus = Cursor::from_point(layout, Some(self.focus.mode), h_pos, y); - let h_pos = Some(h_pos); - Some(if extend { - Self { - anchor: self.anchor, - focus: new_focus, - h_pos, - } - } else { - Self { - anchor: new_focus, - focus: new_focus, - h_pos, - } - }) - } - - pub fn visual_focus(&self, layout: &Layout) -> Option { - self.focus.geometry(layout, 1.5) - } - - // pub fn visual_alternate_focus( - // &self, - // layout: &Layout, - // ) -> Option { - // visual_for_cursor(layout, self.focus.placement.alternate_position()) - // } - - // pub fn visual_anchor(&self, layout: &Layout) -> Option { - // self.anchor.path.visual_line(layout).map(|line| { - // let metrics = line.metrics(); - // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - // let line_max = line_min + metrics.line_height as f64; - // let line_x = self.anchor.offset as f64; - // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) - // }) - // } - - pub fn geometry(&self, layout: &Layout) -> Vec { - let mut rects = Vec::new(); - self.geometry_with(layout, |rect| rects.push(rect)); - rects - } - - pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { - // Ensure we add some visual indicator for selected empty - // lines. - const MIN_RECT_WIDTH: f64 = 4.0; - if self.is_collapsed() { - return; - } - let mut start = self.anchor; - let mut end = self.focus; - if start.text_start > end.text_start { - core::mem::swap(&mut start, &mut end); - } - let text_range = start.text_start..end.text_start; - let line_start_ix = start.path.line_index(); - let line_end_ix = end.path.line_index(); - for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { - continue; - }; - let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; - if line_ix == line_start_ix || line_ix == line_end_ix { - // We only need to run the expensive logic on the first and - // last lines - let mut start_x = metrics.offset as f64; - let mut cur_x = start_x; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&(cluster.text_range().start as u32)) { - cur_x += advance; - } else { - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x as _, line_min, start_x + width, line_max)); - } - cur_x += advance; - start_x = cur_x; - } - } - } - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x, line_min, start_x + width, line_max)); - } - } else { - let x = metrics.offset as f64; - let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); - f(Rect::new(x, line_min, x + width, line_max)); - } - } - } -} diff --git a/parley/src/layout/cursor3.rs b/parley/src/layout/cursor3.rs deleted file mode 100644 index 6e6691c9..00000000 --- a/parley/src/layout/cursor3.rs +++ /dev/null @@ -1,641 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Text selection support. - -use peniko::kurbo::Rect; - -use super::{Affinity, Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; -use core::ops::Range; - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub enum CursorMode { - #[default] - Strong, - Weak, -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Cursor { - pub index: CursorIndex, - text_start: u32, - text_end: u32, -} - -impl Cursor { - pub fn from_point( - layout: &Layout, - mode: Option, - x: f32, - y: f32, - ) -> Self { - let (path, affinity) = ClusterPath::from_point(layout, x, y); - if let Some(cluster) = path.cluster(layout) { - let index = if affinity.is_visually_leading(cluster.is_rtl()) { - cluster.text_range().start - } else { - // path.next_logical(layout).and_then(|p| p.cluster(layout)).and_then(|c| c.text_range().start)) - cluster.text_range().end - }; - Self::from_byte_index(layout, mode, index, Affinity::Downstream) - } else { - Self::default() - } - } - - pub fn from_byte_index( - layout: &Layout, - mode: Option, - index: usize, - affinity: Affinity, - ) -> Self { - Self::from_cursor_index(layout, CursorIndex::new(layout, index, affinity)) - } - - fn from_cursor_index(layout: &Layout, index: CursorIndex) -> Self { - let range = index.text_range(layout); - Self { - index, - text_start: range.start as u32, - text_end: range.end as u32, - } - } - - pub fn index(&self) -> usize { - self.index.index as usize - } - - pub fn affinity(&self) -> Affinity { - self.index.affinity - } - - pub fn text_range(&self) -> Range { - self.text_start as usize..self.text_end as usize - } - - #[must_use] - fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, None, self.index.index as usize, self.index.affinity) - } - - pub fn geometry(&self, layout: &Layout, size: f32) -> Option { - self.index.geometry(layout, size) - } - - pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { - self.index.weak_geometry(layout, size) - } - - pub fn next_visual(&self, layout: &Layout) -> Self { - self.index - .next_visual(layout) - .map(|ix| Self::from_cursor_index(layout, ix)) - .unwrap_or(*self) - } - - pub fn previous_visual(&self, layout: &Layout) -> Self { - self.index - .previous_visual(layout) - .map(|ix| Self::from_cursor_index(layout, ix)) - .unwrap_or(*self) - } -} - -fn next_logical_range(layout: &Layout, path: ClusterPath) -> Option> { - Some(path.next_logical(layout)?.cluster(layout)?.text_range()) -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub enum ClusterPartition { - /// No partition. - #[default] - None, - /// Text direction changes between two clusters. - TextDirection, - /// Soft line break between two clusters. - SoftLine, - /// Hard line break between two clusters. - HardLine, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct CursorIndex { - pub index: u32, - pub affinity: Affinity, - pub kind: CursorIndexKind, -} - -impl Default for CursorIndex { - fn default() -> Self { - Self { - index: 0, - affinity: Default::default(), - kind: CursorIndexKind::Start(Default::default()), - } - } -} - -impl CursorIndex { - pub fn new(layout: &Layout, index: usize, affinity: Affinity) -> Self { - let index = index.min(layout.data.text_len); - let kind = CursorIndexKind::new(layout, index); - Self { - index: index as u32, - affinity, - kind, - } - } - - pub fn next_visual(&self, layout: &Layout) -> Option { - match self.kind { - CursorIndexKind::Start(path) => { - let next = path.next_visual(layout)?; - let index = next.cluster(layout)?.text_range().start; - Some(Self::new(layout, index, self.affinity)) - } - CursorIndexKind::Between(prev, next, _partition) => { - let cluster = prev.cluster(layout)?; - let new = prev.next_visual(layout)?.next_visual(layout)?; - let new_cluster = new.cluster(layout)?; - let range = new_cluster.text_range(); - let index = if new_cluster.is_rtl() { - range.end - } else { - range.start - }; - let affinity = if cluster.is_rtl() != new_cluster.is_rtl() { - self.affinity.invert() - } else { - self.affinity - }; - Some(Self::new(layout, index, affinity)) - } - CursorIndexKind::End(_) => None, - } - } - - pub fn previous_visual(&self, layout: &Layout) -> Option { - match self.kind { - CursorIndexKind::Start(_) => None, - CursorIndexKind::Between(prev, _next, _partition) => { - let cluster = prev.cluster(layout)?; - let new = prev.previous_visual(layout)?; - let new_cluster = new.cluster(layout)?; - let range = new_cluster.text_range(); - let index = if new_cluster.is_rtl() { - range.end - } else { - range.start - }; - let affinity = if cluster.is_rtl() != new_cluster.is_rtl() { - self.affinity.invert() - } else { - self.affinity - }; - Some(Self::new(layout, index, affinity)) - } - CursorIndexKind::End(path) => { - let prev = path.previous_visual(layout)?; - let index = prev.cluster(layout)?.text_range().end; - Some(Self::new(layout, index, self.affinity)) - } - } - } - - pub fn line_index(&self) -> usize { - match self.kind { - CursorIndexKind::Start(path) | CursorIndexKind::End(path) => path.line_index(), - CursorIndexKind::Between(prev, next, _partition) => { - let path = match self.affinity { - Affinity::Upstream => prev, - Affinity::Downstream => next, - }; - path.line_index() - } - } - } - - pub fn text_range(&self, layout: &Layout) -> Range { - match self.kind { - CursorIndexKind::Start(path) => path - .cluster(layout) - .map(|c| c.text_range()) - .unwrap_or_default(), - CursorIndexKind::Between(prev, next, _partition) => { - let path = match self.affinity { - Affinity::Downstream => prev, - Affinity::Upstream => next, - }; - path.cluster(layout) - .map(|c| c.text_range()) - .unwrap_or_default() - } - CursorIndexKind::End(_path) => layout.data.text_len..layout.data.text_len, - } - } - - pub fn geometry(&self, layout: &Layout, size: f32) -> Option { - let (line_index, offset) = match self.kind { - CursorIndexKind::Start(path) => { - let cluster = path.cluster(layout)?; - let line_index = path.line_index(); - let mut offset = path.visual_offset(layout)?; - if cluster.is_rtl() { - offset += cluster.advance(); - } - (line_index, offset) - } - CursorIndexKind::Between(path, _next, _partition) => { - let cluster = path.cluster(layout)?; - let line_index = path.line_index(); - let mut offset = path.visual_offset(layout)?; - if self.affinity.is_visually_leading(cluster.is_rtl()) { - offset += cluster.advance(); - } - (line_index, offset) - } - CursorIndexKind::End(path) => { - let cluster = path.cluster(layout)?; - let line_index = path.line_index(); - let mut offset = path.visual_offset(layout)?; - if !cluster.is_rtl() { - offset += cluster.advance(); - } - (line_index, offset) - } - }; - let line = layout.get(line_index)?; - let metrics = line.metrics(); - Some(Rect::new( - offset as f64, - metrics.min_coord as f64, - offset as f64 + size as f64, - metrics.max_coord as f64, - )) - } - - pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { - match self.kind { - CursorIndexKind::Start(_) - | CursorIndexKind::End(_) - | CursorIndexKind::Between( - _, - _, - ClusterPartition::None | ClusterPartition::SoftLine | ClusterPartition::HardLine, - ) => None, - CursorIndexKind::Between(prev, next, ClusterPartition::TextDirection) => { - let path = match self.affinity { - Affinity::Downstream => next, - Affinity::Upstream => prev, - }; - let cluster = path.cluster(layout)?; - let line_index = path.line_index(); - let mut offset = path.visual_offset(layout)? + cluster.advance(); - // if cluster.is_rtl() { - // offset += cluster.advance(); - // } - let line = layout.get(line_index)?; - let metrics = line.metrics(); - Some(Rect::new( - offset as f64, - metrics.min_coord as f64, - offset as f64 + size as f64, - metrics.max_coord as f64, - )) - } - } - } -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum CursorIndexKind { - /// Index is at the start of the text. - Start(ClusterPath), - /// Index is between two clusters in logical order. - Between(ClusterPath, ClusterPath, ClusterPartition), - /// Index is at the end of the text. - End(ClusterPath), -} - -impl CursorIndexKind { - pub fn new(layout: &Layout, index: usize) -> Self { - let path = ClusterPath::from_byte_index(layout, index); - if index >= layout.data.text_len { - Self::End(path) - } else if let Some(prev_path) = path.previous_logical(layout) { - let partition = if let Some((cluster, prev_cluster)) = - path.cluster(layout).zip(prev_path.cluster(layout)) - { - if prev_path.line_index() != path.line_index() { - if prev_cluster.is_hard_line_break() { - ClusterPartition::HardLine - } else { - ClusterPartition::SoftLine - } - } else if cluster.is_rtl() != prev_cluster.is_rtl() { - ClusterPartition::TextDirection - } else { - ClusterPartition::None - } - } else { - ClusterPartition::None - }; - Self::Between(prev_path, path, partition) - } else { - Self::Start(path) - } - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Selection { - anchor: Cursor, - focus: Cursor, - h_pos: Option, -} - -impl From for Selection { - fn from(value: Cursor) -> Self { - Self { - anchor: value, - focus: value, - h_pos: None, - } - } -} - -impl Selection { - pub fn from_point( - layout: &Layout, - mode: Option, - x: f32, - y: f32, - ) -> Self { - Cursor::from_point(layout, mode, x, y).into() - } - - pub fn from_byte_index( - layout: &Layout, - mode: Option, - index: usize, - affinity: Affinity, - ) -> Self { - Cursor::from_byte_index(layout, mode, index, affinity).into() - } - - pub fn anchor(&self) -> &Cursor { - &self.anchor - } - - pub fn focus(&self) -> &Cursor { - &self.focus - } - - pub fn is_collapsed(&self) -> bool { - self.anchor.text_start == self.focus.text_start - } - - pub fn text_range(&self) -> Range { - if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start as usize..self.focus.text_start as usize - } else { - self.focus.text_start as usize..self.anchor.text_start as usize - } - } - - /// Returns the index where text should be inserted based on this - /// selection. - pub fn insertion_index(&self) -> usize { - self.focus.text_start as usize - } - - #[must_use] - pub fn collapse(&self) -> Self { - Self { - anchor: self.focus, - focus: self.focus, - h_pos: self.h_pos, - } - } - - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = self.anchor.refresh(layout); - let focus = self.focus.refresh(layout); - Self { - anchor, - focus, - h_pos: None, - } - } - - #[must_use] - pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { - let focus = Cursor::from_point(layout, None, x, y); - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } - - // #[must_use] - // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_end as usize), - // extend, - // ) - // } - - // #[must_use] - // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), - // extend, - // ) - // } - - #[must_use] - pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.next_visual(layout), extend) - } - - #[must_use] - pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.previous_visual(layout), extend) - } - - fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { - if extend { - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } else { - focus.into() - } - } - - #[must_use] - pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - // if let Some(line) = self.focus.path.line(layout) { - // self.maybe_extend( - // Cursor::from_byte_index( - // layout, - // Some(self.focus.mode), - // line.text_range().start, - // Affinity::Downstream, - // ), - // extend, - // ) - // } else { - // *self - // } - *self - } - - #[must_use] - pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - // if let Some(line) = self.focus.path.line(layout) { - // self.maybe_extend( - // Cursor::from_byte_index( - // layout, - // Some(self.focus.mode), - // line.text_range().end.saturating_sub(1), - // Affinity::Upstream, - // ), - // extend, - // ) - // } else { - // *self - // } - *self - } - - #[must_use] - pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) - } - - #[must_use] - pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) - } - - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { - // let line_index = self - // .focus - // .placement - // .primary_position() - // .line_index - // .saturating_add_signed(line_delta); - - // let line_index = self - // .focus - // .path - // .line_index() - // .saturating_add_signed(line_delta); - // let line = layout.get(line_index)?; - // let y = line.metrics().baseline - line.metrics().ascent * 0.5; - // let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); - // let new_focus = Cursor::from_point(layout, Some(self.focus.mode), h_pos, y); - // let h_pos = Some(h_pos); - // Some(if extend { - // Self { - // anchor: self.anchor, - // focus: new_focus, - // h_pos, - // } - // } else { - // Self { - // anchor: new_focus, - // focus: new_focus, - // h_pos, - // } - // }) - None - } - - // pub fn visual_alternate_focus( - // &self, - // layout: &Layout, - // ) -> Option { - // visual_for_cursor(layout, self.focus.placement.alternate_position()) - // } - - // pub fn visual_anchor(&self, layout: &Layout) -> Option { - // self.anchor.path.visual_line(layout).map(|line| { - // let metrics = line.metrics(); - // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - // let line_max = line_min + metrics.line_height as f64; - // let line_x = self.anchor.offset as f64; - // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) - // }) - // } - - pub fn geometry(&self, layout: &Layout) -> Vec { - let mut rects = Vec::new(); - self.geometry_with(layout, |rect| rects.push(rect)); - rects - } - - pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { - // Ensure we add some visual indicator for selected empty - // lines. - const MIN_RECT_WIDTH: f64 = 4.0; - if self.is_collapsed() { - return; - } - let mut start = self.anchor; - let mut end = self.focus; - if start.text_start > end.text_start { - core::mem::swap(&mut start, &mut end); - } - let text_range = start.text_start..end.text_start; - let line_start_ix = start.index.line_index(); - let line_end_ix = end.index.line_index(); - for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { - continue; - }; - let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; - if line_ix == line_start_ix || line_ix == line_end_ix { - // We only need to run the expensive logic on the first and - // last lines - let mut start_x = metrics.offset as f64; - let mut cur_x = start_x; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&(cluster.text_range().start as u32)) { - cur_x += advance; - } else { - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x as _, line_min, start_x + width, line_max)); - } - cur_x += advance; - start_x = cur_x; - } - } - } - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x, line_min, start_x + width, line_max)); - } - } else { - let x = metrics.offset as f64; - let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); - f(Rect::new(x, line_min, x + width, line_max)); - } - } - } -} diff --git a/parley/src/layout/select.rs b/parley/src/layout/select.rs deleted file mode 100644 index 61cd4501..00000000 --- a/parley/src/layout/select.rs +++ /dev/null @@ -1,700 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Text selection support. - -use peniko::kurbo::Rect; - -use super::{Affinity, Brush, ClusterPath, Layout}; -use core::ops::Range; - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub enum VisualCursorMode { - /// During cursor motion, affinity is adjusted to prioritize the dominant - /// direction of the layout. - /// - /// That is, if the base direction of the layout is left-to-right, then - /// the visual cursor will represent the position where the next - /// left-to-right character would be inserted, and vice versa. - /// - /// This matches the behavior of Pango's strong cursor. - #[default] - Strong, - /// During cursor motion, affinity is adjusted to prioritize the non-dominant - /// direction of the layout. - /// - /// That is, if the base direction of the layout is left-to-right, then - /// the visual cursor will represent the position where the next - /// right-to-left character would be inserted, and vice versa. - /// - /// This matches the behavior of Pango's weak cursor. - Weak, - /// During cursor motion, affinity is adjusted based on the directionality - /// of the incoming position. - /// - /// That is, if a directional boundary is entered from a left-to-right run - /// of text, then the cursor will represent the position where the next - /// left-to-right character would be inserted, and vice versa. - /// - /// This matches the behavior of Firefox. - Adaptive, -} - -impl VisualCursorMode { - /// Returns the preferred RTL state for the given layout. - /// - /// This is used to handle cursor modes when moving visually - /// by cluster. - fn prefer_rtl(self, layout: &Layout) -> Option { - match self { - Self::Strong => Some(layout.is_rtl()), - Self::Weak => Some(!layout.is_rtl()), - Self::Adaptive => None, - } - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Cursor { - path: ClusterPath, - index: u32, - text_start: u32, - text_end: u32, - visual_offset: f32, - is_rtl: bool, - affinity: Affinity, -} - -impl Cursor { - /// Creates a new cursor for the given layout and point. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (path, affinity) = ClusterPath::from_point(layout, x, y); - Self::from_cluster_path(layout, path, affinity) - } - - /// Returns a new cursor for the given layout, byte index and affinity. - pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - let path = ClusterPath::from_byte_index(layout, index); - Self::from_cluster_path(layout, path, affinity) - } - - fn from_cluster_path( - layout: &Layout, - path: ClusterPath, - affinity: Affinity, - ) -> Self { - let (index, text_start, text_end, visual_offset, is_rtl) = - if let Some(cluster) = path.cluster(layout) { - let mut range = cluster.text_range(); - let index = range.start as u32; - let mut offset = path.visual_offset(layout).unwrap_or_default(); - let is_rtl = cluster.is_rtl(); - let is_left_side = affinity.is_visually_leading(is_rtl); - if !is_left_side { - offset += cluster.advance(); - if !is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - } else if is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - ( - index, - range.start as u32, - range.end as u32, - offset, - cluster.is_rtl(), - ) - } else { - Default::default() - }; - Self { - path, - index, - text_start, - text_end, - visual_offset, - is_rtl, - affinity, - } - } - - /// Returns a new cursor with internal state recomputed to match the given - /// layout. - /// - /// This should be called whenever the layout is rebuilt or resized. - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - Self::from_index(layout, self.index as usize, self.affinity) - } - - /// Returns the path to the target cluster. - pub fn cluster_path(&self) -> ClusterPath { - self.path - } - - /// Returns the text range of the target cluster. - pub fn text_range(&self) -> Range { - self.text_start as usize..self.text_end as usize - } - - /// Returns the visual offset of the target cluster along the direction of - /// text flow. - pub fn visual_offset(&self) -> f32 { - self.visual_offset - } - - /// Returns the byte index associated with the cursor. - pub fn index(&self) -> usize { - self.index as usize - } - - /// Returns the associated affinity for this cursor. - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Returns the visual geometry of the cursor where the next character - /// matching the base direction of the layout would be inserted. - /// - /// If the current cursor is not on a directional boundary, this is also - /// the location where characters opposite the base direction would be - /// inserted. - pub fn strong_geometry(&self, layout: &Layout, size: f32) -> Option { - if self.is_rtl == layout.is_rtl() { - self.geometry(layout, size) - } else { - self.bidi_link_geometry(layout, size) - .or_else(|| self.geometry(layout, size)) - } - } - - /// Returns the visual geometry of the cursor where the next character - /// that is opposite the base direction of the layout would be inserted. - /// - /// This returns `None` when the current cursor is not on a directional - /// boundary. - pub fn weak_geometry(&self, layout: &Layout, size: f32) -> Option { - // Weak cursor only exists if we're on a directional boundary - let bidi_link = self.bidi_link_geometry(layout, size)?; - if self.is_rtl == layout.is_rtl() { - Some(bidi_link) - } else { - self.geometry(layout, size) - } - } - - fn geometry(&self, layout: &Layout, size: f32) -> Option { - let metrics = *self.path.line(layout)?.metrics(); - let line_x = self.visual_offset as f64; - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + size as f64, - metrics.max_coord as f64, - )) - } - - fn bidi_link_geometry(&self, layout: &Layout, size: f32) -> Option { - let (path, cluster) = self.path.bidi_link_cluster(layout, self.affinity)?; - let mut line_x = path.visual_offset(layout)? as f64; - let run = path.run(layout)?; - if run.logical_to_visual(path.logical_index())? != 0 { - line_x += cluster.advance() as f64; - } - let metrics = *path.line(layout)?.metrics(); - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + size as f64, - metrics.max_coord as f64, - )) - } - - pub fn next_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { - let prefer_rtl = mode.prefer_rtl(layout); - if self.affinity.is_visually_leading(self.is_rtl) { - // Check for directional boundary condition - if let Some((next_path, next_cluster)) = self.path.next_visual_cluster(layout) { - if next_cluster.is_rtl() != self.is_rtl { - println!("MOVING RIGHT INTO BIDI BOUNDARY"); - if let Some(prefer_rtl) = prefer_rtl { - if self.is_rtl != prefer_rtl { - return Self::from_cluster_path( - layout, - next_path, - self.affinity.invert(), - ); - } - } - } - } - // We're moving right so we want to track right-side affinity; - // let's swap. - Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { - let next_rtl = next_cluster.is_rtl(); - // Check for directional boundary condition - if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { - if next_next_cluster.is_rtl() != next_rtl { - println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); - if let Some(prefer_rtl) = prefer_rtl { - if next_rtl != prefer_rtl { - return Self::from_cluster_path(layout, next_next, self.affinity); - } else { - return Self::from_cluster_path(layout, next, self.affinity); - } - } - } - } - let affinity = if self.is_rtl != next_rtl { - // println!("MOVING INTO BIDI BOUNDARY"); - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, next, affinity) - } else { - *self - } - } - } - - pub fn previous_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { - let prefer_rtl = mode.prefer_rtl(layout); - if !self.affinity.is_visually_leading(self.is_rtl) { - // Check for directional boundary condition - if let Some((prev_path, prev_cluster)) = self.path.previous_visual_cluster(layout) { - if prev_cluster.is_rtl() != self.is_rtl { - println!("MOVING LEFT INTO BIDI BOUNDARY"); - if let Some(prefer_rtl) = prefer_rtl { - if self.is_rtl != prefer_rtl { - return Self::from_cluster_path( - layout, - prev_path, - self.affinity.invert(), - ); - } - } - } - } - // We're moving left so we want to track left-side affinity; - // let's swap - Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { - let prev_rtl = prev_cluster.is_rtl(); - // Check for directional boundary condition - if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { - if prev_prev_cluster.is_rtl() != prev_rtl { - println!("MOVING LEFT INTO BIDI BOUNDARY 2"); - if let Some(prefer_rtl) = prefer_rtl { - if prev_rtl != prefer_rtl { - return Self::from_cluster_path(layout, prev_prev, self.affinity); - } else { - return Self::from_cluster_path(layout, prev, self.affinity); - } - } - } - } - let affinity = if self.is_rtl != prev_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, prev, affinity) - } else { - *self - } - } - } - - pub fn next_word(&self, layout: &Layout) -> Self { - let mut next_path = if self.affinity == Affinity::Upstream { - self.path.next_logical(layout).unwrap_or(self.path) - } else { - self.path - }; - while let Some((path, cluster)) = next_path.next_word_cluster(layout) { - next_path = path; - if !cluster.is_space_or_nbsp() { - break; - } - } - Self::from_cluster_path(layout, next_path, Affinity::default()) - } - - pub fn previous_word(&self, layout: &Layout) -> Self { - let mut next_path = if self.affinity == Affinity::Upstream { - self.path.next_logical(layout).unwrap_or(self.path) - } else { - self.path - }; - // let mut next_path = self.path; - while let Some((path, cluster)) = next_path.previous_word_cluster(layout) { - next_path = path; - if !cluster.is_space_or_nbsp() { - break; - } - } - Self::from_cluster_path(layout, next_path, Affinity::default()) - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Selection { - anchor: Cursor, - focus: Cursor, - /// Current horizontal position. Used for tracking line movement. - h_pos: Option, -} - -impl From for Selection { - fn from(value: Cursor) -> Self { - Self { - anchor: value, - focus: value, - h_pos: None, - } - } -} - -impl Selection { - /// Creates a collapsed selection with the anchor and focus set to the - /// position associated with the given point. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - Cursor::from_point(layout, x, y).into() - } - - /// Creates a collapsed selection with the anchor and focus set to the - /// position associated with the given byte index and affinity. - pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - Cursor::from_index(layout, index, affinity).into() - } - - /// Creates a new selection bounding the word at the given coordinates. - pub fn word_from_point(layout: &Layout, x: f32, y: f32) -> Self { - let mut anchor = Cursor::from_point(layout, x, y); - if !(anchor.affinity == Affinity::Downstream - && anchor - .cluster_path() - .cluster(layout) - .map(|cluster| cluster.is_word_boundary()) - .unwrap_or_default()) - { - anchor = anchor.previous_word(layout); - } - let mut focus = anchor.next_word(layout); - if anchor.is_rtl { - core::mem::swap(&mut anchor, &mut focus); - } - Self { - anchor, - focus, - h_pos: None, - } - } - - /// Returns the anchor point of the selection. - /// - /// This represents the location where the selection was initiated. - pub fn anchor(&self) -> &Cursor { - &self.anchor - } - - /// Returns the focus point of the selection. - /// - /// This represents the current location of the selection. - pub fn focus(&self) -> &Cursor { - &self.focus - } - - /// Returns true when the anchor and focus are at the same position. - pub fn is_collapsed(&self) -> bool { - self.anchor.text_start == self.focus.text_start - } - - /// Returns the range of text bounded by this selection. - /// - /// This is equivalent to the text that would be removed when pressing the - /// delete key. - pub fn text_range(&self) -> Range { - if self.is_collapsed() { - self.focus.text_range() - } else if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start as usize..self.focus.text_start as usize - } else { - self.focus.text_start as usize..self.anchor.text_start as usize - } - } - - /// Returns the index where text should be inserted based on this - /// selection. - pub fn insertion_index(&self) -> usize { - self.focus.text_start as usize - } - - /// Returns a new collapsed selection at the position of the current - /// focus. - #[must_use] - pub fn collapse(&self) -> Self { - Self { - anchor: self.focus, - focus: self.focus, - h_pos: self.h_pos, - } - } - - /// Refreshes the internal cursor state to match the the given layout. - /// - /// This should be called whenever the layout is rebuilt or resized. - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = self.anchor.refresh(layout); - let focus = self.focus.refresh(layout); - Self { - anchor, - focus, - h_pos: None, - } - } - - /// Returns a new selection with the focus extended to the given point. - #[must_use] - pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { - let focus = Cursor::from_point(layout, x, y); - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } - - /// Returns a new selection with the focus moved to the next cluster in - /// visual order. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn next_visual( - &self, - layout: &Layout, - mode: VisualCursorMode, - extend: bool, - ) -> Self { - self.maybe_extend(self.focus.next_visual(layout, mode), extend) - } - - /// Returns a new selection with the focus moved to the previous cluster in - /// visual order. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn previous_visual( - &self, - layout: &Layout, - mode: VisualCursorMode, - extend: bool, - ) -> Self { - self.maybe_extend(self.focus.previous_visual(layout, mode), extend) - } - - /// Returns a new selection with the focus moved to the next word. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn next_word(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.next_word(layout), extend) - } - - /// Returns a new selection with the focus moved to the previous word. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn previous_word(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.previous_word(layout), extend) - } - - fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { - if extend { - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } else { - focus.into() - } - } - - /// Returns a new selection with the focus moved to the start of the - /// current line. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_index(layout, line.text_range().start, Affinity::Downstream), - extend, - ) - } else { - *self - } - } - - /// Returns a new selection with the focus moved to the end of the - /// current line. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_index( - layout, - line.text_range().end.saturating_sub(1), - Affinity::Upstream, - ), - extend, - ) - } else { - *self - } - } - - /// Returns a new selection with the focus moved to the next line. The - /// current horizontal position will be maintained. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) - } - - /// Returns a new selection with the focus moved to the previous line. The - /// current horizontal position will be maintained. - /// - /// If `extend` is `true` then the current anchor will be retained, - /// otherwise the new selection will be collapsed. - #[must_use] - pub fn previous_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) - } - - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { - let line_index = self - .focus - .path - .line_index() - .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; - let y = line.metrics().baseline - line.metrics().ascent * 0.5; - let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); - let new_focus = Cursor::from_point(layout, h_pos, y); - let h_pos = Some(h_pos); - Some(if extend { - Self { - anchor: self.anchor, - focus: new_focus, - h_pos, - } - } else { - Self { - anchor: new_focus, - focus: new_focus, - h_pos, - } - }) - } - - /// Returns a vector containing the rectangles which represent the visual - /// geometry of this selection for the given layout. - /// - /// This is a convenience method built on [`geometry_with`](Self::geometry_with). - pub fn geometry(&self, layout: &Layout) -> Vec { - let mut rects = Vec::new(); - self.geometry_with(layout, |rect| rects.push(rect)); - rects - } - - /// Invokes `f` with the sequence of rectangles which represent the visual - /// geometry of this selection for the given layout. - /// - /// This avoids allocation if the intent is to render the rectangles - /// immediately. - pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { - // Ensure we add some visual indicator for selected empty - // lines. - // Make this configurable? - const MIN_RECT_WIDTH: f64 = 4.0; - if self.is_collapsed() { - return; - } - let mut start = self.anchor; - let mut end = self.focus; - if start.text_start > end.text_start { - core::mem::swap(&mut start, &mut end); - } - let text_range = start.text_start..end.text_start; - let line_start_ix = start.path.line_index(); - let line_end_ix = end.path.line_index(); - for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { - continue; - }; - let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; - if line_ix == line_start_ix || line_ix == line_end_ix { - // We only need to run the expensive logic on the first and - // last lines - let mut start_x = metrics.offset as f64; - let mut cur_x = start_x; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&(cluster.text_range().start as u32)) { - cur_x += advance; - } else { - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x as _, line_min, start_x + width, line_max)); - } - cur_x += advance; - start_x = cur_x; - } - } - } - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x, line_min, start_x + width, line_max)); - } - } else { - let x = metrics.offset as f64; - let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); - f(Rect::new(x, line_min, x + width, line_max)); - } - } - } -} diff --git a/parley/src/layout/select2.rs b/parley/src/layout/select2.rs deleted file mode 100644 index 7e0007f1..00000000 --- a/parley/src/layout/select2.rs +++ /dev/null @@ -1,491 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Text selection support. - -use peniko::kurbo::Rect; - -use super::{Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; -use core::ops::Range; - -/// Determines how a cursor attaches to a cluster. -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] -pub enum Affinity { - /// Left side for LTR clusters and right side for RTL clusters. - #[default] - Downstream = 0, - /// Right side for LTR clusters and left side for RTL clusters. - Upstream = 1, -} - -impl Affinity { - pub fn from_rtl_and_side(is_rtl: bool, side: ClusterSide) -> Self { - match (is_rtl, side) { - // right edge of RTL and left edge of LTR - (true, ClusterSide::Trailing) | (false, ClusterSide::Leading) => Affinity::Downstream, - // left edge of RTL and right edge of LTR - (true, ClusterSide::Leading) | (false, ClusterSide::Trailing) => Affinity::Upstream, - } - } - - pub fn invert(&self) -> Self { - match self { - Self::Downstream => Self::Upstream, - Self::Upstream => Self::Downstream, - } - } - - /// Returns true if the cursor should be placed on the left side. - pub fn is_left_side(&self, is_rtl: bool) -> bool { - match (*self, is_rtl) { - (Self::Upstream, true) | (Self::Downstream, false) => true, - (Self::Upstream, false) | (Self::Downstream, true) => false, - } - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Cursor { - path: ClusterPath, - index: u32, - text_start: u32, - text_end: u32, - visual_offset: f32, - is_rtl: bool, - affinity: Affinity, -} - -impl Cursor { - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (path, side) = ClusterPath::from_point(layout, x, y); - let affinity = Affinity::from_rtl_and_side( - path.cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(), - side, - ); - Self::from_cluster_path(layout, path, affinity) - } - - pub fn from_byte_index( - layout: &Layout, - byte_index: usize, - affinity: Affinity, - ) -> Self { - let path = ClusterPath::from_byte_index(layout, byte_index); - Self::from_cluster_path(layout, path, affinity) - } - - fn from_cluster_path( - layout: &Layout, - path: ClusterPath, - affinity: Affinity, - ) -> Self { - let (index, text_start, text_end, visual_offset, is_rtl) = - if let Some(cluster) = path.cluster(layout) { - let mut range = cluster.text_range(); - let index = range.start as u32; - let mut offset = path.visual_offset(layout).unwrap_or_default(); - let is_rtl = cluster.is_rtl(); - let is_left_side = affinity.is_left_side(is_rtl); - if !is_left_side { - offset += cluster.advance(); - if !is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - } else if is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - ( - index, - range.start as u32, - range.end as u32, - offset, - cluster.is_rtl(), - ) - } else { - Default::default() - }; - Self { - path, - index, - text_start, - text_end, - visual_offset, - is_rtl, - affinity, - } - } - - #[must_use] - fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, self.index as usize, self.affinity) - } - - /// Returns the path to the target cluster. - pub fn cluster_path(&self) -> ClusterPath { - self.path - } - - /// Returns the text range of the target cluster. - pub fn text_range(&self) -> Range { - self.text_start as usize..self.text_end as usize - } - - /// Returns the visual offset of the target cluster along the direction of - /// text flow. - pub fn visual_offset(&self) -> f32 { - self.visual_offset - } - - /// Returns the byte index associated with the cursor. - pub fn index(&self) -> usize { - self.index as usize - } - - pub fn affinity(&self) -> Affinity { - self.affinity - } - - pub fn geometry(&self, layout: &Layout, width: f32) -> Option { - let metrics = *self.path.line(layout)?.metrics(); - let line_x = self.visual_offset as f64; - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + width as f64, - metrics.max_coord as f64, - )) - } - - pub fn next_visual(&self, layout: &Layout) -> Self { - if self.affinity.is_left_side(self.is_rtl) { - // We're moving right so we want to track right-side affinity; - // let's swap - Self::from_byte_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some(path) = self.path.next_visual(layout) { - let next_rtl = path - .cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(); - let affinity = if self.is_rtl != next_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, path, affinity) - } else { - *self - } - } - } - - pub fn previous_visual(&self, layout: &Layout) -> Self { - if !self.affinity.is_left_side(self.is_rtl) { - // We're moving left so we want to track left-side affinity; - // let's swap - Self::from_byte_index(layout, self.index as usize, self.affinity.invert()) - } else { - if let Some(path) = self.path.previous_visual(layout) { - let next_rtl = path - .cluster(layout) - .map(|cluster| cluster.is_rtl()) - .unwrap_or_default(); - let affinity = if self.is_rtl != next_rtl { - self.affinity.invert() - } else { - self.affinity - }; - Self::from_cluster_path(layout, path, affinity) - } else { - *self - } - } - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Selection { - anchor: Cursor, - focus: Cursor, - h_pos: Option, -} - -impl From for Selection { - fn from(value: Cursor) -> Self { - Self { - anchor: value, - focus: value, - h_pos: None, - } - } -} - -impl Selection { - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - Cursor::from_point(layout, x, y).into() - } - - pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - Cursor::from_byte_index(layout, index, affinity).into() - } - - pub fn anchor(&self) -> &Cursor { - &self.anchor - } - - pub fn focus(&self) -> &Cursor { - &self.focus - } - - pub fn is_collapsed(&self) -> bool { - self.anchor.text_start == self.focus.text_start - } - - pub fn text_range(&self) -> Range { - if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start as usize..self.focus.text_start as usize - } else { - self.focus.text_start as usize..self.anchor.text_start as usize - } - } - - /// Returns the index where text should be inserted based on this - /// selection. - pub fn insertion_index(&self) -> usize { - self.focus.text_start as usize - } - - #[must_use] - pub fn collapse(&self) -> Self { - Self { - anchor: self.focus, - focus: self.focus, - h_pos: self.h_pos, - } - } - - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = self.anchor.refresh(layout); - let focus = self.focus.refresh(layout); - Self { - anchor, - focus, - h_pos: None, - } - } - - #[must_use] - pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { - let focus = Cursor::from_point(layout, x, y); - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } - - // #[must_use] - // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_end as usize), - // extend, - // ) - // } - - // #[must_use] - // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), - // extend, - // ) - // } - - #[must_use] - pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.next_visual(layout), extend) - } - - #[must_use] - pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.previous_visual(layout), extend) - } - - fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { - if extend { - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } else { - focus.into() - } - } - - #[must_use] - pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_byte_index(layout, line.text_range().start, Affinity::Downstream), - extend, - ) - } else { - *self - } - } - - #[must_use] - pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - if let Some(line) = self.focus.path.line(layout) { - self.maybe_extend( - Cursor::from_byte_index( - layout, - line.text_range().end.saturating_sub(1), - Affinity::Upstream, - ), - extend, - ) - } else { - *self - } - } - - #[must_use] - pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) - } - - #[must_use] - pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) - } - - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { - // let line_index = self - // .focus - // .placement - // .primary_position() - // .line_index - // .saturating_add_signed(line_delta); - let line_index = self - .focus - .path - .line_index() - .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; - let y = line.metrics().baseline - line.metrics().ascent * 0.5; - let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); - let new_focus = Cursor::from_point(layout, h_pos, y); - let h_pos = Some(h_pos); - Some(if extend { - Self { - anchor: self.anchor, - focus: new_focus, - h_pos, - } - } else { - Self { - anchor: new_focus, - focus: new_focus, - h_pos, - } - }) - } - - pub fn visual_focus(&self, layout: &Layout) -> Option { - self.focus.geometry(layout, 1.5) - } - - // pub fn visual_alternate_focus( - // &self, - // layout: &Layout, - // ) -> Option { - // visual_for_cursor(layout, self.focus.placement.alternate_position()) - // } - - // pub fn visual_anchor(&self, layout: &Layout) -> Option { - // self.anchor.path.visual_line(layout).map(|line| { - // let metrics = line.metrics(); - // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - // let line_max = line_min + metrics.line_height as f64; - // let line_x = self.anchor.offset as f64; - // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) - // }) - // } - - pub fn geometry(&self, layout: &Layout) -> Vec { - let mut rects = Vec::new(); - self.geometry_with(layout, |rect| rects.push(rect)); - rects - } - - pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { - // Ensure we add some visual indicator for selected empty - // lines. - const MIN_RECT_WIDTH: f64 = 4.0; - if self.is_collapsed() { - return; - } - let mut start = self.anchor; - let mut end = self.focus; - if start.text_start > end.text_start { - core::mem::swap(&mut start, &mut end); - } - let text_range = start.text_start..end.text_start; - let line_start_ix = start.path.line_index(); - let line_end_ix = end.path.line_index(); - for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { - continue; - }; - let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; - if line_ix == line_start_ix || line_ix == line_end_ix { - // We only need to run the expensive logic on the first and - // last lines - let mut start_x = metrics.offset as f64; - let mut cur_x = start_x; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&(cluster.text_range().start as u32)) { - cur_x += advance; - } else { - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x as _, line_min, start_x + width, line_max)); - } - cur_x += advance; - start_x = cur_x; - } - } - } - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x, line_min, start_x + width, line_max)); - } - } else { - let x = metrics.offset as f64; - let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); - f(Rect::new(x, line_min, x + width, line_max)); - } - } - } -} diff --git a/parley/src/layout/selection.rs b/parley/src/layout/selection.rs deleted file mode 100644 index 087b57b0..00000000 --- a/parley/src/layout/selection.rs +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Text selection support. - -use peniko::kurbo::Rect; - -use super::{Brush, Cluster, ClusterPath, ClusterSide, Layout, Line, Run}; -use core::ops::Range; - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Cursor { - path: ClusterPath, - index: u32, - text_start: u32, - text_end: u32, - visual_offset: f32, - is_rtl: bool, -} - -impl Cursor { - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (mut path, side) = ClusterPath::from_point(layout, x, y); - if side == ClusterSide::Trailing { - path = path.next_visual(layout).unwrap_or(path); - } - Self::from_cluster_path(layout, path) - } - - pub fn from_byte_index(layout: &Layout, byte_index: usize) -> Self { - let path = ClusterPath::from_byte_index(layout, byte_index); - Self::from_cluster_path(layout, path) - } - - fn from_cluster_path( - layout: &Layout, - mut path: ClusterPath, - ) -> Self { - // if side == ClusterSide::Trailing { - // path = path.next_visual(layout).unwrap_or(path) - // }; - let (index, text_start, text_end, visual_offset, is_rtl) = - if let Some(cluster) = path.cluster(layout) { - let range = cluster.text_range(); - let index = range.start as u32; - let mut offset = path.visual_offset(layout).unwrap_or_default(); - if cluster.is_rtl() { - //offset += cluster.advance(); - } - ( - index, - range.start as u32, - range.end as u32, - offset, - cluster.is_rtl(), - ) - } else { - Default::default() - }; - Self { - path, - index, - text_start, - text_end, - visual_offset, - is_rtl, - } - } - - #[must_use] - fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, self.index as usize) - } - - /// Returns the path to the target cluster. - pub fn cluster_path(&self) -> ClusterPath { - self.path - } - - /// Returns the text range of the target cluster. - pub fn text_range(&self) -> Range { - self.text_start as usize..self.text_end as usize - } - - /// Returns the visual offset of the target cluster along the direction of - /// text flow. - pub fn visual_offset(&self) -> f32 { - self.visual_offset - } - - /// Returns the byte index associated with the cursor. - pub fn index(&self) -> usize { - self.index as usize - } - - pub fn geometry(&self, layout: &Layout, width: f32) -> Option { - let metrics = *self.path.line(layout)?.metrics(); - let line_x = self.visual_offset as f64; - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + width as f64, - metrics.max_coord as f64, - )) - } - - pub fn weak_geometry(&self, layout: &Layout, width: f32) -> Option { - let alternate = self.path.alternate_path(layout)?; - let metrics = *alternate.line(layout)?.metrics(); - let line_x = alternate.visual_offset(layout)? as f64; - Some(Rect::new( - line_x, - metrics.min_coord as f64, - line_x + width as f64, - metrics.max_coord as f64, - )) - } - - pub fn next_visual(&self, layout: &Layout) -> Self { - if let Some(path) = self.path.next_visual(layout) { - Self::from_cluster_path(layout, path) - } else { - *self - } - } - - pub fn previous_visual(&self, layout: &Layout) -> Self { - if let Some(path) = self.path.previous_visual(layout) { - Self::from_cluster_path(layout, path) - } else { - *self - } - } -} - -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct Selection { - anchor: Cursor, - focus: Cursor, - h_pos: Option, -} - -impl From for Selection { - fn from(value: Cursor) -> Self { - Self { - anchor: value, - focus: value, - h_pos: None, - } - } -} - -impl Selection { - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - Cursor::from_point(layout, x, y).into() - } - - pub fn from_byte_index(layout: &Layout, index: usize) -> Self { - Cursor::from_byte_index(layout, index).into() - } - - pub fn anchor(&self) -> &Cursor { - &self.anchor - } - - pub fn focus(&self) -> &Cursor { - &self.focus - } - - pub fn is_collapsed(&self) -> bool { - self.anchor.text_start == self.focus.text_start - } - - pub fn text_range(&self) -> Range { - if self.anchor.text_start < self.focus.text_start { - self.anchor.text_start as usize..self.focus.text_start as usize - } else { - self.focus.text_start as usize..self.anchor.text_start as usize - } - } - - /// Returns the index where text should be inserted based on this - /// selection. - pub fn insertion_index(&self) -> usize { - self.focus.text_start as usize - } - - #[must_use] - pub fn collapse(&self) -> Self { - Self { - anchor: self.focus, - focus: self.focus, - h_pos: self.h_pos, - } - } - - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - let anchor = self.anchor.refresh(layout); - let focus = self.focus.refresh(layout); - Self { - anchor, - focus, - h_pos: None, - } - } - - #[must_use] - pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { - let focus = Cursor::from_point(layout, x, y); - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } - - // #[must_use] - // pub fn next_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_end as usize), - // extend, - // ) - // } - - // #[must_use] - // pub fn prev_logical(&self, layout: &Layout, extend: bool) -> Self { - // self.maybe_extend( - // Cursor::from_byte_index(layout, self.focus.text_start.saturating_sub(1) as usize), - // extend, - // ) - // } - - #[must_use] - pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.next_visual(layout), extend) - } - - #[must_use] - pub fn prev_visual(&self, layout: &Layout, extend: bool) -> Self { - self.maybe_extend(self.focus.previous_visual(layout), extend) - } - - fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { - if extend { - Self { - anchor: self.anchor, - focus, - h_pos: None, - } - } else { - focus.into() - } - } - - #[must_use] - pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { - // if let Some(line) = self.focus.path.line(layout) { - // self.maybe_extend( - // Cursor::from_byte_index(layout, line.text_range().start, Affinity::Downstream), - // extend, - // ) - // } else { - // *self - // } - *self - } - - #[must_use] - pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { - // if let Some(line) = self.focus.path.line(layout) { - // self.maybe_extend( - // Cursor::from_byte_index( - // layout, - // line.text_range().end.saturating_sub(1), - // Affinity::Upstream, - // ), - // extend, - // ) - // } else { - // *self - // } - *self - } - - #[must_use] - pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) - } - - #[must_use] - pub fn prev_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) - } - - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { - // let line_index = self - // .focus - // .placement - // .primary_position() - // .line_index - // .saturating_add_signed(line_delta); - let line_index = self - .focus - .path - .line_index() - .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; - let y = line.metrics().baseline - line.metrics().ascent * 0.5; - let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); - let new_focus = Cursor::from_point(layout, h_pos, y); - let h_pos = Some(h_pos); - Some(if extend { - Self { - anchor: self.anchor, - focus: new_focus, - h_pos, - } - } else { - Self { - anchor: new_focus, - focus: new_focus, - h_pos, - } - }) - } - - pub fn visual_focus(&self, layout: &Layout) -> Option { - self.focus.geometry(layout, 1.5) - } - - // pub fn visual_alternate_focus( - // &self, - // layout: &Layout, - // ) -> Option { - // visual_for_cursor(layout, self.focus.placement.alternate_position()) - // } - - // pub fn visual_anchor(&self, layout: &Layout) -> Option { - // self.anchor.path.visual_line(layout).map(|line| { - // let metrics = line.metrics(); - // let line_min = (metrics.baseline - metrics.ascent - metrics.leading * 0.5) as f64; - // let line_max = line_min + metrics.line_height as f64; - // let line_x = self.anchor.offset as f64; - // peniko::kurbo::Line::new((line_x, line_min - 10.0), (line_x, line_max - 10.0)) - // }) - // } - - pub fn geometry(&self, layout: &Layout) -> Vec { - let mut rects = Vec::new(); - self.geometry_with(layout, |rect| rects.push(rect)); - rects - } - - pub fn geometry_with(&self, layout: &Layout, mut f: impl FnMut(Rect)) { - // Ensure we add some visual indicator for selected empty - // lines. - const MIN_RECT_WIDTH: f64 = 4.0; - if self.is_collapsed() { - return; - } - let mut start = self.anchor; - let mut end = self.focus; - if start.text_start > end.text_start { - core::mem::swap(&mut start, &mut end); - } - let text_range = start.text_start..end.text_start; - let line_start_ix = start.path.line_index(); - let line_end_ix = end.path.line_index(); - for line_ix in line_start_ix..=line_end_ix { - let Some(line) = layout.get(line_ix as usize) else { - continue; - }; - let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; - if line_ix == line_start_ix || line_ix == line_end_ix { - // We only need to run the expensive logic on the first and - // last lines - let mut start_x = metrics.offset as f64; - let mut cur_x = start_x; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&(cluster.text_range().start as u32)) { - cur_x += advance; - } else { - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x as _, line_min, start_x + width, line_max)); - } - cur_x += advance; - start_x = cur_x; - } - } - } - if cur_x != start_x { - let width = (cur_x - start_x).max(MIN_RECT_WIDTH); - f(Rect::new(start_x, line_min, start_x + width, line_max)); - } - } else { - let x = metrics.offset as f64; - let width = (metrics.advance as f64).max(MIN_RECT_WIDTH); - f(Rect::new(x, line_min, x + width, line_max)); - } - } - } -} From 5aceaaa0140ed21f3296e40d0f34f6f434039c8b Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 10:56:18 -0400 Subject: [PATCH 10/18] mostly final state --- examples/tiny_skia_render/src/main.rs | 2 +- examples/vello_editor/src/text.rs | 14 +- parley/src/layout/cluster.rs | 652 +++++++++++--------------- parley/src/layout/cursor.rs | 302 +++++++----- parley/src/layout/line/greedy.rs | 80 ++-- parley/src/layout/line/mod.rs | 24 +- parley/src/layout/mod.rs | 39 +- parley/src/layout/run.rs | 20 +- 8 files changed, 550 insertions(+), 583 deletions(-) diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index 6b21fd22..10ee4876 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -75,7 +75,7 @@ fn main() { let mut layout: Layout = builder.build(); // Perform layout (including bidi resolution and shaping) with start alignment - layout.break_all_lines(max_advance, Alignment::Start); + layout.break_all_lines(max_advance); layout.align(max_advance, Alignment::Start); let width = layout.width().ceil() as u32; let height = layout.height().ceil() as u32; diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 3af08e33..b726b470 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use clipboard::ClipboardProvider; -use parley::layout::cursor::{Selection, VisualCursorMode}; +use parley::layout::cursor::{Selection, VisualMode}; use parley::layout::Affinity; use parley::{layout::PositionedLayoutItem, FontContext}; use peniko::{kurbo::Affine, Color, Fill}; @@ -32,7 +32,7 @@ pub struct Editor { buffer: String, layout: Layout, selection: Selection, - cursor_mode: VisualCursorMode, + cursor_mode: VisualMode, last_click_time: Option, click_count: u32, pointer_down: bool, @@ -54,11 +54,12 @@ impl Editor { builder.push_default(&parley::style::StyleProperty::FontSize(32.0)); builder.push_default(&parley::style::StyleProperty::LineHeight(1.2)); builder.push_default(&parley::style::StyleProperty::FontStack( - parley::style::FontStack::Source("system-ui"), + parley::style::FontStack::Source("verdana"), )); builder.build_into(&mut self.layout); + self.layout.break_all_lines(Some(width - INSET * 2.0)); self.layout - .break_all_lines(Some(width - INSET * 2.0), parley::layout::Alignment::Start); + .align(Some(width - INSET * 2.0), parley::layout::Alignment::Start); self.width = width; } @@ -157,7 +158,6 @@ impl Editor { }; self.update_layout(self.width, 1.0); self.selection = self.selection.refresh(&self.layout); - // Selection::from_byte_index(&self.layout, start, Default::default()); } KeyCode::Backspace => { let start = if self.selection.is_collapsed() { @@ -214,6 +214,8 @@ impl Editor { } else { self.click_count = 1; } + } else { + self.click_count = 1; } self.last_click_time = Some(now); match self.click_count { @@ -273,7 +275,7 @@ impl Editor { scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor); }; if let Some(cursor) = self.selection.focus().weak_geometry(&self.layout, 1.5) { - scene.fill(Fill::NonZero, transform, Color::YELLOW, None, &cursor); + scene.fill(Fill::NonZero, transform, Color::LIGHT_GRAY, None, &cursor); }; for line in self.layout.lines() { for item in line.items() { diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 96a384f7..876e407b 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -4,6 +4,79 @@ use super::*; impl<'a, B: Brush> Cluster<'a, B> { + /// Returns the cluster for the given layout and byte index. + pub fn from_index(layout: &'a Layout, byte_index: usize) -> Self { + let mut path = ClusterPath::default(); + if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { + path.line_index = line_index as u32; + for (run_index, run) in line.runs().enumerate() { + path.run_index = run_index as u32; + if !run.text_range().contains(&byte_index) { + continue; + } + for (cluster_index, cluster) in run.clusters().enumerate() { + path.logical_index = cluster_index as u32; + if cluster.text_range().contains(&byte_index) { + return path.cluster(layout).unwrap(); + } + } + } + } + path.cluster(layout).unwrap() + } + + /// Returns the cluster and affinity for the given layout and point. + pub fn from_point(layout: &'a Layout, x: f32, y: f32) -> (Self, Affinity) { + let mut path = ClusterPath::default(); + if let Some((line_index, line)) = layout.line_for_offset(y) { + path.line_index = line_index as u32; + let mut offset = 0.0; + let last_run_index = line.len().saturating_sub(1); + for (run_index, run) in line.runs().enumerate() { + let is_last_run = run_index == last_run_index; + let run_advance = run.advance(); + path.run_index = run_index as u32; + path.logical_index = 0; + if x > offset + run_advance && !is_last_run { + offset += run_advance; + continue; + } + let last_cluster_index = run.cluster_range().len().saturating_sub(1); + for (visual_index, cluster) in run.visual_clusters().enumerate() { + let is_last_cluster = is_last_run && visual_index == last_cluster_index; + path.logical_index = + run.visual_to_logical(visual_index).unwrap_or_default() as u32; + let cluster_advance = cluster.advance(); + let edge = offset; + offset += cluster_advance; + if x > offset && !is_last_cluster { + continue; + } + let affinity = + Affinity::new(cluster.is_rtl(), x <= edge + cluster_advance * 0.5); + return (path.cluster(layout).unwrap(), affinity); + } + } + } + (path.cluster(layout).unwrap(), Affinity::default()) + } + + /// Returns the line that contains the cluster. + pub fn line(&self) -> Line<'a, B> { + self.run.layout.get(self.run.line_index as usize).unwrap() + } + + /// Returns the run that contains the cluster. + pub fn run(&self) -> Run<'a, B> { + self.run.clone() + } + + /// Returns the path that contains the set of indices to reach the cluster + /// from a layout. + pub fn path(&self) -> ClusterPath { + self.path + } + /// Returns the range of text that defines the cluster. pub fn text_range(&self) -> Range { self.data.text_range(self.run.data) @@ -62,174 +135,31 @@ impl<'a, B: Brush> Cluster<'a, B> { } else { let start = self.run.data.glyph_start + self.data.glyph_offset as usize; GlyphIter::Slice( - self.run.layout.glyphs[start..start + self.data.glyph_len as usize].iter(), + self.run.layout.data.glyphs[start..start + self.data.glyph_len as usize].iter(), ) } } - pub(crate) fn info(&self) -> ClusterInfo { - self.data.info - } -} - -/// Determines how a cursor attaches to a cluster. -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] -pub enum Affinity { - /// Left side for LTR clusters and right side for RTL clusters. - #[default] - Downstream = 0, - /// Right side for LTR clusters and left side for RTL clusters. - Upstream = 1, -} - -impl Affinity { - pub(crate) fn new(is_rtl: bool, is_leading: bool) -> Self { - match (is_rtl, is_leading) { - // trailing edge of RTL and leading edge of LTR - (true, false) | (false, true) => Affinity::Downstream, - // leading edge of RTL and trailing edge of LTR - (true, true) | (false, false) => Affinity::Upstream, - } - } - - pub fn invert(&self) -> Self { - match self { - Self::Downstream => Self::Upstream, - Self::Upstream => Self::Downstream, - } - } - - /// Returns true if the cursor should be placed on the leading edge. - pub fn is_visually_leading(&self, is_rtl: bool) -> bool { - match (*self, is_rtl) { - (Self::Upstream, true) | (Self::Downstream, false) => true, - (Self::Upstream, false) | (Self::Downstream, true) => false, - } - } - - /// Returns true if the cursor should be placed on the trailing edge. - pub fn is_visually_trailing(&self, is_rtl: bool) -> bool { - !self.is_visually_leading(is_rtl) + /// Returns true if this cluster is at the beginning of a line. + pub fn is_start_of_line(&self) -> bool { + self.path.run_index == 0 && self.run.logical_to_visual(self.path.logical_index()) == Some(0) } -} -/// Index based path to a cluster. -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct ClusterPath { - line_index: u32, - run_index: u32, - logical_index: u32, -} - -impl ClusterPath { - /// Returns the path to the cluster for the given layout and byte index. - pub fn from_byte_index(layout: &Layout, byte_index: usize) -> Self { - let mut path = Self::default(); - if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { - path.line_index = line_index as u32; - for (run_index, run) in line.runs().enumerate() { - path.run_index = run_index as u32; - if !run.text_range().contains(&byte_index) { - continue; - } - for (cluster_index, cluster) in run.clusters().enumerate() { - path.logical_index = cluster_index as u32; - if cluster.text_range().contains(&byte_index) { - return path; - } - } - } - } - path + /// Returns true if this cluster is at the end of a line. + pub fn is_end_of_line(&self) -> bool { + self.line().len().saturating_sub(1) == self.path.run_index() + && self.run.logical_to_visual(self.path.logical_index()) + == Some(self.run.cluster_range().len().saturating_sub(1)) } - /// Returns the path to the cluster and the clicked side for the given layout - /// and point. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> (Self, Affinity) { - let mut path = Self::default(); - if let Some((line_index, line)) = layout.line_for_offset(y) { - path.line_index = line_index as u32; - let mut offset = 0.0; - let last_run_index = line.len().saturating_sub(1); - for (run_index, run) in line.runs().enumerate() { - let is_last_run = run_index == last_run_index; - let run_advance = run.advance(); - path.run_index = run_index as u32; - path.logical_index = 0; - if x > offset + run_advance && !is_last_run { - offset += run_advance; - continue; - } - let last_cluster_index = run.cluster_range().len().saturating_sub(1); - for (visual_index, cluster) in run.visual_clusters().enumerate() { - let is_last_cluster = is_last_run && visual_index == last_cluster_index; - path.logical_index = - run.visual_to_logical(visual_index).unwrap_or_default() as u32; - let cluster_advance = cluster.advance(); - let edge = offset; - offset += cluster_advance; - if x > offset && !is_last_cluster { - continue; - } - let affinity = - Affinity::new(cluster.is_rtl(), x <= edge + cluster_advance * 0.5); - return (path, affinity); - } - } + /// If the cluster as at the end of the line, returns the reason + /// for the line break. + pub fn is_line_break(&self) -> Option { + if self.is_end_of_line() { + Some(self.line().data.break_reason) + } else { + None } - (path, Affinity::default()) - } - - /// Returns the index of the line containing this cluster. - pub fn line_index(&self) -> usize { - self.line_index as usize - } - - /// Returns the index of the run (within the owning line) containing this - /// cluster. - pub fn run_index(&self) -> usize { - self.run_index as usize - } - - /// Returns the logical index of the cluster within the owning run. - pub fn logical_index(&self) -> usize { - self.logical_index as usize - } - - /// Returns the line for this path and the specified layout. - pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - layout.get(self.line_index()) - } - - /// Returns the run for this path and the specified layout. - pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.line(layout)?.run(self.run_index()) - } - - /// Returns the cluster for this path and the specified layout. - pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.run(layout)?.get(self.logical_index()) - } - - /// Returns true if this cluster is at the beginning of a line. - pub fn is_start_of_line(&self, layout: &Layout) -> bool { - self.run_index == 0 - && self - .run(layout) - .and_then(|run| run.logical_to_visual(self.logical_index())) - == Some(0) - } - - /// Returns true if this cluster is at the end of a line. - pub fn is_end_of_line(&self, layout: &Layout) -> bool { - self.line(layout).map(|line| line.len().saturating_sub(1)) == Some(self.run_index()) - && self - .run(layout) - .map(|run| { - run.logical_to_visual(self.logical_index()) - == Some(run.cluster_range().len().saturating_sub(1)) - }) - .unwrap_or_default() } /// If this cluster, combined with the given affinity, sits on a @@ -239,113 +169,87 @@ impl ClusterPath { /// For example, if this cluster is a left-to-right cluster, then this /// will return the cluster that represents the position where a /// right-to-left character would be inserted, and vice versa. - pub fn bidi_link_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - affinity: Affinity, - ) -> Option<(Self, Cluster<'a, B>)> { - let run = self.run(layout)?; - let run_end = run.len().checked_sub(1)?; - let visual_index = run.logical_to_visual(self.logical_index())?; - let cluster = self.cluster(layout)?; - let is_rtl = cluster.is_rtl(); + pub fn bidi_link(&self, affinity: Affinity) -> Option { + let run_end = self.run.len().checked_sub(1)?; + let visual_index = self.run.logical_to_visual(self.path.logical_index())?; + let is_rtl = self.is_rtl(); let is_leading = affinity.is_visually_leading(is_rtl); let at_start = visual_index == 0 && is_leading; let at_end = visual_index == run_end && !is_leading; - let other_path = if (at_start && !is_rtl) || (at_end && is_rtl) { - let line = self.line(layout)?; - let prev_run_index = self.run_index().checked_sub(1)?; - let prev_run = line.run(prev_run_index)?; - ClusterPath { - line_index: self.line_index, - run_index: prev_run_index as u32, - logical_index: prev_run.len().checked_sub(1)? as u32, - } + let other = if (at_start && !is_rtl) || (at_end && is_rtl) { + self.previous_logical()? } else if (at_end && !is_rtl) || (at_start && is_rtl) { - ClusterPath { - line_index: self.line_index, - run_index: self.run_index() as u32 + 1, - logical_index: 0, - } + self.next_logical()? } else { return None; }; - let other_cluster = other_path.cluster(layout)?; - if other_cluster.is_rtl() == is_rtl { + if other.is_rtl() == is_rtl { return None; } - Some((other_path, other_path.cluster(layout)?)) + Some(other) } - /// Returns the path of the cluster that follows this one in visual order. - pub fn next_visual(&self, layout: &Layout) -> Option { - let line = self.line(layout)?; - let run = line.run(self.run_index())?; - let visual_index = run.logical_to_visual(self.logical_index())?; - if let Some(cluster_index) = run.visual_to_logical(visual_index + 1) { - // Easy mode: next visual cluster is in the same run - Some(Self { - line_index: self.line_index, - run_index: self.run_index, - logical_index: cluster_index as u32, - }) + /// Returns the cluster that follows this one in logical order. + pub fn next_logical(&self) -> Option { + if self.path.logical_index() + 1 < self.run.cluster_range().len() { + // Fast path: next cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, + logical_index: self.path.logical_index + 1, + } + .cluster(self.run.layout) } else { - // We just want to find the first line/run following this one that - // contains any cluster. - let mut run_index = self.run_index() + 1; - for line_index in self.line_index()..layout.len() { - let line = layout.get(line_index)?; - for run_index in run_index..line.len() { - if let Some(run) = line.run(run_index) { - if !run.cluster_range().is_empty() { - return Some(Self { - line_index: line_index as u32, - run_index: run_index as u32, - logical_index: run.visual_to_logical(0)? as u32, - }); - } - } - } - // Restart at first run on next line - run_index = 0; + let index = self.text_range().end; + if index >= self.run.layout.data.text_len { + return None; } - None + // We have to search for the cluster containing our end index + Some(Self::from_index(self.run.layout, index)) } } - pub fn next_visual_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.next_visual(layout) - .and_then(|path| Some((path, path.cluster(layout)?))) - } - - /// Returns the path of the cluster that follows this one in logical order. - pub fn next_logical(&self, layout: &Layout) -> Option { - let line = self.line(layout)?; - let run = line.run(self.run_index())?; - if self.logical_index() + 1 < run.cluster_range().len() { - // Easy mode: next cluster is in the same run - Some(Self { - line_index: self.line_index, - run_index: self.run_index, - logical_index: self.logical_index + 1, - }) + /// Returns the cluster that precedes this one in logical order. + pub fn previous_logical(&self) -> Option { + if self.path.logical_index > 0 { + // Fast path: previous cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, + logical_index: self.path.logical_index - 1, + } + .cluster(self.run.layout) + } else { + Some(Self::from_index( + self.run.layout, + self.text_range().start.checked_sub(1)?, + )) + } + } + + /// Returns the cluster that follows this one in visual order. + pub fn next_visual(&self) -> Option { + let layout = self.run.layout; + let run = self.run.clone(); + let visual_index = run.logical_to_visual(self.path.logical_index())?; + if let Some(cluster_index) = run.visual_to_logical(visual_index + 1) { + // Fast path: next visual cluster is in the same run + run.get(cluster_index) } else { // We just want to find the first line/run following this one that // contains any cluster. - let mut run_index = self.run_index() + 1; - for line_index in self.line_index()..layout.len() { + let mut run_index = self.path.run_index() + 1; + for line_index in self.path.line_index()..layout.len() { let line = layout.get(line_index)?; for run_index in run_index..line.len() { if let Some(run) = line.run(run_index) { if !run.cluster_range().is_empty() { - return Some(Self { + return ClusterPath { line_index: line_index as u32, run_index: run_index as u32, - logical_index: 0, - }); + logical_index: run.visual_to_logical(0)? as u32, + } + .cluster(layout); } } } @@ -356,186 +260,84 @@ impl ClusterPath { } } - pub fn next_logical_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.next_logical(layout) - .and_then(|path| Some((path, path.cluster(layout)?))) - } - - /// Returns the path of the cluster that precedes this one in visual order. - pub fn previous_visual(&self, layout: &Layout) -> Option { - let line = self.line(layout)?; - let run = line.run(self.run_index())?; - let visual_index = run.logical_to_visual(self.logical_index())?; + /// Returns the cluster that precedes this one in visual order. + pub fn previous_visual(&self) -> Option { + let visual_index = self.run.logical_to_visual(self.path.logical_index())?; if let Some(cluster_index) = visual_index .checked_sub(1) - .and_then(|visual_index| run.visual_to_logical(visual_index)) + .and_then(|visual_index| self.run.visual_to_logical(visual_index)) { - // Easy mode: previous visual cluster is in the same run - Some(Self { - line_index: self.line_index, - run_index: self.run_index, + // Fast path: previous visual cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, logical_index: cluster_index as u32, - }) - } else { - // We just want to find the first line/run preceding this one that - // contains any cluster. - let mut run_index = Some(self.run_index()); - for line_index in (0..=self.line_index()).rev() { - let line = layout.get(line_index)?; - let first_run = run_index.unwrap_or(line.len()); - for run_index in (0..first_run).rev() { - if let Some(run) = line.run(run_index) { - let range = run.cluster_range(); - if !range.is_empty() { - return Some(Self { - line_index: line_index as u32, - run_index: run_index as u32, - logical_index: run.visual_to_logical(range.len() - 1)? as u32, - }); - } - } - } - // Restart at last run - run_index = None; } - None - } - } - - pub fn previous_visual_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.previous_visual(layout) - .and_then(|path| Some((path, path.cluster(layout)?))) - } - - /// Returns the path of the cluster that precedes this one in logical - /// order. - pub fn previous_logical(&self, layout: &Layout) -> Option { - if self.logical_index > 0 { - // Easy mode: previous cluster is in the same run - Some(Self { - line_index: self.line_index, - run_index: self.run_index, - logical_index: self.logical_index - 1, - }) + .cluster(self.run.layout) } else { // We just want to find the first line/run preceding this one that // contains any cluster. - let mut run_index = Some(self.run_index()); - for line_index in (0..=self.line_index()).rev() { + let layout = self.run.layout; + let mut run_index = Some(self.path.run_index()); + for line_index in (0..=self.path.line_index()).rev() { let line = layout.get(line_index)?; let first_run = run_index.unwrap_or(line.len()); for run_index in (0..first_run).rev() { if let Some(run) = line.run(run_index) { let range = run.cluster_range(); if !range.is_empty() { - return Some(Self { + return ClusterPath { line_index: line_index as u32, run_index: run_index as u32, - logical_index: (range.len() - 1) as u32, - }); + logical_index: run.visual_to_logical(range.len() - 1)? as u32, + } + .cluster(layout); } } } - // Restart at last run run_index = None; } None } } - pub fn previous_logical_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.previous_logical(layout) - .and_then(|path| Some((path, path.cluster(layout)?))) - } - - pub fn next_word(&self, layout: &Layout) -> Option { - let line_start = self.line_index(); - let mut run_start = self.run_index(); - let mut cluster_start = self.logical_index() + 1; - for line_index in line_start..layout.len() { - let line = layout.get(line_index)?; - for run_index in run_start..line.len() { - let run = line.run(run_index)?; - for cluster_index in cluster_start..run.len() { - let cluster = run.get(cluster_index)?; - if cluster.is_word_boundary() { - return Some(Self { - line_index: line_index as u32, - run_index: run_index as u32, - logical_index: cluster_index as u32, - }); - } - } - cluster_start = 0; + /// Returns the next cluster that is marked as a word boundary. + pub fn next_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(next) = cluster.next_logical() { + if next.is_word_boundary() { + return Some(next); } - run_start = 0; + cluster = next; } None } - pub fn next_word_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.next_word(layout) - .and_then(|p| Some((p, p.cluster(layout)?))) - } - - pub fn previous_word(&self, layout: &Layout) -> Option { - let line_start = self.line_index(); - let mut run_start = Some(self.run_index() + 1); - let mut cluster_start = Some(self.logical_index()); - for line_index in (0..=line_start).rev() { - let line = layout.get(line_index)?; - let run_start = run_start.take().unwrap_or(line.len()); - for run_index in (0..run_start).rev() { - let run = line.run(run_index)?; - let cluster_start = cluster_start.take().unwrap_or(run.len()); - for cluster_index in (0..cluster_start).rev() { - let cluster = run.get(cluster_index)?; - if cluster.is_word_boundary() { - return Some(Self { - line_index: line_index as u32, - run_index: run_index as u32, - logical_index: cluster_index as u32, - }); - } - } + /// Returns the previous cluster that is marked as a word boundary. + pub fn previous_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(prev) = cluster.previous_logical() { + if prev.is_word_boundary() { + return Some(prev); } + cluster = prev; } None } - pub fn previous_word_cluster<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> Option<(Self, Cluster<'a, B>)> { - self.previous_word(layout) - .and_then(|p| Some((p, p.cluster(layout)?))) - } - /// Returns the visual offset of this cluster along direction of text flow. /// /// This cost of this function is roughly linear in the number of clusters /// on the containing line. - pub fn visual_offset(&self, layout: &Layout) -> Option { - let line = self.line(layout)?; + pub fn visual_offset(&self) -> Option { + let line = self.path.line(self.run.layout)?; let mut offset = 0.0; - for run_index in 0..=self.run_index() { + for run_index in 0..=self.path.run_index() { let run = line.run(run_index)?; - if run_index != self.run_index() { + if run_index != self.path.run_index() { offset += run.advance(); } else { - let visual_index = run.logical_to_visual(self.logical_index())?; + let visual_index = run.logical_to_visual(self.path.logical_index())?; for cluster in run.visual_clusters().take(visual_index) { offset += cluster.advance(); } @@ -543,6 +345,100 @@ impl ClusterPath { } Some(offset) } + + pub(crate) fn info(&self) -> ClusterInfo { + self.data.info + } +} + +/// Determines how a cursor attaches to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub enum Affinity { + /// Left side for LTR clusters and right side for RTL clusters. + #[default] + Downstream = 0, + /// Right side for LTR clusters and left side for RTL clusters. + Upstream = 1, +} + +impl Affinity { + pub(crate) fn new(is_rtl: bool, is_leading: bool) -> Self { + match (is_rtl, is_leading) { + // trailing edge of RTL and leading edge of LTR + (true, false) | (false, true) => Affinity::Downstream, + // leading edge of RTL and trailing edge of LTR + (true, true) | (false, false) => Affinity::Upstream, + } + } + + pub fn invert(&self) -> Self { + match self { + Self::Downstream => Self::Upstream, + Self::Upstream => Self::Downstream, + } + } + + /// Returns true if the cursor should be placed on the leading edge. + pub fn is_visually_leading(&self, is_rtl: bool) -> bool { + match (*self, is_rtl) { + (Self::Upstream, true) | (Self::Downstream, false) => true, + (Self::Upstream, false) | (Self::Downstream, true) => false, + } + } + + /// Returns true if the cursor should be placed on the trailing edge. + pub fn is_visually_trailing(&self, is_rtl: bool) -> bool { + !self.is_visually_leading(is_rtl) + } +} + +/// Index based path to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct ClusterPath { + line_index: u32, + run_index: u32, + logical_index: u32, +} + +impl ClusterPath { + pub(crate) fn new(line_index: u32, run_index: u32, logical_index: u32) -> Self { + Self { + line_index, + run_index, + logical_index, + } + } + + /// Returns the index of the line containing this cluster. + pub fn line_index(&self) -> usize { + self.line_index as usize + } + + /// Returns the index of the run (within the owning line) containing this + /// cluster. + pub fn run_index(&self) -> usize { + self.run_index as usize + } + + /// Returns the logical index of the cluster within the owning run. + pub fn logical_index(&self) -> usize { + self.logical_index as usize + } + + /// Returns the line for this path and the specified layout. + pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + layout.get(self.line_index()) + } + + /// Returns the run for this path and the specified layout. + pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.line(layout)?.run(self.run_index()) + } + + /// Returns the cluster for this path and the specified layout. + pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.run(layout)?.get(self.logical_index()) + } } #[derive(Clone)] diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index f5610871..f022778d 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -3,12 +3,13 @@ //! Text selection support. -use super::{Affinity, Brush, ClusterPath, Layout}; +use super::{Affinity, BreakReason, Brush, Cluster, ClusterPath, Layout}; use core::ops::Range; use peniko::kurbo::Rect; +/// Defines how a cursor will bind to a text position when moving visually. #[derive(Copy, Clone, PartialEq, Default, Debug)] -pub enum VisualCursorMode { +pub enum VisualMode { /// During cursor motion, affinity is adjusted to prioritize the dominant /// direction of the layout. /// @@ -39,7 +40,7 @@ pub enum VisualCursorMode { Adaptive, } -impl VisualCursorMode { +impl VisualMode { /// Returns the preferred RTL state for the given layout. /// /// This is used to handle cursor modes when moving visually @@ -53,6 +54,7 @@ impl VisualCursorMode { } } +/// A single position in a layout. #[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Cursor { path: ClusterPath, @@ -65,62 +67,47 @@ pub struct Cursor { } impl Cursor { - /// Creates a new cursor for the given layout and point. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (path, affinity) = ClusterPath::from_point(layout, x, y); - Self::from_cluster_path(layout, path, affinity) - } - /// Returns a new cursor for the given layout, byte index and affinity. pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - let path = ClusterPath::from_byte_index(layout, index); - Self::from_cluster_path(layout, path, affinity) + let cluster = Cluster::from_index(layout, index); + Self::from_cluster(cluster, affinity) } - fn from_cluster_path( - layout: &Layout, - path: ClusterPath, - affinity: Affinity, - ) -> Self { - let (index, text_start, text_end, visual_offset, is_rtl) = - if let Some(cluster) = path.cluster(layout) { - let mut range = cluster.text_range(); - let index = range.start as u32; - let mut offset = path.visual_offset(layout).unwrap_or_default(); - let is_rtl = cluster.is_rtl(); - let is_left_side = affinity.is_visually_leading(is_rtl); - if !is_left_side { - offset += cluster.advance(); - if !is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - } else if is_rtl { - range = path - .next_logical(layout) - .and_then(|path| path.cluster(layout)) - .map(|cluster| cluster.text_range()) - .unwrap_or(range.end..range.end); - } - ( - index, - range.start as u32, - range.end as u32, - offset, - cluster.is_rtl(), - ) - } else { - Default::default() - }; + /// Creates a new cursor for the given layout and point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (cluster, affinity) = Cluster::from_point(layout, x, y); + Self::from_cluster(cluster, affinity) + } + + fn from_cluster(cluster: Cluster, mut affinity: Affinity) -> Self { + let mut range = cluster.text_range(); + let index = range.start as u32; + let mut offset = cluster.visual_offset().unwrap_or_default(); + let is_rtl = cluster.is_rtl(); + if cluster.is_line_break() == Some(BreakReason::Explicit) { + affinity = Affinity::Downstream; + } + let is_left_side = affinity.is_visually_leading(is_rtl); + if !is_left_side { + offset += cluster.advance(); + if !is_rtl { + range = cluster + .next_logical() + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } + } else if is_rtl { + range = cluster + .next_logical() + .map(|cluster| cluster.text_range()) + .unwrap_or(range.end..range.end); + } Self { - path, + path: cluster.path(), index, - text_start, - text_end, - visual_offset, + text_start: range.start as u32, + text_end: range.end as u32, + visual_offset: offset, is_rtl, affinity, } @@ -203,9 +190,10 @@ impl Cursor { } fn bidi_link_geometry(&self, layout: &Layout, size: f32) -> Option { - let (path, cluster) = self.path.bidi_link_cluster(layout, self.affinity)?; - let mut line_x = path.visual_offset(layout)? as f64; - let run = path.run(layout)?; + let cluster = self.path.cluster(layout)?.bidi_link(self.affinity)?; + let mut line_x = cluster.visual_offset()? as f64; + let run = cluster.run(); + let path = cluster.path(); if run.logical_to_visual(path.logical_index())? != 0 { line_x += cluster.advance() as f64; } @@ -218,20 +206,24 @@ impl Cursor { )) } - pub fn next_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + pub fn next_visual(&self, layout: &Layout, mode: VisualMode) -> Self { let prefer_rtl = mode.prefer_rtl(layout); + let Some(cluster) = self.path.cluster(layout) else { + return *self; + }; if self.affinity.is_visually_leading(self.is_rtl) { - // Check for directional boundary condition - if let Some((next_path, next_cluster)) = self.path.next_visual_cluster(layout) { + if let Some(next_cluster) = cluster.next_visual() { + // Handle hard line breaks + if cluster.is_line_break() == Some(BreakReason::Explicit) { + // If we're at the front of a hard line break and moving + // right, skip directly to the leading edge of the next cluster + return Self::from_cluster(next_cluster, self.affinity); + } + // Handle text direction boundaries if next_cluster.is_rtl() != self.is_rtl { - println!("MOVING RIGHT INTO BIDI BOUNDARY"); if let Some(prefer_rtl) = prefer_rtl { if self.is_rtl != prefer_rtl { - return Self::from_cluster_path( - layout, - next_path, - self.affinity.invert(), - ); + return Self::from_cluster(next_cluster, self.affinity.invert()); } } } @@ -239,47 +231,66 @@ impl Cursor { // We're moving right so we want to track right-side affinity; // let's swap. Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else if let Some((next, next_cluster)) = self.path.next_visual_cluster(layout) { - let next_rtl = next_cluster.is_rtl(); - // Check for directional boundary condition - if let Some((next_next, next_next_cluster)) = next.next_visual_cluster(layout) { - if next_next_cluster.is_rtl() != next_rtl { - println!("MOVING RIGHT INTO BIDI BOUNDARY 2"); + } else if let Some(next) = cluster.next_visual() { + // Handle soft line breaks + if matches!( + cluster.is_line_break(), + Some(BreakReason::Regular) | Some(BreakReason::Emergency) + ) { + // Without this check, moving to the next line will + // skip the first character which can be jarring + return Self::from_cluster(next, self.affinity.invert()); + } + // And hard line breaks + if next.is_line_break() == Some(BreakReason::Explicit) { + if let Some(next_next) = next.next_visual() { + return Self::from_cluster(next_next, self.affinity.invert()); + } + } + let next_rtl = next.is_rtl(); + if let Some(next_next) = next.next_visual() { + // Check for directional boundary condition + if next_next.is_rtl() != next_rtl { if let Some(prefer_rtl) = prefer_rtl { if next_rtl != prefer_rtl { - return Self::from_cluster_path(layout, next_next, self.affinity); + return Self::from_cluster(next_next, self.affinity); } else { - return Self::from_cluster_path(layout, next, self.affinity); + return Self::from_cluster(next, self.affinity); } } } } let affinity = if self.is_rtl != next_rtl { - // println!("MOVING INTO BIDI BOUNDARY"); self.affinity.invert() } else { self.affinity }; - Self::from_cluster_path(layout, next, affinity) + Self::from_cluster(next, affinity) } else { *self } } - pub fn previous_visual(&self, layout: &Layout, mode: VisualCursorMode) -> Self { + pub fn previous_visual(&self, layout: &Layout, mode: VisualMode) -> Self { let prefer_rtl = mode.prefer_rtl(layout); + let Some(cluster) = self.path.cluster(layout) else { + return *self; + }; if !self.affinity.is_visually_leading(self.is_rtl) { + // Handle hard line breaks + if cluster.is_hard_line_break() { + // If we're at the back of a hard line break and moving + // left, skip directly to the trailing edge of the next cluster + if let Some(next) = cluster.previous_logical() { + return Self::from_cluster(next, self.affinity); + } + } // Check for directional boundary condition - if let Some((prev_path, prev_cluster)) = self.path.previous_visual_cluster(layout) { - if prev_cluster.is_rtl() != self.is_rtl { - println!("MOVING LEFT INTO BIDI BOUNDARY"); + if let Some(prev) = cluster.previous_visual() { + if prev.is_rtl() != self.is_rtl { if let Some(prefer_rtl) = prefer_rtl { if self.is_rtl != prefer_rtl { - return Self::from_cluster_path( - layout, - prev_path, - self.affinity.invert(), - ); + return Self::from_cluster(prev, self.affinity.invert()); } } } @@ -287,17 +298,25 @@ impl Cursor { // We're moving left so we want to track left-side affinity; // let's swap Self::from_index(layout, self.index as usize, self.affinity.invert()) - } else if let Some((prev, prev_cluster)) = self.path.previous_visual_cluster(layout) { - let prev_rtl = prev_cluster.is_rtl(); + } else if let Some(prev) = cluster.previous_visual() { + // Handle soft line breaks + if matches!( + prev.is_line_break(), + Some(BreakReason::Regular) | Some(BreakReason::Emergency) + ) { + // Match the behavior of next_visual: move to the end of the soft line + // break + return Self::from_cluster(prev, self.affinity.invert()); + } + let prev_rtl = prev.is_rtl(); // Check for directional boundary condition - if let Some((prev_prev, prev_prev_cluster)) = prev.previous_visual_cluster(layout) { - if prev_prev_cluster.is_rtl() != prev_rtl { - println!("MOVING LEFT INTO BIDI BOUNDARY 2"); + if let Some(prev_prev) = prev.previous_visual() { + if prev_prev.is_rtl() != prev_rtl { if let Some(prefer_rtl) = prefer_rtl { if prev_rtl != prefer_rtl { - return Self::from_cluster_path(layout, prev_prev, self.affinity); + return Self::from_cluster(prev_prev, self.affinity); } else { - return Self::from_cluster_path(layout, prev, self.affinity); + return Self::from_cluster(prev, self.affinity); } } } @@ -307,44 +326,46 @@ impl Cursor { } else { self.affinity }; - Self::from_cluster_path(layout, prev, affinity) + Self::from_cluster(prev, affinity) } else { *self } } pub fn next_word(&self, layout: &Layout) -> Self { - let mut next_path = if self.affinity == Affinity::Upstream { - self.path.next_logical(layout).unwrap_or(self.path) - } else { - self.path + let Some(mut next) = self.path.cluster(layout) else { + return *self; }; - while let Some((path, cluster)) = next_path.next_word_cluster(layout) { - next_path = path; + if self.affinity == Affinity::Upstream { + next = next.next_logical().unwrap_or(next); + } + while let Some(cluster) = next.next_word() { + next = cluster.clone(); if !cluster.is_space_or_nbsp() { break; } } - Self::from_cluster_path(layout, next_path, Affinity::default()) + Self::from_cluster(next, Affinity::default()) } pub fn previous_word(&self, layout: &Layout) -> Self { - let mut next_path = if self.affinity == Affinity::Upstream { - self.path.next_logical(layout).unwrap_or(self.path) - } else { - self.path + let Some(mut next) = self.path.cluster(layout) else { + return *self; }; - // let mut next_path = self.path; - while let Some((path, cluster)) = next_path.previous_word_cluster(layout) { - next_path = path; + if self.affinity == Affinity::Upstream { + next = next.next_logical().unwrap_or(next); + } + while let Some(cluster) = next.previous_word() { + next = cluster.clone(); if !cluster.is_space_or_nbsp() { break; } } - Self::from_cluster_path(layout, next_path, Affinity::default()) + Self::from_cluster(next, Affinity::default()) } } +/// A range within a layout. #[derive(Copy, Clone, PartialEq, Default, Debug)] pub struct Selection { anchor: Cursor, @@ -364,19 +385,19 @@ impl From for Selection { } impl Selection { - /// Creates a collapsed selection with the anchor and focus set to the - /// position associated with the given point. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - Cursor::from_point(layout, x, y).into() - } - /// Creates a collapsed selection with the anchor and focus set to the /// position associated with the given byte index and affinity. pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { Cursor::from_index(layout, index, affinity).into() } - /// Creates a new selection bounding the word at the given coordinates. + /// Creates a collapsed selection with the anchor and focus set to the + /// position associated with the given point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + /// Creates a new selection bounding the word at the given point. pub fn word_from_point(layout: &Layout, x: f32, y: f32) -> Self { let mut anchor = Cursor::from_point(layout, x, y); if !(anchor.affinity == Affinity::Downstream @@ -483,9 +504,16 @@ impl Selection { pub fn next_visual( &self, layout: &Layout, - mode: VisualCursorMode, + mode: VisualMode, extend: bool, ) -> Self { + if !extend && !self.is_collapsed() { + if self.focus.text_start > self.anchor.text_start { + return self.focus.into(); + } else { + return self.anchor.into(); + } + } self.maybe_extend(self.focus.next_visual(layout, mode), extend) } @@ -498,9 +526,16 @@ impl Selection { pub fn previous_visual( &self, layout: &Layout, - mode: VisualCursorMode, + mode: VisualMode, extend: bool, ) -> Self { + if !extend && !self.is_collapsed() { + if self.focus.text_start < self.anchor.text_start { + return self.focus.into(); + } else { + return self.anchor.into(); + } + } self.maybe_extend(self.focus.previous_visual(layout, mode), extend) } @@ -579,7 +614,7 @@ impl Selection { /// otherwise the new selection will be collapsed. #[must_use] pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, 1, extend).unwrap_or(*self) + self.move_lines(layout, 1, extend) } /// Returns a new selection with the focus moved to the previous line. The @@ -589,26 +624,37 @@ impl Selection { /// otherwise the new selection will be collapsed. #[must_use] pub fn previous_line(&self, layout: &Layout, extend: bool) -> Self { - self.move_line(layout, -1, extend).unwrap_or(*self) + self.move_lines(layout, -1, extend) } - fn move_line( - &self, - layout: &Layout, - line_delta: isize, - extend: bool, - ) -> Option { + /// Returns a new selection with the focus moved the specified number of + /// lines. + /// + /// The sign of the `delta` parameter determines the direction to move with + /// negative values moving toward previous lines and positive ones moving + /// toward next lines. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn move_lines(&self, layout: &Layout, delta: isize, extend: bool) -> Self { + if delta == 0 { + return *self; + } let line_index = self .focus .path .line_index() - .saturating_add_signed(line_delta); - let line = layout.get(line_index)?; + .saturating_add_signed(delta) + .min(layout.len().saturating_sub(1)); + let Some(line) = layout.get(line_index) else { + return *self; + }; let y = line.metrics().baseline - line.metrics().ascent * 0.5; let h_pos = self.h_pos.unwrap_or(self.focus.visual_offset); let new_focus = Cursor::from_point(layout, h_pos, y); let h_pos = Some(h_pos); - Some(if extend { + if extend { Self { anchor: self.anchor, focus: new_focus, @@ -620,7 +666,7 @@ impl Selection { focus: new_focus, h_pos, } - }) + } } /// Returns a vector containing the rectangles which represent the visual diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index 3bd72a62..0a75c378 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -93,7 +93,7 @@ impl BreakerState { /// Line breaking support for a paragraph. pub struct BreakLines<'a, B: Brush> { - layout: &'a mut LayoutData, + layout: &'a mut Layout, lines: LineLayout, state: BreakerState, prev_state: Option, @@ -101,12 +101,12 @@ pub struct BreakLines<'a, B: Brush> { } impl<'a, B: Brush> BreakLines<'a, B> { - pub(crate) fn new(layout: &'a mut LayoutData) -> Self { - unjustify(layout); - layout.width = 0.; - layout.height = 0.; + pub(crate) fn new(layout: &'a mut Layout) -> Self { + unjustify(&mut layout.data); + layout.data.width = 0.; + layout.data.height = 0.; let mut lines = LineLayout::default(); - lines.swap(layout); + lines.swap(&mut layout.data); lines.lines.clear(); lines.line_items.clear(); Self { @@ -134,7 +134,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Computes the next line in the paragraph. Returns the advance and size /// (width and height for horizontal layouts) of the line. - pub fn break_next(&mut self, max_advance: f32, alignment: Alignment) -> Option<(f32, f32)> { + pub fn break_next(&mut self, max_advance: f32) -> Option<(f32, f32)> { // Maintain iterator state if self.done { return None; @@ -151,7 +151,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { &mut self.lines, &mut self.state.line, max_advance, - alignment, + Alignment::Start, $break_reason, ) }; @@ -163,9 +163,9 @@ impl<'a, B: Brush> BreakLines<'a, B> { // dbg!(&self.state.line.items); // Iterate over remaining runs in the Layout - let item_count = self.layout.items.len(); + let item_count = self.layout.data.items.len(); while self.state.item_idx < item_count { - let item = &self.layout.items[self.state.item_idx]; + let item = &self.layout.data.items[self.state.item_idx]; // println!( // "\nitem = {} {:?}. x: {}", @@ -175,7 +175,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { match item.kind { LayoutItemKind::InlineBox => { - let inline_box = &self.layout.inline_boxes[item.index]; + let inline_box = &self.layout.data.inline_boxes[item.index]; // Compute the x position of the content being currently processed let next_x = self.state.line.x + inline_box.width; @@ -211,9 +211,9 @@ impl<'a, B: Brush> BreakLines<'a, B> { } LayoutItemKind::TextRun => { let run_idx = item.index; - let run_data = &self.layout.runs[run_idx]; + let run_data = &self.layout.data.runs[run_idx]; - let run = Run::new(self.layout, run_data, None); + let run = Run::new(self.layout, 0, 0, run_data, None); let cluster_start = run_data.cluster_range.start; let cluster_end = run_data.cluster_range.end; @@ -394,7 +394,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Breaks all remaining lines with the specified maximum advance. This /// consumes the line breaker. - pub fn break_remaining(mut self, max_advance: f32, alignment: Alignment) { + pub fn break_remaining(mut self, max_advance: f32) { // println!("\nDEBUG ITEMS"); // for item in &self.layout.items { // match item.kind { @@ -408,7 +408,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { // println!("\nBREAK ALL"); - while self.break_next(max_advance, alignment).is_some() {} + while self.break_next(max_advance).is_some() {} self.finish(); } @@ -427,7 +427,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { run.is_whitespace = true; if run.bidi_level & 1 != 0 { // RTL runs check for "trailing" whitespace at the front. - for cluster in self.layout.clusters[run.cluster_range.clone()].iter() { + for cluster in self.layout.data.clusters[run.cluster_range.clone()].iter() { if cluster.info.is_whitespace() { run.has_trailing_whitespace = true; } else { @@ -436,7 +436,10 @@ impl<'a, B: Brush> BreakLines<'a, B> { } } } else { - for cluster in self.layout.clusters[run.cluster_range.clone()].iter().rev() { + for cluster in self.layout.data.clusters[run.cluster_range.clone()] + .iter() + .rev() + { if cluster.info.is_whitespace() { run.has_trailing_whitespace = true; } else { @@ -465,7 +468,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { { match line_item.kind { LayoutItemKind::InlineBox => { - let item = &self.layout.inline_boxes[line_item.index]; + let item = &self.layout.data.inline_boxes[line_item.index]; // Advance is already computed in "commit line" for items @@ -490,8 +493,8 @@ impl<'a, B: Brush> BreakLines<'a, B> { needs_reorder = true; } - let run = &self.layout.runs[line_item.index]; - let line_height = line_item.compute_line_height(self.layout); + let run = &self.layout.data.runs[line_item.index]; + let line_height = line_item.compute_line_height(&self.layout.data); line.metrics.line_height = line.metrics.line_height.max(line_height); // Ignore trailing whitespace for metrics computation @@ -501,10 +504,11 @@ impl<'a, B: Brush> BreakLines<'a, B> { } // Compute the run's advance by summing the advances of its constituent clusters - line_item.advance = self.layout.clusters[line_item.cluster_range.clone()] - .iter() - .map(|c| c.advance) - .sum(); + line_item.advance = self.layout.data.clusters + [line_item.cluster_range.clone()] + .iter() + .map(|c| c.advance) + .sum(); // Compute the run's vertical metrics line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); @@ -535,7 +539,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { .rfind(|item| item.is_text_run()); if let Some(last_run) = last_run { if !last_run.cluster_range.is_empty() { - let cluster = &self.layout.clusters[last_run.cluster_range.end - 1]; + let cluster = &self.layout.data.clusters[last_run.cluster_range.end - 1]; if cluster.info.whitespace().is_space_or_nbsp() { line.metrics.trailing_whitespace = cluster.advance; } @@ -548,7 +552,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { if !line.item_range.is_empty() { let line_item = &self.lines.line_items[line.item_range.start]; if line_item.is_text_run() { - let run = &self.layout.runs[line_item.index]; + let run = &self.layout.data.runs[line_item.index]; line.metrics.ascent = run.metrics.ascent; line.metrics.descent = run.metrics.descent; line.metrics.leading = run.metrics.leading; @@ -588,9 +592,9 @@ impl<'a, B: Brush> Drop for BreakLines<'a, B> { } // Save the computed widths/height to the layout - self.layout.width = width; - self.layout.full_width = full_width; - self.layout.height = height; + self.layout.data.width = width; + self.layout.data.full_width = full_width; + self.layout.data.height = height; // for (i, line) in self.lines.lines.iter().enumerate() { // println!("LINE {i}"); @@ -601,7 +605,7 @@ impl<'a, B: Brush> Drop for BreakLines<'a, B> { // } // Save the computed lines to the layout - self.lines.swap(self.layout); + self.lines.swap(&mut self.layout.data); } } @@ -653,23 +657,23 @@ impl<'a, B: Brush> Drop for BreakLines<'a, B> { // } fn try_commit_line( - layout: &LayoutData, + layout: &Layout, lines: &mut LineLayout, state: &mut LineState, max_advance: f32, alignment: Alignment, break_reason: BreakReason, ) -> bool { - let is_empty = layout.text_len == 0; + let is_empty = layout.data.text_len == 0; // Ensure that the cluster and item endpoints are within range - state.clusters.end = state.clusters.end.min(layout.clusters.len()); - state.items.end = state.items.end.min(layout.items.len()); + state.clusters.end = state.clusters.end.min(layout.data.clusters.len()); + state.items.end = state.items.end.min(layout.data.items.len()); let start_item_idx = lines.line_items.len(); // let start_run_idx = lines.line_items.last().map(|item| item.index).unwrap_or(0); - let items_to_commit = &layout.items[state.items.clone()]; + let items_to_commit = &layout.data.items[state.items.clone()]; // Compute first and last run index let is_text_run = |item: &LayoutItem| item.kind == LayoutItemKind::TextRun; @@ -692,7 +696,7 @@ fn try_commit_line( match item.kind { LayoutItemKind::InlineBox => { - let inline_box = &layout.inline_boxes[item.index]; + let inline_box = &layout.data.inline_boxes[item.index]; lines.line_items.push(LineItemData { kind: LayoutItemKind::InlineBox, @@ -708,7 +712,7 @@ fn try_commit_line( }); } LayoutItemKind::TextRun => { - let run_data = &layout.runs[item.index]; + let run_data = &layout.data.runs[item.index]; // Compute cluster range // The first and last ranges have overrides to account for line-breaks within runs @@ -730,7 +734,7 @@ fn try_commit_line( } // Push run to line - let run = Run::new(layout, run_data, None); + let run = Run::new(layout, 0, 0, run_data, None); let text_range = if run_data.cluster_range.is_empty() { 0..0 } else { diff --git a/parley/src/layout/line/mod.rs b/parley/src/layout/line/mod.rs index f62c085c..2fcfcc75 100644 --- a/parley/src/layout/line/mod.rs +++ b/parley/src/layout/line/mod.rs @@ -32,22 +32,25 @@ impl<'a, B: Brush> Line<'a, B> { if index >= self.data.item_range.end { return None; } - let item = self.layout.line_items.get(index)?; + let item = self.layout.data.line_items.get(index)?; Some(item) } /// Returns the run at the specified index. pub fn run(&self, index: usize) -> Option> { + let original_index = index; let index = self.data.item_range.start + index; if index >= self.data.item_range.end { return None; } - let item = self.layout.line_items.get(index)?; + let item = self.layout.data.line_items.get(index)?; if item.kind == LayoutItemKind::TextRun { Some(Run { layout: self.layout, - data: self.layout.runs.get(item.index)?, + line_index: self.index, + index: original_index as u32, + data: self.layout.data.runs.get(item.index)?, line_data: Some(item), }) } else { @@ -59,13 +62,16 @@ impl<'a, B: Brush> Line<'a, B> { // TODO: provide iterator over inline_boxes and items pub fn runs(&self) -> impl Iterator> + 'a + Clone { let copy = self.clone(); - let line_items = ©.layout.line_items[self.data.item_range.clone()]; + let line_items = ©.layout.data.line_items[self.data.item_range.clone()]; line_items .iter() - .filter(|item| item.kind == LayoutItemKind::TextRun) - .map(move |line_data| Run { + .enumerate() + .filter(|(_, item)| item.kind == LayoutItemKind::TextRun) + .map(move |(index, line_data)| Run { layout: copy.layout, - data: ©.layout.runs[line_data.index], + line_index: copy.index, + index: index as u32, + data: ©.layout.data.runs[line_data.index], line_data: Some(line_data), }) } @@ -216,7 +222,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { let item = self.line.item(self.item_index)?; match item.kind { LayoutItemKind::InlineBox => { - let inline_box = &self.line.layout.inline_boxes[item.index]; + let inline_box = &self.line.layout.data.inline_boxes[item.index]; let x = self.offset + self.line.data.metrics.offset; @@ -247,7 +253,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { glyph_count += 1; advance += glyph.advance; } - let style = run.layout.styles.get(style_index)?; + let style = run.layout.data.styles.get(style_index)?; let glyph_start = self.glyph_start; self.glyph_start += glyph_count; let offset = self.offset; diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 96d09bb5..7633df82 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -89,7 +89,8 @@ impl Layout { /// Returns the line at the specified index. pub fn get(&self, index: usize) -> Option> { Some(Line { - layout: &self.data, + index: index as u32, + layout: self, data: self.data.lines.get(index)?, }) } @@ -109,21 +110,26 @@ impl Layout { /// Returns an iterator over the lines in the layout. pub fn lines(&self) -> impl Iterator> + '_ + Clone { - self.data.lines.iter().map(move |data| Line { - layout: &self.data, - data, - }) + self.data + .lines + .iter() + .enumerate() + .map(move |(index, data)| Line { + index: index as u32, + layout: self, + data, + }) } /// Returns line breaker to compute lines for the layout. pub fn break_lines(&mut self) -> BreakLines { - BreakLines::new(&mut self.data) + BreakLines::new(self) } /// Breaks all lines with the specified maximum advance. - pub fn break_all_lines(&mut self, max_advance: Option, alignment: Alignment) { + pub fn break_all_lines(&mut self, max_advance: Option) { self.break_lines() - .break_remaining(max_advance.unwrap_or(f32::MAX), alignment); + .break_remaining(max_advance.unwrap_or(f32::MAX)); } // Apply to alignment to layout relative to the specified container width. If container_width is not @@ -132,15 +138,6 @@ impl Layout { align(&mut self.data, container_width, alignment); } - /// Returns an iterator over the runs in the layout. - pub fn runs(&self) -> impl Iterator> + '_ + Clone { - self.data.runs.iter().map(move |data| Run { - layout: &self.data, - data, - line_data: None, - }) - } - /// Returns the index and `Line` object for the line containing the /// given byte `index` in the source text. pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line)> { @@ -197,7 +194,9 @@ impl Default for Layout { /// Sequence of clusters with a single font and style. #[derive(Copy, Clone)] pub struct Run<'a, B: Brush> { - layout: &'a LayoutData, + layout: &'a Layout, + line_index: u32, + index: u32, data: &'a RunData, line_data: Option<&'a LineItemData>, } @@ -205,6 +204,7 @@ pub struct Run<'a, B: Brush> { /// Atomic unit of text. #[derive(Copy, Clone)] pub struct Cluster<'a, B: Brush> { + path: ClusterPath, run: Run<'a, B>, data: &'a ClusterData, } @@ -229,7 +229,8 @@ impl Glyph { /// Line in a text layout. #[derive(Copy, Clone)] pub struct Line<'a, B: Brush> { - layout: &'a LayoutData, + layout: &'a Layout, + index: u32, data: &'a LineData, } diff --git a/parley/src/layout/run.rs b/parley/src/layout/run.rs index 9e7f3b6a..147232f8 100644 --- a/parley/src/layout/run.rs +++ b/parley/src/layout/run.rs @@ -5,12 +5,16 @@ use super::*; impl<'a, B: Brush> Run<'a, B> { pub(crate) fn new( - layout: &'a LayoutData, + layout: &'a Layout, + line_index: u32, + index: u32, data: &'a RunData, line_data: Option<&'a LineItemData>, ) -> Self { Self { layout, + line_index, + index, data, line_data, } @@ -18,7 +22,7 @@ impl<'a, B: Brush> Run<'a, B> { /// Returns the font for the run. pub fn font(&self) -> &Font { - self.layout.fonts.get(self.data.font_index).unwrap() + self.layout.data.fonts.get(self.data.font_index).unwrap() } /// Returns the font size for the run. @@ -35,6 +39,7 @@ impl<'a, B: Brush> Run<'a, B> { /// with the run. pub fn normalized_coords(&self) -> &[NormalizedCoord] { self.layout + .data .coords .get(self.data.coords_range.clone()) .unwrap_or(&[]) @@ -89,10 +94,12 @@ impl<'a, B: Brush> Run<'a, B> { .line_data .map(|d| &d.cluster_range) .unwrap_or(&self.data.cluster_range); + let original_index = index; let index = range.start + index; Some(Cluster { + path: ClusterPath::new(self.line_index, self.index, original_index as u32), run: self.clone(), - data: self.layout.clusters.get(index)?, + data: self.layout.data.clusters.get(index)?, }) } @@ -175,8 +182,13 @@ impl<'a, B: Brush> Iterator for Clusters<'a, B> { self.range.next()? }; Some(Cluster { + path: ClusterPath::new( + self.run.line_index, + self.run.index, + (index - self.run.cluster_range().start) as u32, + ), run: self.run.clone(), - data: self.run.layout.clusters.get(index)?, + data: self.run.layout.data.clusters.get(index)?, }) } } From abc9da8c5ddf7bf29a574d800c36ea1320159fb4 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 10:56:47 -0400 Subject: [PATCH 11/18] remove .vscode directory --- .vscode/launch.json | 157 -------------------------------------------- 1 file changed, 157 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 7ff31f9f..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'fontique'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=fontique" - ], - "filter": { - "name": "fontique", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'parley'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=parley" - ], - "filter": { - "name": "parley", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'tiny_skia_render'", - "cargo": { - "args": [ - "build", - "--bin=tiny_skia_render", - "--package=tiny_skia_render" - ], - "filter": { - "name": "tiny_skia_render", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'tiny_skia_render'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=tiny_skia_render", - "--package=tiny_skia_render" - ], - "filter": { - "name": "tiny_skia_render", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'swash_render'", - "cargo": { - "args": [ - "build", - "--bin=swash_render", - "--package=swash_render" - ], - "filter": { - "name": "swash_render", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'swash_render'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=swash_render", - "--package=swash_render" - ], - "filter": { - "name": "swash_render", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'vello_editor'", - "cargo": { - "args": [ - "build", - "--bin=vello_editor", - "--package=vello_editor" - ], - "filter": { - "name": "vello_editor", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'vello_render'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=vello_render", - "--package=vello_render" - ], - "filter": { - "name": "vello_render", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file From d72da0d5dfe94cb93ab7b45f87830b94de96170a Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 10:58:52 -0400 Subject: [PATCH 12/18] use system-ui font --- examples/vello_editor/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index b726b470..00792fe9 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -54,7 +54,7 @@ impl Editor { builder.push_default(&parley::style::StyleProperty::FontSize(32.0)); builder.push_default(&parley::style::StyleProperty::LineHeight(1.2)); builder.push_default(&parley::style::StyleProperty::FontStack( - parley::style::FontStack::Source("verdana"), + parley::style::FontStack::Source("system-ui"), )); builder.build_into(&mut self.layout); self.layout.break_all_lines(Some(width - INSET * 2.0)); From 8e4b1997222ba960450e26eb4363fffcabd2de05 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 11:08:14 -0400 Subject: [PATCH 13/18] make no_std happy --- parley/src/layout/cursor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index f022778d..421fc3ce 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -4,6 +4,7 @@ //! Text selection support. use super::{Affinity, BreakReason, Brush, Cluster, ClusterPath, Layout}; +use alloc::vec::Vec; use core::ops::Range; use peniko::kurbo::Rect; From c352505dbff76e6a4bd0f4b11a681d9420b4ae6b Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 11:18:06 -0400 Subject: [PATCH 14/18] use cmd on macos; add cut command --- examples/vello_editor/src/text.rs | 47 +++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 00792fe9..8c70ff0e 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -91,19 +91,50 @@ impl Editor { if !event.state.is_pressed() { return; } - let (shift, ctrl) = self + #[allow(unused)] + let (shift, ctrl, cmd) = self .modifiers - .map(|mods| (mods.state().shift_key(), mods.state().control_key())) + .map(|mods| { + ( + mods.state().shift_key(), + mods.state().control_key(), + mods.state().super_key(), + ) + }) .unwrap_or_default(); + #[cfg(target_os = "macos")] + let action_mod = cmd; + #[cfg(not(target_os = "macos"))] + let action_mod = ctrl; if let PhysicalKey::Code(code) = event.physical_key { match code { - KeyCode::KeyC if ctrl => { - let text = &self.buffer[self.selection.text_range()]; - let mut cb: clipboard::ClipboardContext = - ClipboardProvider::new().unwrap(); - cb.set_contents(text.to_owned()).ok(); + KeyCode::KeyC if action_mod => { + if !self.selection.is_collapsed() { + let text = &self.buffer[self.selection.text_range()]; + let mut cb: clipboard::ClipboardContext = + ClipboardProvider::new().unwrap(); + cb.set_contents(text.to_owned()).ok(); + } + } + KeyCode::KeyX if action_mod => { + if !self.selection.is_collapsed() { + let text = &self.buffer[self.selection.text_range()]; + let mut cb: clipboard::ClipboardContext = + ClipboardProvider::new().unwrap(); + cb.set_contents(text.to_owned()).ok(); + if let Some(start) = self.delete_current_selection() { + self.update_layout(self.width, 1.0); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = + Selection::from_index(&self.layout, start, affinity); + } + } } - KeyCode::KeyV if ctrl => { + KeyCode::KeyV if action_mod => { let mut cb: clipboard::ClipboardContext = ClipboardProvider::new().unwrap(); let text = cb.get_contents().unwrap_or_default(); From 063935730a21b516af10bd8acd6f118e8841d839 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 11:32:12 -0400 Subject: [PATCH 15/18] try clipboard_rs --- Cargo.lock | 728 +++++++++++++++++++++++++++--- examples/vello_editor/Cargo.toml | 2 +- examples/vello_editor/src/text.rs | 17 +- 3 files changed, 681 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73316a69..2563a945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "allocator-api2" version = "0.2.18" @@ -85,6 +91,23 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -124,6 +147,29 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -139,6 +185,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -151,6 +203,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitstream-io" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" + [[package]] name = "block" version = "0.1.6" @@ -166,6 +224,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "built" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" + [[package]] name = "bumpalo" version = "3.16.0" @@ -198,6 +262,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.7.1" @@ -247,6 +317,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -266,25 +346,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "clipboard" -version = "0.5.0" +name = "clipboard-rs" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +checksum = "c718c0784ab208e35cc764e8b3f864caf5cd82a65d793fb99ddb3565ae028726" dependencies = [ "clipboard-win", - "objc", - "objc-foundation", - "objc_id", - "x11-clipboard", + "cocoa", + "image", + "x11rb", ] [[package]] name = "clipboard-win" -version = "2.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ - "winapi", + "error-code", + "windows-win", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", ] [[package]] @@ -297,6 +407,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.6.0" @@ -417,12 +533,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "cursor-icon" version = "1.1.0" @@ -501,6 +642,12 @@ dependencies = [ "wio", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -517,6 +664,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + [[package]] name = "euclid" version = "0.22.10" @@ -526,6 +679,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -545,6 +714,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "font-types" version = "0.5.5" @@ -653,6 +831,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -747,6 +935,16 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -772,6 +970,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -880,16 +1084,43 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "num-traits", "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indexmap" version = "2.4.0" @@ -900,6 +1131,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "jni" version = "0.21.1" @@ -931,6 +1182,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.70" @@ -974,12 +1231,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1051,6 +1325,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1060,6 +1343,15 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1090,6 +1382,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1160,6 +1458,69 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1199,17 +1560,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -1413,15 +1763,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -1567,6 +1908,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "presser" version = "0.3.1" @@ -1579,7 +1929,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -1596,6 +1946,34 @@ name = "profiling" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.65", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" @@ -1615,18 +1993,117 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "range-alloc" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.19.3" @@ -1661,6 +2138,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rgb" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +dependencies = [ + "bytemuck", +] + [[package]] name = "roxmltree" version = "0.19.0" @@ -1740,6 +2226,15 @@ dependencies = [ "syn 2.0.65", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1752,6 +2247,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "skrifa" version = "0.19.3" @@ -1821,6 +2325,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -1909,6 +2422,25 @@ dependencies = [ "syn 2.0.65", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "termcolor" version = "1.4.1" @@ -1938,6 +2470,17 @@ dependencies = [ "syn 2.0.65", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -1984,11 +2527,26 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.20", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1998,7 +2556,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.18", ] [[package]] @@ -2053,6 +2624,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vello" version = "0.2.1" @@ -2077,7 +2659,7 @@ name = "vello_editor" version = "0.1.0" dependencies = [ "anyhow", - "clipboard", + "clipboard-rs", "parley", "peniko", "pollster", @@ -2110,6 +2692,12 @@ dependencies = [ "vello_encoding", ] +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -2328,6 +2916,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" version = "0.20.1" @@ -2566,6 +3160,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-win" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e23e33622b3b52f948049acbec9bcc34bf6e26d74176b88941f213c75cf2dc" +dependencies = [ + "error-code", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2759,6 +3362,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "wio" version = "0.2.2" @@ -2774,15 +3386,6 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" -[[package]] -name = "x11-clipboard" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" -dependencies = [ - "xcb", -] - [[package]] name = "x11-dl" version = "2.21.0" @@ -2815,16 +3418,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" -[[package]] -name = "xcb" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" -dependencies = [ - "libc", - "log", -] - [[package]] name = "xcursor" version = "0.3.8" @@ -2898,6 +3491,7 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -2954,3 +3548,27 @@ dependencies = [ "quote", "syn 2.0.65", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/examples/vello_editor/Cargo.toml b/examples/vello_editor/Cargo.toml index 8c251b51..22ad78d0 100644 --- a/examples/vello_editor/Cargo.toml +++ b/examples/vello_editor/Cargo.toml @@ -13,7 +13,7 @@ pollster = "0.3.0" winit = "0.30.3" parley = { workspace = true, default-features = true } peniko = { workspace = true } -clipboard = "0.5.0" +clipboard-rs = "0.1.11" [lints] workspace = true diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 8c70ff0e..c7b3dc28 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -1,7 +1,7 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use clipboard::ClipboardProvider; +use clipboard_rs::{Clipboard, ClipboardContext}; use parley::layout::cursor::{Selection, VisualMode}; use parley::layout::Affinity; use parley::{layout::PositionedLayoutItem, FontContext}; @@ -111,17 +111,15 @@ impl Editor { KeyCode::KeyC if action_mod => { if !self.selection.is_collapsed() { let text = &self.buffer[self.selection.text_range()]; - let mut cb: clipboard::ClipboardContext = - ClipboardProvider::new().unwrap(); - cb.set_contents(text.to_owned()).ok(); + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); } } KeyCode::KeyX if action_mod => { if !self.selection.is_collapsed() { let text = &self.buffer[self.selection.text_range()]; - let mut cb: clipboard::ClipboardContext = - ClipboardProvider::new().unwrap(); - cb.set_contents(text.to_owned()).ok(); + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); if let Some(start) = self.delete_current_selection() { self.update_layout(self.width, 1.0); let (start, affinity) = if start > 0 { @@ -135,9 +133,8 @@ impl Editor { } } KeyCode::KeyV if action_mod => { - let mut cb: clipboard::ClipboardContext = - ClipboardProvider::new().unwrap(); - let text = cb.get_contents().unwrap_or_default(); + let cb = ClipboardContext::new().unwrap(); + let text = cb.get_text().unwrap_or_default(); let start = self .delete_current_selection() .unwrap_or_else(|| self.selection.focus().text_range().start); From 66979c500e393945e743b30f96501c5838a5725f Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 21 Aug 2024 11:44:54 -0400 Subject: [PATCH 16/18] ignore spurious cusor move on macos --- examples/vello_editor/src/text.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index c7b3dc28..deef658e 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -269,8 +269,10 @@ impl Editor { } } WindowEvent::CursorMoved { position, .. } => { + let prev_pos = self.cursor_pos; self.cursor_pos = (position.x as f32 - INSET, position.y as f32 - INSET); - if self.pointer_down { + // macOS seems to generate a spurious move after selecting word? + if self.pointer_down && prev_pos != self.cursor_pos { self.selection = self.selection.extend_to_point( &self.layout, self.cursor_pos.0, From d02765a7c5389232b30d3ec3a5b23ea2f717a8b8 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 22 Aug 2024 13:22:59 -0400 Subject: [PATCH 17/18] review feedback - support ctrl+home/end - fix delete with non-collapsed selections - move to start/end of line when maxing out up/down respectively - fix panic on empty text buffer - proper cursor placement when using left/right to collapse selection - fix weird cursor offset bug when line ends with RTL run containing only whitespace that is reordered - triple click to select line --- .typos.toml | 10 +++ examples/vello_editor/Cargo.toml | 7 +- examples/vello_editor/src/text.rs | 134 ++++++++++++++++++++---------- parley/src/layout/cluster.rs | 19 ++--- parley/src/layout/cursor.rs | 39 ++++++--- parley/src/layout/line/greedy.rs | 12 +-- 6 files changed, 145 insertions(+), 76 deletions(-) diff --git a/.typos.toml b/.typos.toml index 98e24772..d43c5c85 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,6 +6,16 @@ # word is treated as always correct. If the value is an empty string, the word # is treated as always incorrect. +[default] +extend-ignore-re = [ + # Matches lorem ipsum text. + # In general, regexes are only matched until the end of a line by typos, + # and the repeated matcher at the end of both of these also ensures that + # matching ends at quotes or symbols commonly used to terminate comments. + "Lorem ipsum [a-zA-Z .,]*", + "Phasellus in viverra dolor [a-zA-Z .,]*", +] + # Match Identifier - Case Sensitive [default.extend-identifiers] Beng = "Beng" diff --git a/examples/vello_editor/Cargo.toml b/examples/vello_editor/Cargo.toml index 22ad78d0..472d3ce2 100644 --- a/examples/vello_editor/Cargo.toml +++ b/examples/vello_editor/Cargo.toml @@ -13,7 +13,12 @@ pollster = "0.3.0" winit = "0.30.3" parley = { workspace = true, default-features = true } peniko = { workspace = true } -clipboard-rs = "0.1.11" [lints] workspace = true + +[target.'cfg(target_os = "android")'.dependencies] +winit = { version = "0.30.3", features = ["android-native-activity"] } + +[target.'cfg(not(target_os = "android"))'.dependencies] +clipboard-rs = "0.1.11" diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index deef658e..7549bbde 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -63,6 +63,7 @@ impl Editor { self.width = width; } + #[allow(unused)] pub fn active_text(&self) -> ActiveText { if self.selection.is_collapsed() { let range = self @@ -70,14 +71,60 @@ impl Editor { .focus() .cluster_path() .cluster(&self.layout) - .unwrap() - .text_range(); + .map(|c| c.text_range()) + .unwrap_or_default(); ActiveText::FocusedCluster(self.selection.focus().affinity(), &self.buffer[range]) } else { ActiveText::Selection(&self.buffer[self.selection.text_range()]) } } + #[cfg(not(target_os = "android"))] + fn handle_clipboard(&mut self, code: KeyCode) { + match code { + KeyCode::KeyC => { + if !self.selection.is_collapsed() { + let text = &self.buffer[self.selection.text_range()]; + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + } + } + KeyCode::KeyX => { + if !self.selection.is_collapsed() { + let text = &self.buffer[self.selection.text_range()]; + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + if let Some(start) = self.delete_current_selection() { + self.update_layout(self.width, 1.0); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = Selection::from_index(&self.layout, start, affinity); + } + } + } + KeyCode::KeyV => { + let cb = ClipboardContext::new().unwrap(); + let text = cb.get_text().unwrap_or_default(); + let start = self + .delete_current_selection() + .unwrap_or_else(|| self.selection.focus().text_range().start); + self.buffer.insert_str(start, &text); + self.update_layout(self.width, 1.0); + self.selection = + Selection::from_index(&self.layout, start + text.len(), Affinity::default()); + } + _ => {} + } + } + + #[cfg(target_os = "android")] + fn handle_clipboard(&mut self, _code: KeyCode) { + // TODO: support clipboard on Android + } + pub fn handle_event(&mut self, event: &WindowEvent) { match event { WindowEvent::Resized(size) => { @@ -109,42 +156,13 @@ impl Editor { if let PhysicalKey::Code(code) = event.physical_key { match code { KeyCode::KeyC if action_mod => { - if !self.selection.is_collapsed() { - let text = &self.buffer[self.selection.text_range()]; - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - } + self.handle_clipboard(code); } KeyCode::KeyX if action_mod => { - if !self.selection.is_collapsed() { - let text = &self.buffer[self.selection.text_range()]; - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - if let Some(start) = self.delete_current_selection() { - self.update_layout(self.width, 1.0); - let (start, affinity) = if start > 0 { - (start - 1, Affinity::Upstream) - } else { - (start, Affinity::Downstream) - }; - self.selection = - Selection::from_index(&self.layout, start, affinity); - } - } + self.handle_clipboard(code); } KeyCode::KeyV if action_mod => { - let cb = ClipboardContext::new().unwrap(); - let text = cb.get_text().unwrap_or_default(); - let start = self - .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_range().start); - self.buffer.insert_str(start, &text); - self.update_layout(self.width, 1.0); - self.selection = Selection::from_index( - &self.layout, - start + text.len(), - Affinity::default(), - ); + self.handle_clipboard(code); } KeyCode::ArrowLeft => { self.selection = if ctrl { @@ -172,20 +190,35 @@ impl Editor { self.selection = self.selection.next_line(&self.layout, shift); } KeyCode::Home => { - self.selection = self.selection.line_start(&self.layout, shift); + if ctrl { + self.selection = + self.selection.move_lines(&self.layout, isize::MIN, shift); + } else { + self.selection = self.selection.line_start(&self.layout, shift); + } } KeyCode::End => { - self.selection = self.selection.line_end(&self.layout, shift); + if ctrl { + self.selection = + self.selection.move_lines(&self.layout, isize::MAX, shift); + } else { + self.selection = self.selection.line_end(&self.layout, shift); + } } KeyCode::Delete => { - if self.selection.is_collapsed() { + let start = if self.selection.is_collapsed() { let range = self.selection.focus().text_range(); + let start = range.start; self.buffer.replace_range(range, ""); + Some(start) } else { - self.delete_current_selection(); + self.delete_current_selection() }; - self.update_layout(self.width, 1.0); - self.selection = self.selection.refresh(&self.layout); + if let Some(start) = start { + self.update_layout(self.width, 1.0); + self.selection = + Selection::from_index(&self.layout, start, Affinity::default()); + } } KeyCode::Backspace => { let start = if self.selection.is_collapsed() { @@ -229,7 +262,7 @@ impl Editor { } } - println!("Active text: {:?}", self.active_text()); + // println!("Active text: {:?}", self.active_text()); } WindowEvent::MouseInput { state, button, .. } => { if *button == winit::event::MouseButton::Left { @@ -238,7 +271,7 @@ impl Editor { let now = Instant::now(); if let Some(last) = self.last_click_time.take() { if now.duration_since(last).as_secs_f64() < 0.25 { - self.click_count = (self.click_count + 1) % 3; + self.click_count = (self.click_count + 1) % 4; } else { self.click_count = 1; } @@ -248,14 +281,23 @@ impl Editor { self.last_click_time = Some(now); match self.click_count { 2 => { - println!("SELECTING WORD"); self.selection = Selection::word_from_point( &self.layout, self.cursor_pos.0, self.cursor_pos.1, ); } - // TODO: handle line + 3 => { + let focus = *Selection::from_point( + &self.layout, + self.cursor_pos.0, + self.cursor_pos.1, + ) + .line_start(&self.layout, true) + .focus(); + self.selection = + Selection::from(focus).line_end(&self.layout, true); + } _ => { self.selection = Selection::from_point( &self.layout, @@ -264,7 +306,7 @@ impl Editor { ); } } - println!("Active text: {:?}", self.active_text()); + // println!("Active text: {:?}", self.active_text()); } } } @@ -278,7 +320,7 @@ impl Editor { self.cursor_pos.0, self.cursor_pos.1, ); - println!("Active text: {:?}", self.active_text()); + // println!("Active text: {:?}", self.active_text()); } } _ => {} diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 876e407b..e56bc7fa 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -5,7 +5,7 @@ use super::*; impl<'a, B: Brush> Cluster<'a, B> { /// Returns the cluster for the given layout and byte index. - pub fn from_index(layout: &'a Layout, byte_index: usize) -> Self { + pub fn from_index(layout: &'a Layout, byte_index: usize) -> Option { let mut path = ClusterPath::default(); if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { path.line_index = line_index as u32; @@ -17,16 +17,16 @@ impl<'a, B: Brush> Cluster<'a, B> { for (cluster_index, cluster) in run.clusters().enumerate() { path.logical_index = cluster_index as u32; if cluster.text_range().contains(&byte_index) { - return path.cluster(layout).unwrap(); + return path.cluster(layout); } } } } - path.cluster(layout).unwrap() + path.cluster(layout) } /// Returns the cluster and affinity for the given layout and point. - pub fn from_point(layout: &'a Layout, x: f32, y: f32) -> (Self, Affinity) { + pub fn from_point(layout: &'a Layout, x: f32, y: f32) -> Option<(Self, Affinity)> { let mut path = ClusterPath::default(); if let Some((line_index, line)) = layout.line_for_offset(y) { path.line_index = line_index as u32; @@ -54,11 +54,11 @@ impl<'a, B: Brush> Cluster<'a, B> { } let affinity = Affinity::new(cluster.is_rtl(), x <= edge + cluster_advance * 0.5); - return (path.cluster(layout).unwrap(), affinity); + return Some((path.cluster(layout)?, affinity)); } } } - (path.cluster(layout).unwrap(), Affinity::default()) + Some((path.cluster(layout)?, Affinity::default())) } /// Returns the line that contains the cluster. @@ -205,7 +205,7 @@ impl<'a, B: Brush> Cluster<'a, B> { return None; } // We have to search for the cluster containing our end index - Some(Self::from_index(self.run.layout, index)) + Self::from_index(self.run.layout, index) } } @@ -220,10 +220,7 @@ impl<'a, B: Brush> Cluster<'a, B> { } .cluster(self.run.layout) } else { - Some(Self::from_index( - self.run.layout, - self.text_range().start.checked_sub(1)?, - )) + Self::from_index(self.run.layout, self.text_range().start.checked_sub(1)?) } } diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index 421fc3ce..ca3dd5c7 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -9,7 +9,7 @@ use core::ops::Range; use peniko::kurbo::Rect; /// Defines how a cursor will bind to a text position when moving visually. -#[derive(Copy, Clone, PartialEq, Default, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] pub enum VisualMode { /// During cursor motion, affinity is adjusted to prioritize the dominant /// direction of the layout. @@ -70,13 +70,17 @@ pub struct Cursor { impl Cursor { /// Returns a new cursor for the given layout, byte index and affinity. pub fn from_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - let cluster = Cluster::from_index(layout, index); + let Some(cluster) = Cluster::from_index(layout, index) else { + return Self::default(); + }; Self::from_cluster(cluster, affinity) } /// Creates a new cursor for the given layout and point. pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (cluster, affinity) = Cluster::from_point(layout, x, y); + let Some((cluster, affinity)) = Cluster::from_point(layout, x, y) else { + return Self::default(); + }; Self::from_cluster(cluster, affinity) } @@ -364,6 +368,11 @@ impl Cursor { } Self::from_cluster(next, Affinity::default()) } + + /// Used for determining visual order of two cursors. + fn visual_order_key(&self) -> (usize, f32) { + (self.path.line_index(), self.visual_offset) + } } /// A range within a layout. @@ -509,7 +518,7 @@ impl Selection { extend: bool, ) -> Self { if !extend && !self.is_collapsed() { - if self.focus.text_start > self.anchor.text_start { + if self.focus.visual_order_key() > self.anchor.visual_order_key() { return self.focus.into(); } else { return self.anchor.into(); @@ -531,7 +540,7 @@ impl Selection { extend: bool, ) -> Self { if !extend && !self.is_collapsed() { - if self.focus.text_start < self.anchor.text_start { + if self.focus.visual_order_key() < self.anchor.visual_order_key() { return self.focus.into(); } else { return self.anchor.into(); @@ -642,13 +651,19 @@ impl Selection { if delta == 0 { return *self; } - let line_index = self - .focus - .path - .line_index() - .saturating_add_signed(delta) - .min(layout.len().saturating_sub(1)); - let Some(line) = layout.get(line_index) else { + let line_limit = layout.len().saturating_sub(1); + let line_index = self.focus.path.line_index(); + let new_line_index = line_index.saturating_add_signed(delta); + if delta < 0 && line_index.checked_add_signed(delta).is_none() { + return self + .move_lines(layout, -(line_index as isize), extend) + .line_start(layout, extend); + } else if delta > 0 && new_line_index > line_limit { + return self + .move_lines(layout, (line_limit - line_index) as isize, extend) + .line_end(layout, extend); + } + let Some(line) = layout.get(new_line_index) else { return *self; }; let y = line.metrics().baseline - line.metrics().ascent * 0.5; diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index 0a75c378..3c831a7a 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -497,12 +497,6 @@ impl<'a, B: Brush> BreakLines<'a, B> { let line_height = line_item.compute_line_height(&self.layout.data); line.metrics.line_height = line.metrics.line_height.max(line_height); - // Ignore trailing whitespace for metrics computation - // (we are iterating backwards so trailing whitespace comes first) - if !have_metrics && line_item.is_whitespace { - continue; - } - // Compute the run's advance by summing the advances of its constituent clusters line_item.advance = self.layout.data.clusters [line_item.cluster_range.clone()] @@ -510,6 +504,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { .map(|c| c.advance) .sum(); + // Ignore trailing whitespace for metrics computation + // (we are iterating backwards so trailing whitespace comes first) + if !have_metrics && line_item.is_whitespace { + continue; + } + // Compute the run's vertical metrics line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); line.metrics.descent = line.metrics.descent.max(run.metrics.descent); From 3beb203c8595c8808a94d758d4911311f12627ee Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 22 Aug 2024 13:25:36 -0400 Subject: [PATCH 18/18] really fix android builds this time --- examples/vello_editor/src/text.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 7549bbde..897654a8 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -1,6 +1,7 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +#[cfg(not(target_os = "android"))] use clipboard_rs::{Clipboard, ClipboardContext}; use parley::layout::cursor::{Selection, VisualMode}; use parley::layout::Affinity;