From dce595fbc02daa674fcf4c9c781817033c393fa7 Mon Sep 17 00:00:00 2001 From: "dedal.qq" Date: Thu, 29 Oct 2026 21:13:10 +0300 Subject: [PATCH] commit --- Cargo.lock | 4693 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 + init.lua | 55 + src/api.rs | 310 +++ src/app.rs | 175 ++ src/constants.rs | 22 + src/editor.rs | 159 ++ src/keymap.rs | 352 +++ src/main.rs | 39 + src/script.rs | 121 + src/syntax.rs | 522 +++++ src/tests.rs | 646 ++++++ src/ui/code_editor.rs | 298 +++ src/ui/icons.rs | 93 + src/ui/mod.rs | 8 + src/ui/resize_handles.rs | 121 + src/ui/tabs.rs | 228 ++ src/ui/title_bar.rs | 121 + 18 files changed, 7983 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 init.lua create mode 100644 src/api.rs create mode 100644 src/app.rs create mode 100644 src/constants.rs create mode 100644 src/editor.rs create mode 100644 src/keymap.rs create mode 100644 src/main.rs create mode 100644 src/script.rs create mode 100644 src/syntax.rs create mode 100644 src/tests.rs create mode 100644 src/ui/code_editor.rs create mode 100644 src/ui/icons.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/resize_handles.rs create mode 100644 src/ui/tabs.rs create mode 100644 src/ui/title_bar.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bc73208 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3" +dependencies = [ + "accesskit", + "hashbrown 0.17.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", +] + +[[package]] +name = "accesskit_macos" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc" +dependencies = [ + "accesskit", + "accesskit_ios", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arboard" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.61.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[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.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.6", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.6", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.6", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.6", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.5", + "rustix 1.1.6", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "egui_system_font_provider", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster", + "profiling", + "raw-window-handle", + "send_wrapper", + "static_assertions", + "wasm-bindgen", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.13.2", + "emath", + "epaint", + "itertools", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", + "web-sys", +] + +[[package]] +name = "egui-rust-test" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "mlua", + "tree-sitter", + "tree-sitter-go", + "tree-sitter-javascript", + "tree-sitter-json", + "tree-sitter-python", + "tree-sitter-rust", +] + +[[package]] +name = "egui-wgpu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0309cbe1febed48af5b974e4bcc424b35e9664863a4d402697147b57a488c864" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.21", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de" +dependencies = [ + "accesskit_winit", + "arboard", + "block2 0.6.2", + "bytemuck", + "egui", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "profiling", + "winit", +] + +[[package]] +name = "egui_system_font_provider" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2a2fdfe3c05dd9f691d2b1388b2312d6dce1af502f217c22136d22d9bd76f6" +dependencies = [ + "epaint", + "fontique", + "log", + "poll-promise", + "skrifa", + "sys-locale", + "unicode-script", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "emath" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "epaint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types", + "harfrust", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa", + "smallvec", + "unicode-general-category", + "unicode-properties", + "unicode-segmentation", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontique" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts", + "roxmltree", + "smallvec", + "windows", + "windows-core", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.6", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glifo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "png", + "skrifa", + "smallvec", + "vello_common", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.2", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inventory" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "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", + "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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.5", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lua-src" +version = "551.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d400ffef0e3d4d29287092bdc5a276d0bf468b2c69709d5bab0d469312d9f947" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.7.4+c6ffc14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db548b001f0b7da416daf0ffae963912bae6cad1799fa9f4eaba09da6dba837" +dependencies = [ + "cc", + "which", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mlua" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7ae6b65bf54b7fdd4cf5b6f44f60862350ebf3fe3779b3a9cc005e7fa14f81" +dependencies = [ + "bstr", + "either", + "inventory", + "libc", + "mlua-sys", + "mlua_derive", + "num-traits", + "parking_lot", + "rustc-hash 2.1.3", +] + +[[package]] +name = "mlua-sys" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1631cda17bd6486410ab751d91f5fd4484f441589278cdad879eb3afc721a285" +dependencies = [ + "cc", + "cfg-if", + "libc", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[package]] +name = "mlua_derive" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4070bd35dad9477c94311c4668145bae8991b8accacf7201a77b14cab8a9e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[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.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "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.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parlance" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73072a9470554c8e794778acece2312661218aaabb635600957d952cf01c716e" + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "poll-promise" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df" +dependencies = [ + "document-features", + "static_assertions", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[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.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[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 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash 0.2.0", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.5", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.6", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[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.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[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.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[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", + "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 = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-go" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-json" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af592be68c579aa78a16846bd19422978c3c52e438523d45ff5d1bff1f9d4a" + +[[package]] +name = "tree-sitter-python" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vello_common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "log", + "peniko", + "png", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_cpu" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown 0.17.1", + "png", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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 = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.6", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.6", + "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.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.6", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +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 = "webbrowser" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "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 = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", + "windows-result", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "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 = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.6", + "thiserror 2.0.21", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.6", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.6", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.9", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..faa13bd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "egui-rust-test" +version = "0.1.0" +edition = "2024" + +[dependencies] +egui = "0.37" +eframe = { version = "0.37", features = ["wayland", "x11", "default_fonts"] } +mlua = { version = "0.12", features = ["lua54", "vendored", "macros"] } + +# tree-sitter и грамматики для подсветки синтаксиса. +tree-sitter = "0.25" +tree-sitter-rust = "0.24" +tree-sitter-go = "0.23" +tree-sitter-javascript = "0.23" +tree-sitter-json = "0.24" +tree-sitter-python = "0.23" + +[profile.dev] +opt-level = 1 diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..c81e6ba --- /dev/null +++ b/init.lua @@ -0,0 +1,55 @@ +-- Стартовый сценарий приложения. +-- +-- Приложение выполняет этот файл при запуске. Каталог поиска задаётся +-- константой SCRIPT_DIR в src/constants.rs (по умолчанию — текущий каталог). + +print("[lua] init.lua выполнен") + +-- Доступные методы объекта app (номера вкладок — с нуля): +-- app:tabs() -- {{ index = 0, title = "main.rs" }, ...} +-- app:active_tab() -- индекс активной вкладки +-- app:set_active_tab(index) -- true, если вкладка существует +-- app:next_tab() -- переключить на следующую, вернуть индекс +-- app:previous_tab() -- переключить на предыдущую, вернуть индекс +-- app:open_tab() -- открыть пустую вкладку, вернуть индекс +-- app:close_tab(index) -- true, если вкладка закрыта +-- app:active_text() -- текст активной вкладки +-- app:set_active_text(text) -- заменить текст активной вкладки +-- app:set_language(i, "rust") -- включить подсветку (rust|go|js|json|python) +-- -- для вкладки i; false — нет вкладки/языка +-- app:language(i) -- язык подсветки вкладки или nil +-- app:clear_language(i) -- выключить подсветку для вкладки +-- app:save() -- сохранить активную вкладку в её файл +-- app:map_key(keys, fn) -- обработчик клавиши/сочетания +-- -- "a" нажатие A +-- -- "1" нажатие 1 +-- -- "!a" отпускание A +-- -- "" C при зажатом Shift +-- -- "" Z при зажатом Ctrl +-- -- "" отпускание R при зажатом Alt +-- -- клавиша берётся по физическому коду: +-- -- Caps Lock и раскладка не важны +-- app:close() -- закрыть окно +-- app:toggle_maximize() +-- app:set_maximized(true/false) +-- app:minimize() +-- app:is_maximized() +-- +-- Методы добавляются в src/api.rs: всё, что есть в impl AppApi, помеченном +-- #[mlua::userdata_impl], автоматически доступно из Lua. +-- +-- Другие Lua-файлы можно подключать из каталога сценариев: +-- include("tools.lua") -- выполнить файл сразу +-- require("tools") -- выполнить один раз (как обычный Lua-модуль) +-- +-- Подсветка синтаксиса по умолчанию выключена. Включить её можно так: +-- app:set_language(0, "rust") + +app:set_language(0, "rust") +app:next_tab() + +-- Пример привязки: Ctrl+S сохраняет вкладку, отпускание Esc закрывает окно. +-- app:map_key("", function() app:save() end) +-- app:map_key("", function() app:close() end) + +app:map_key("", function() app:close() end) diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..20f5441 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,310 @@ +//! Глобальный API приложения. +//! +//! Единственная точка входа для команд управления окном и редактором: UI-код +//! не работает с `ViewportCommand` и состоянием редактора напрямую, а вызывает +//! методы [`AppApi`]. + +use std::cell::RefCell; +use std::rc::Rc; + +use eframe::egui; + +use crate::editor::EditorState; +use crate::keymap::{KeyBinding, KeyMap}; +use crate::syntax::Language; + +thread_local! { + /// Текущий экземпляр API. `thread_local`, потому что `egui::Context` привязан + /// к потоку с event loop; заодно это делает тесты независимыми друг от друга. + static INSTANCE: RefCell> = const { RefCell::new(None) }; +} + +/// Краткая информация об открытой вкладке. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TabInfo { + /// Номер вкладки (с нуля), слева направо. + pub index: usize, + /// Название — то же, что показано в заголовке вкладки. + pub title: String, +} + +/// Глобальный объект с функциями управления приложением. +/// +/// Использование: `AppApi::global().close()`, `AppApi::global().tabs()`. +/// Экземпляр устанавливается в начале каждого кадра (см. [`AppApi::install`]), +/// поэтому всегда указывает на актуальные `egui::Context` и состояние редактора. +/// +/// Методы этого `impl`-блока автоматически экспортируются в Lua через +/// `#[mlua::userdata_impl]`: в сценариях они доступны как `app:method(...)`. +/// Чтобы не экспортировать метод, пометьте его `#[lua(skip)]`. +#[derive(Clone, mlua::UserData)] +pub struct AppApi { + #[lua(skip)] + ctx: egui::Context, + #[lua(skip)] + editor: Rc>, + #[lua(skip)] + keymap: Rc>, +} + +#[mlua::userdata_impl] +impl AppApi { + /// Делает переданные контекст, состояние редактора и карту клавиш текущими + /// для [`AppApi::global`]. + #[lua(skip)] + pub fn install( + ctx: egui::Context, + editor: Rc>, + keymap: Rc>, + ) -> Self { + let api = Self { + ctx, + editor, + keymap, + }; + INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone())); + api + } + + /// Глобальный экземпляр API. + /// + /// # Panics + /// Если [`AppApi::install`] ещё не вызывался (ни одного кадра не отрисовано). + #[lua(skip)] + pub fn global() -> Self { + INSTANCE.with(|slot| { + slot.borrow() + .clone() + .expect("AppApi не инициализирован: сначала вызовите AppApi::install") + }) + } + + /// Контекст egui, к которому привязан API. + #[lua(skip)] + pub fn context(&self) -> &egui::Context { + &self.ctx + } + + /// Все открытые вкладки в порядке слева направо. + #[allow(dead_code)] + // Часть публичного API приложения: вызывается внешним кодом и тестами. + #[lua(infallible)] + pub fn tabs(&self) -> Vec { + self.editor + .borrow() + .documents() + .iter() + .enumerate() + .map(|(index, doc)| TabInfo { + index, + title: doc.name.clone(), + }) + .collect() + } + + /// Индекс активной вкладки в списке [`AppApi::tabs`]. + #[allow(dead_code)] + // Часть публичного API приложения: вызывается внешним кодом и тестами. + #[lua(infallible, name = "active_tab")] + pub fn active_tab_index(&self) -> usize { + self.editor.borrow().active_index() + } + + /// Переключает на следующую вкладку (по кругу) и возвращает её индекс. + #[allow(dead_code)] + // Часть публичного API приложения: вызывается внешним кодом и тестами. + #[lua(infallible)] + pub fn next_tab(&self) -> usize { + let mut editor = self.editor.borrow_mut(); + let next = (editor.active_index() + 1) % editor.documents().len(); + editor.set_active(next); + next + } + + /// Переключает на предыдущую вкладку (по кругу) и возвращает её индекс. + #[allow(dead_code)] + // Часть публичного API приложения: вызывается внешним кодом и тестами. + #[lua(infallible)] + pub fn previous_tab(&self) -> usize { + let mut editor = self.editor.borrow_mut(); + let count = editor.documents().len(); + let previous = (editor.active_index() + count - 1) % count; + editor.set_active(previous); + previous + } + + /// Закрывает вкладку по её номеру в списке [`AppApi::tabs`]. + /// + /// Возвращает `false`, если вкладки с таким номером нет. Если закрывается + /// последняя вкладка, вместо неё открывается пустой документ. + #[lua(infallible)] + pub fn close_tab(&self, index: usize) -> bool { + let mut editor = self.editor.borrow_mut(); + if index >= editor.documents().len() { + return false; + } + editor.close(index); + true + } + + /// Сохраняет содержимое активной вкладки в файл, названный как сама вкладка. + /// + /// Путь считается относительно текущего рабочего каталога процесса. + /// При успешной записи с вкладки снимается маркер несохранённых изменений. + #[lua(skip)] + pub fn save(&self) -> std::io::Result<()> { + let mut editor = self.editor.borrow_mut(); + let doc = editor.active_mut(); + std::fs::write(&doc.name, &doc.content)?; + doc.mark_saved(); + Ok(()) + } + + /// Lua-версия [`AppApi::save`]: ошибка ввода-вывода становится ошибкой Lua. + #[lua(name = "save")] + fn lua_save(&self) -> mlua::Result<()> { + self.save() + .map_err(|err| mlua::Error::RuntimeError(err.to_string())) + } + + /// Назначает обработчик на нажатие или отпускание клавиши. + /// + /// `keys` записывается в духе nvim: + /// + /// * `a`, `1` — нажатие клавиши без модификаторов; + /// * ``, ``, `` — нажатие с Shift, Ctrl или Alt; + /// * `!a` — отпускание клавиши; + /// * `` — отпускание клавиши при зажатом Alt. + /// + /// Клавиша определяется по физическому коду, поэтому Caps Lock и раскладка + /// не важны. Повторное назначение той же клавиши заменяет обработчик. + /// + /// # Errors + /// Возвращает ошибку Lua, если запись привязки некорректна. + pub fn map_key(&self, keys: String, callback: mlua::Function) -> mlua::Result<()> { + let binding = KeyBinding::parse(&keys).map_err(mlua::Error::RuntimeError)?; + self.keymap.borrow_mut().bind(binding, callback); + Ok(()) + } + + /// Содержимое активной вкладки. + #[lua(infallible)] + pub fn active_text(&self) -> String { + self.editor.borrow().active().content.clone() + } + + /// Заменяет содержимое активной вкладки. + #[lua(skip)] + pub fn set_active_text(&self, text: impl Into) { + self.editor.borrow_mut().active_mut().content = text.into(); + } + + /// Lua-версия [`AppApi::set_active_text`] (generic-методы не экспортируются). + #[lua(infallible, name = "set_active_text")] + fn lua_set_active_text(&self, text: String) { + self.set_active_text(text); + } + + /// Открывает новую пустую вкладку, делает её активной и возвращает её индекс. + #[lua(infallible)] + pub fn open_tab(&self) -> usize { + let mut editor = self.editor.borrow_mut(); + editor.open_new(); + editor.active_index() + } + + /// Делает вкладку активной по её номеру в списке [`AppApi::tabs`]. + /// + /// Возвращает `false`, если вкладки с таким номером нет. + #[lua(infallible)] + pub fn set_active_tab(&self, index: usize) -> bool { + let mut editor = self.editor.borrow_mut(); + if index >= editor.documents().len() { + return false; + } + editor.set_active(index); + true + } + + /// Включает подсветку синтаксиса для вкладки `index`. + /// + /// `language` — название языка: `rust`, `go`, `js`, `json` или `python` + /// (принимаются также `rs`, `golang`, `javascript`, `py`; регистр не важен). + /// Подсветка по умолчанию выключена и включается только этим методом. + /// + /// Возвращает `false`, если вкладки нет или язык не поддерживается. + #[lua(skip)] + pub fn set_language(&self, index: usize, language: &str) -> bool { + let Some(language) = Language::from_name(language) else { + return false; + }; + let mut editor = self.editor.borrow_mut(); + let Some(doc) = editor.document_mut(index) else { + return false; + }; + doc.language = Some(language); + true + } + + /// Lua-версия [`AppApi::set_language`]. + #[lua(infallible, name = "set_language")] + fn lua_set_language(&self, index: usize, language: String) -> bool { + self.set_language(index, &language) + } + + /// Язык подсветки вкладки: `"rust"`, `"go"`, `"js"`, `"json"`, `"python"` + /// или `nil`, если подсветка выключена / вкладки нет. + #[lua(infallible)] + pub fn language(&self, index: usize) -> Option { + self.editor + .borrow() + .document(index) + .and_then(|doc| doc.language) + .map(|language| language.name().to_owned()) + } + + /// Выключает подсветку синтаксиса для вкладки. + /// + /// Возвращает `false`, если вкладки с таким номером нет. + #[lua(infallible)] + pub fn clear_language(&self, index: usize) -> bool { + let mut editor = self.editor.borrow_mut(); + let Some(doc) = editor.document_mut(index) else { + return false; + }; + doc.language = None; + true + } + + /// Закрывает окно приложения. + #[lua(infallible)] + pub fn close(&self) { + self.ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + + /// Разворачивает окно на весь экран или возвращает его прежний размер. + #[lua(infallible)] + pub fn toggle_maximize(&self) { + self.set_maximized(!self.is_maximized()); + } + + /// Разворачивает (`true`) или восстанавливает (`false`) окно. + #[lua(infallible)] + pub fn set_maximized(&self, maximized: bool) { + self.ctx + .send_viewport_cmd(egui::ViewportCommand::Maximized(maximized)); + } + + /// Сворачивает окно. + #[lua(infallible)] + pub fn minimize(&self) { + self.ctx + .send_viewport_cmd(egui::ViewportCommand::Minimized(true)); + } + + /// Развёрнуто ли окно прямо сейчас. + #[lua(infallible)] + pub fn is_maximized(&self) -> bool { + self.ctx.input(|i| i.viewport().maximized.unwrap_or(false)) + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f698994 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,175 @@ +//! Приложение: окно без системных рамок и его содержимое. + +use std::cell::RefCell; +use std::rc::Rc; + +use eframe::egui; + +use crate::api::AppApi; +use crate::constants::CORNER_RADIUS; +use crate::editor::EditorState; +use crate::keymap::KeyMap; +use crate::script::ScriptEngine; +use crate::ui::title_bar::TitleBarResponses; +use crate::ui::{code_editor, resize_handles, tabs, title_bar}; + +#[derive(Default)] +pub struct MyApp { + /// Открытые документы, вкладки над редактором и активная вкладка. + /// + /// Состояние общее с [`AppApi`] (`Rc>`), чтобы им можно было + /// управлять через `AppApi::global()`. + editor: Rc>, + /// Привязки клавиш и их Lua-обработчики. + /// + /// Лежит перед `scripts`, чтобы обработчики (ссылки на Lua-значения) + /// уничтожились раньше самого Lua-движка. + keymap: Rc>, + /// Движок Lua, созданный при старте. + /// + /// Хранится, чтобы глобальное окружение и загруженные модули жили, пока + /// работает приложение. + #[allow(dead_code)] + scripts: Option, +} + +impl eframe::App for MyApp { + /// Фон вне скруглённого окна должен быть полностью прозрачным, + /// иначе eframe зальёт его цветом по умолчанию и скругления не будет видно. + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + egui::Color32::TRANSPARENT.to_normalized_gamma_f32() + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + self.show(ui); + } +} + +impl MyApp { + /// Создаёт приложение и сразу выполняет стартовый Lua-сценарий. + /// + /// Ошибки чтения/выполнения сценария печатаются в stderr и не мешают + /// запуску приложения: без `init.lua` редактор просто работает как обычно. + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let mut app = Self::default(); + + // API доступен сценарию уже до первого кадра. + let api = AppApi::install(cc.egui_ctx.clone(), app.editor.clone(), app.keymap.clone()); + match ScriptEngine::new(api) { + Ok(engine) => { + if let Err(err) = engine.run_startup() { + eprintln!("Lua: {err}"); + } + app.scripts = Some(engine); + } + Err(err) => eprintln!("Lua: не удалось создать движок: {err}"), + } + + app + } + + /// Состояние редактора (доступ для тестов). + #[cfg(test)] + pub fn editor(&self) -> std::cell::Ref<'_, EditorState> { + self.editor.borrow() + } + + /// Всё содержимое окна. Вынесено из `eframe::App::ui`, чтобы можно было + /// вернуть наружу `Response`-ы заголовка (пригодится для проверок/тестов). + pub fn show(&mut self, ui: &mut egui::Ui) -> TitleBarResponses { + let ctx = ui.ctx().clone(); + + // Публикуем глобальный API на текущий кадр: дальше любой модуль может + // управлять приложением и редактором через `AppApi::global()`. + AppApi::install(ctx.clone(), self.editor.clone(), self.keymap.clone()); + + // Обработчики клавиш вызываем до отрисовки, чтобы изменения состояния + // успели попасть в текущий кадр. + self.dispatch_keys(&ctx); + + // Базовый фрейм для всего окна приложения со скруглением + let style = ui.style().clone(); + let panel_frame = egui::Frame::new() + .fill(style.visuals.window_fill()) // Фон из текущей темы + .corner_radius(egui::CornerRadius::same(CORNER_RADIUS)) // Скругление углов самого окна + .stroke(egui::Stroke::new( + 1.0, + style.visuals.widgets.noninteractive.bg_stroke.color, + )) + .inner_margin(0.0) // Чтобы заголовок плотно прижимался к краям + .outer_margin(0.0); + + let mut bar = None; + + egui::CentralPanel::default() + .frame(panel_frame) + .show(ui, |ui| { + // 1. Отрисовка интегрированного заголовка (Title Bar) + // с именем активного документа, как в обычном редакторе. + let title = self.editor.borrow().active().name.clone(); + bar = Some(title_bar::render(ui, &title)); + + // 2. Вкладки и редактор занимают всю оставшуюся площадь + self.show_workspace(ui); + }); + + // Контент прилегает к краям панели, поэтому рамку окна рисуем поверх него. + ui.painter().rect_stroke( + ctx.content_rect(), + egui::CornerRadius::same(CORNER_RADIUS), + egui::Stroke::new(1.0, style.visuals.widgets.noninteractive.bg_stroke.color), + egui::StrokeKind::Inside, + ); + + // 3. Невидимые зоны изменения размера по краям (после контента — значит, поверх него). + resize_handles::render(ui); + + bar.expect("CentralPanel всегда отрисовывает заголовок") + } + + /// Вызывает Lua-обработчики, совпавшие с событиями клавиатуры этого кадра. + fn dispatch_keys(&self, ctx: &egui::Context) { + // Заём карты заканчивается до вызова колбэков: обработчик может сам + // вызвать `app:map_key` и изменить привязки. + let callbacks = self.keymap.borrow().callbacks_for(ctx); + for callback in callbacks { + if let Err(err) = callback.call::<()>(()) { + eprintln!("Lua: ошибка в обработчике клавиши: {err}"); + } + } + } + + /// Полоса вкладок и редактор активного документа. + fn show_workspace(&mut self, ui: &mut egui::Ui) { + let mut focus_editor = false; + + // Ctrl+S пишет активную вкладку в файл с её названием. + if ui.input_mut(|input| input.consume_key(egui::Modifiers::COMMAND, egui::Key::S)) + && let Err(err) = AppApi::global().save() + { + eprintln!("Не удалось сохранить вкладку: {err}"); + } + + // Заём держим только на время отрисовки, чтобы действия из API + // (они берут тот же `RefCell`) не конфликтовали с ним. + let actions = { + let editor = self.editor.borrow(); + tabs::render(ui, editor.documents(), editor.active_index()) + }; + + if let Some(index) = actions.clicked_tab { + self.editor.borrow_mut().set_active(index); + focus_editor = true; + } + if let Some(index) = actions.closed_tab { + AppApi::global().close_tab(index); + } + if actions.add_clicked { + self.editor.borrow_mut().open_new(); + focus_editor = true; + } + + let mut editor = self.editor.borrow_mut(); + code_editor::render(ui, editor.active_mut(), focus_editor); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..d87a5a7 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,22 @@ +//! Константы оформления окна. + +/// Насколько сильно скругляются углы окна. +pub const CORNER_RADIUS: u8 = 12; + +/// Высота интегрированного заголовка. +pub const TITLE_BAR_HEIGHT: f32 = 32.0; + +/// Толщина невидимой зоны по краям окна, за которую можно менять размер. +pub const RESIZE_BORDER: f32 = 6.0; + +/// Высота полосы вкладок над редактором. +pub const TAB_BAR_HEIGHT: f32 = 34.0; + +/// Каталог, в котором приложение ищет Lua-сценарии. +/// +/// По умолчанию это текущий рабочий каталог процесса — именно там приложение +/// ожидает `init.lua`. +pub const SCRIPT_DIR: &str = "."; + +/// Имя стартового сценария, который выполняется при запуске приложения. +pub const STARTUP_SCRIPT: &str = "init.lua"; diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..d43669e --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,159 @@ +//! Документы редактора и состояние вкладок. + +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::syntax::{Language, SyntaxState}; + +/// Счётчик идентификаторов документов. +static NEXT_ID: AtomicU64 = AtomicU64::new(1); + +/// Открытый в редакторе документ. +pub struct Document { + /// Стабильный идентификатор, пока документ открыт. + /// + /// По нему `TextEdit` и `ScrollArea` хранят курсор, выделение и прокрутку, + /// поэтому переключение вкладок не сбрасывает состояние редактора. + pub id: u64, + pub name: String, + pub content: String, + /// Язык подсветки синтаксиса. + /// + /// `None` — подсветка выключена (поведение по умолчанию). Включается + /// только явно через `AppApi::set_language`, например из Lua-сценария. + pub language: Option, + /// Разбор tree-sitter и кэш подсветки для этого документа. + pub syntax: SyntaxState, + /// Содержимое на момент последнего «сохранения». + saved_content: String, +} + +impl Document { + pub fn new(name: impl Into, content: impl Into) -> Self { + let content = content.into(); + Self { + id: NEXT_ID.fetch_add(1, Ordering::Relaxed), + name: name.into(), + saved_content: content.clone(), + content, + language: None, + syntax: SyntaxState::default(), + } + } + + /// Есть ли изменения, которые ещё не сохранены. + pub fn is_dirty(&self) -> bool { + self.content != self.saved_content + } + + /// Помечает текущее содержимое сохранённым. + pub fn mark_saved(&mut self) { + self.saved_content = self.content.clone(); + } +} + +/// Открытые документы и активная вкладка. +/// +/// Инвариант: `documents` никогда не пуст — редактор всегда показывает хотя бы +/// один документ. Это позволяет `active`/`active_mut` не возвращать `Option`. +pub struct EditorState { + documents: Vec, + active: usize, + next_untitled: u32, +} + +impl Default for EditorState { + fn default() -> Self { + Self { + documents: vec![ + Document::new("main.rs", SAMPLE_RUST), + Document::new("notes.md", SAMPLE_NOTES), + ], + active: 0, + next_untitled: 0, + } + } +} + +impl EditorState { + pub fn documents(&self) -> &[Document] { + &self.documents + } + + pub fn active_index(&self) -> usize { + self.active + } + + pub fn active(&self) -> &Document { + &self.documents[self.active] + } + + pub fn active_mut(&mut self) -> &mut Document { + &mut self.documents[self.active] + } + + /// Документ по номеру вкладки (для API). + pub fn document(&self, index: usize) -> Option<&Document> { + self.documents.get(index) + } + + /// Изменяемый документ по номеру вкладки (для API). + pub fn document_mut(&mut self, index: usize) -> Option<&mut Document> { + self.documents.get_mut(index) + } + + /// Делает вкладку активной, если индекс существует. + pub fn set_active(&mut self, index: usize) { + if index < self.documents.len() { + self.active = index; + } + } + + /// Открывает пустую вкладку и делает её активной. + pub fn open_new(&mut self) { + self.next_untitled += 1; + self.documents.push(Document::new( + format!("untitled-{}.txt", self.next_untitled), + "", + )); + self.active = self.documents.len() - 1; + } + + /// Закрывает вкладку и выбирает соседнюю. + /// + /// Если закрывается последняя вкладка, вместо неё открывается пустой документ. + pub fn close(&mut self, index: usize) { + if index >= self.documents.len() { + return; + } + + self.documents.remove(index); + + if self.documents.is_empty() { + self.open_new(); + } else if index < self.active { + self.active -= 1; + } else if index == self.active { + self.active = self.active.min(self.documents.len() - 1); + } + } +} + +const SAMPLE_RUST: &str = r#"//! Демонстрационный файл. +// Правьте код прямо в редакторе: вкладки и нумерация строк уже здесь. + +fn main() { + let mut numbers = vec![3, 1, 2]; + numbers.sort(); + + for (i, n) in numbers.iter().enumerate() { + println!("{i}: {n}"); + } +} +"#; + +const SAMPLE_NOTES: &str = r#"# Заметки + +- «+» на панели вкладок открывает новый файл +- крестик на вкладке закрывает её +- Ctrl+S сохраняет вкладку в файл с её названием +"#; diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000..80cab0b --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,352 @@ +//! Привязки клавиш к Lua-обработчикам. +//! +//! Запись привязки немного напоминает nvim: +//! +//! * `a`, `1` — нажатие клавиши без модификаторов; +//! * ``, ``, `` — нажатие с Shift, Ctrl или Alt; +//! * `!a` — отпускание клавиши; +//! * `` — отпускание клавиши при зажатом Alt. +//! +//! Клавиша всегда определяется по физическому коду (`physical_key`), поэтому +//! Caps Lock и текущая раскладка клавиатуры на совпадение не влияют. + +use eframe::egui; + +/// Модификаторы, которые учитываются при совпадении привязки. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Modifiers { + pub shift: bool, + pub ctrl: bool, + pub alt: bool, +} + +impl Modifiers { + /// Все три модификатора должны совпасть точно: `` не сработает, + /// если вместе с Ctrl зажат ещё и Shift. + fn matches(self, pressed: &egui::Modifiers) -> bool { + self.shift == pressed.shift && self.ctrl == pressed.ctrl && self.alt == pressed.alt + } +} + +/// Разобранная привязка клавиши. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct KeyBinding { + /// Физическая клавиша (не зависит от раскладки и Caps Lock). + pub key: egui::Key, + pub modifiers: Modifiers, + /// `true` — обработчик вызывается при отпускании клавиши. + pub on_release: bool, +} + +impl KeyBinding { + /// Разбирает строку вида `a`, `!1`, `` или ``. + /// + /// # Errors + /// Возвращает текст ошибки, если строка пуста, сочетание не закрыто, + /// модификатор или клавиша неизвестны. + pub fn parse(spec: &str) -> Result { + let spec = spec.trim(); + if spec.is_empty() { + return Err("пустая привязка клавиши".to_owned()); + } + + if spec.starts_with('<') || spec.ends_with('>') { + if !(spec.starts_with('<') && spec.ends_with('>')) { + return Err(format!("незакрытое сочетание клавиш: {spec}")); + } + return Self::parse_chord(&spec[1..spec.len() - 1]); + } + + let (on_release, key) = split_release(spec); + Ok(Self { + key: parse_key(key)?, + modifiers: Modifiers::default(), + on_release, + }) + } + + /// Разбирает содержимое угловых скобок: модификаторы через `-`, последний + /// элемент — клавиша. Например, `S-c` или `A-!r`. + fn parse_chord(inner: &str) -> Result { + let tokens: Vec<&str> = inner.split('-').collect(); + if tokens.iter().any(|token| token.trim().is_empty()) { + return Err(format!("пустое сочетание клавиш: <{inner}>")); + } + + let (key_token, modifier_tokens) = tokens + .split_last() + .expect("split('-') всегда возвращает хотя бы один элемент"); + + let mut modifiers = Modifiers::default(); + for token in modifier_tokens { + match token.trim() { + "S" | "s" => modifiers.shift = true, + "C" | "c" => modifiers.ctrl = true, + "A" | "a" => modifiers.alt = true, + other => return Err(format!("неизвестный модификатор: {other}")), + } + } + + let (on_release, key) = split_release(key_token.trim()); + Ok(Self { + key: parse_key(key)?, + modifiers, + on_release, + }) + } + + /// Совпадает ли привязка с конкретным событием клавиатуры. + fn matches(&self, key: egui::Key, pressed: bool, modifiers: &egui::Modifiers) -> bool { + self.key == key && self.on_release == !pressed && self.modifiers.matches(modifiers) + } +} + +/// Отделяет `!` (событие отпускания) от имени клавиши. +fn split_release(token: &str) -> (bool, &str) { + match token.strip_prefix('!') { + Some(rest) => (true, rest.trim()), + None => (false, token), + } +} + +/// Превращает имя клавиши из привязки в [`egui::Key`]. +fn parse_key(token: &str) -> Result { + let token = token.trim(); + if token.is_empty() { + return Err("не указана клавиша".to_owned()); + } + + // Односимвольные клавиши (`a`, `1`, `-`) и точные имена (`F1`, `Space`). + if let Some(key) = egui::Key::from_name(token) { + return Ok(key); + } + + // Регистронезависимые имена: `space`, `enter`, `esc`, `pgup` и т. п. + if let Some(key) = alias_key(token) { + return Ok(key); + } + + // `f1` → `F1`. + if let Some(key) = egui::Key::from_name(&token.to_ascii_uppercase()) { + return Ok(key); + } + + Err(format!("неизвестная клавиша: {token}")) +} + +/// Имена клавиш в нижнем регистре, которых нет в [`egui::Key::from_name`]. +fn alias_key(token: &str) -> Option { + let name = match token.to_ascii_lowercase().as_str() { + "space" => "Space", + "tab" => "Tab", + "enter" | "return" | "cr" => "Enter", + "esc" | "escape" => "Escape", + "backspace" | "bs" => "Backspace", + "delete" | "del" => "Delete", + "insert" => "Insert", + "home" => "Home", + "end" => "End", + "pageup" | "pgup" => "PageUp", + "pagedown" | "pgdn" => "PageDown", + "up" | "arrowup" => "ArrowUp", + "down" | "arrowdown" => "ArrowDown", + "left" | "arrowleft" => "ArrowLeft", + "right" | "arrowright" => "ArrowRight", + _ => return None, + }; + egui::Key::from_name(name) +} + +/// Привязка вместе с Lua-функцией, которую нужно выполнить. +struct Binding { + binding: KeyBinding, + callback: mlua::Function, +} + +/// Все назначенные привязки клавиш. +#[derive(Default)] +pub struct KeyMap { + bindings: Vec, +} + +impl KeyMap { + /// Добавляет привязку. Повторное назначение той же клавиши заменяет + /// предыдущий обработчик. + pub fn bind(&mut self, binding: KeyBinding, callback: mlua::Function) { + if let Some(existing) = self + .bindings + .iter_mut() + .find(|existing| existing.binding == binding) + { + existing.callback = callback; + } else { + self.bindings.push(Binding { binding, callback }); + } + } + + /// Собирает обработчики, которые нужно вызвать для событий текущего кадра. + /// + /// Заём `KeyMap` здесь заканчивается: вызывать колбэки нужно уже после + /// возврата из метода, потому что обработчик может сам менять привязки + /// через `app:map_key`. + pub fn callbacks_for(&self, ctx: &egui::Context) -> Vec { + let key_events: Vec<(egui::Key, bool, egui::Modifiers)> = ctx.input(|input| { + input + .events + .iter() + .filter_map(|event| { + let egui::Event::Key { + key, + physical_key, + pressed, + modifiers, + .. + } = event + else { + return None; + }; + + // Физический код не зависит от раскладки и Caps Lock. + // Логическая клавиша — запасной вариант для платформ, + // где physical_key не заполняется. + Some((physical_key.unwrap_or(*key), *pressed, *modifiers)) + }) + .collect() + }); + + let mut callbacks = Vec::new(); + for (key, pressed, modifiers) in key_events { + for binding in &self.bindings { + if binding.binding.matches(key, pressed, &modifiers) { + callbacks.push(binding.callback.clone()); + } + } + } + + callbacks + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(spec: &str) -> KeyBinding { + KeyBinding::parse(spec).expect("привязка должна разбираться") + } + + #[test] + fn plain_keys_and_release() { + assert_eq!( + parse("a"), + KeyBinding { + key: egui::Key::A, + modifiers: Modifiers::default(), + on_release: false, + } + ); + assert_eq!( + parse("1"), + KeyBinding { + key: egui::Key::Num1, + modifiers: Modifiers::default(), + on_release: false, + } + ); + assert_eq!( + parse("!a"), + KeyBinding { + key: egui::Key::A, + modifiers: Modifiers::default(), + on_release: true, + } + ); + } + + #[test] + fn chords_with_modifiers() { + assert_eq!( + parse(""), + KeyBinding { + key: egui::Key::C, + modifiers: Modifiers { + shift: true, + ..Default::default() + }, + on_release: false, + } + ); + assert_eq!( + parse(""), + KeyBinding { + key: egui::Key::Z, + modifiers: Modifiers { + ctrl: true, + ..Default::default() + }, + on_release: false, + } + ); + assert_eq!( + parse(""), + KeyBinding { + key: egui::Key::R, + modifiers: Modifiers { + alt: true, + ..Default::default() + }, + on_release: true, + } + ); + assert_eq!( + parse(""), + KeyBinding { + key: egui::Key::X, + modifiers: Modifiers { + shift: true, + ctrl: true, + alt: true, + }, + on_release: false, + } + ); + } + + #[test] + fn invalid_bindings_are_rejected() { + for spec in ["", " ", "<", "", "!"] { + assert!( + KeyBinding::parse(spec).is_err(), + "строка {spec:?} должна быть отклонена" + ); + } + } + + #[test] + fn named_keys_are_case_insensitive() { + assert_eq!(parse("").key, egui::Key::Space); + assert_eq!(parse("").key, egui::Key::Enter); + assert_eq!(parse("").key, egui::Key::Escape); + assert_eq!(parse("").key, egui::Key::F1); + assert_eq!(parse("").key, egui::Key::PageDown); + } + + #[test] + fn matching_requires_exact_modifiers() { + let binding = parse(""); + let shift = egui::Modifiers { + shift: true, + ..Default::default() + }; + let shift_ctrl = egui::Modifiers { + shift: true, + ctrl: true, + ..Default::default() + }; + + assert!(binding.matches(egui::Key::C, true, &shift)); + assert!(!binding.matches(egui::Key::C, true, &shift_ctrl)); + assert!(!binding.matches(egui::Key::C, false, &shift)); + assert!(!binding.matches(egui::Key::V, true, &shift)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9bfb84b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +mod api; +mod app; +mod constants; +mod editor; +mod keymap; +mod script; +mod syntax; +mod ui; + +#[cfg(test)] +mod tests; + +use app::MyApp; +use eframe::egui; + +fn main() -> eframe::Result<()> { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([900.0, 600.0]) + .with_min_inner_size([420.0, 280.0]) + .with_decorations(false) // Отключаем стандартные рамки ОС + .with_transparent(true), // Прозрачность для скругления углов + // Аппаратный MSAA (сглаживание на уровне GPU): 0 — выключен, + // дальше степень двойки. Для плоского 2D-интерфейса egui разница + // почти незаметна, но если внутри появится 3D/сложные меши, + // это единственный «настоящий» переключатель сглаживания. + multisampling: 4, + // Дизеринг убирает полосы (banding) на градиентах и полупрозрачных + // заливках. Он включён по умолчанию, здесь оставлен явно. + dithering: true, + ..Default::default() + }; + + eframe::run_native( + "Custom Window 0.37", + options, + Box::new(|cc| Ok(Box::new(MyApp::new(cc)))), + ) +} diff --git a/src/script.rs b/src/script.rs new file mode 100644 index 0000000..1835f52 --- /dev/null +++ b/src/script.rs @@ -0,0 +1,121 @@ +//! Выполнение Lua-сценариев приложения. +//! +//! При запуске приложение выполняет [`STARTUP_SCRIPT`] из каталога [`SCRIPT_DIR`] +//! (см. [`ScriptEngine::run_startup`]). Скриптам доступны: +//! +//! * объект `app` — управление окном и редактором (методы [`AppApi`], вызываются +//! через двоеточие: `app:next_tab()`); +//! * функция `include(path)` — выполняет другой Lua-файл относительно [`SCRIPT_DIR`] +//! (или по абсолютному пути) в общем глобальном окружении; +//! * стандартный `require`, который также ищет модули в [`SCRIPT_DIR`]. + +use std::path::{Path, PathBuf}; + +use mlua::{Lua, Table}; + +use crate::api::{AppApi, TabInfo}; +use crate::constants::{SCRIPT_DIR, STARTUP_SCRIPT}; + +/// Вкладка превращается в Lua-таблицу `{ index = 0, title = "main.rs" }`. +impl mlua::IntoLua for TabInfo { + fn into_lua(self, lua: &Lua) -> mlua::Result { + let tab = lua.create_table()?; + tab.set("index", self.index)?; + tab.set("title", self.title)?; + Ok(mlua::Value::Table(tab)) + } +} + +/// Движок Lua и состояние его глобального окружения. +pub struct ScriptEngine { + lua: Lua, +} + +impl ScriptEngine { + /// Создаёт движок и публикует в нём API приложения. + pub fn new(api: AppApi) -> mlua::Result { + let engine = Self { lua: Lua::new() }; + engine.expose_app_api(api)?; + engine.configure_require()?; + Ok(engine) + } + + /// Выполняет стартовый сценарий из [`SCRIPT_DIR`]. + /// + /// # Errors + /// Возвращает ошибку, если файла нет, его не удалось прочитать или в нём + /// есть синтаксическая/исполняемая ошибка. + pub fn run_startup(&self) -> mlua::Result<()> { + self.run_file(&script_path(STARTUP_SCRIPT)) + } + + /// Выполняет Lua-файл в глобальном окружении движка. + pub fn run_file(&self, path: &Path) -> mlua::Result<()> { + run_file(&self.lua, path).map(|_| ()) + } + + /// Публикует объект `app` с функциями управления приложением. + /// + /// Все методы [`AppApi`] из `#[mlua::userdata_impl]`-блока доступны как + /// методы userdata: `app:next_tab()`, `app:set_active_text("...")`. + /// Номера вкладок всюду нумеруются с нуля. + fn expose_app_api(&self, api: AppApi) -> mlua::Result<()> { + self.lua + .globals() + .set("app", self.lua.create_userdata(api)?)?; + Ok(()) + } + + /// Добавляет каталог сценариев в `package.path`, чтобы `require` находил + /// соседние Lua-модули, и объявляет `include`. + fn configure_require(&self) -> mlua::Result<()> { + let globals = self.lua.globals(); + + if let Ok(package) = globals.get::("package") { + let current: String = package.get("path").unwrap_or_default(); + let dir = SCRIPT_DIR; + let extra = format!("{dir}/?.lua;{dir}/?/init.lua"); + let path = if current.is_empty() { + extra + } else { + format!("{extra};{current}") + }; + package.set("path", path)?; + } + + // include выполняет файл сразу, как dofile, но путь по умолчанию + // отсчитывается от каталога сценариев, а не от cwd. + globals.set( + "include", + self.lua + .create_function(|lua, path: String| run_file(lua, &script_path(&path)))?, + )?; + + Ok(()) + } +} + +/// Путь к файлу сценария: абсолютные пути используются как есть, остальные +/// отсчитываются от [`SCRIPT_DIR`]. +fn script_path(path: &str) -> PathBuf { + let path = Path::new(path); + if path.is_absolute() { + path.to_path_buf() + } else { + Path::new(SCRIPT_DIR).join(path) + } +} + +/// Читает и выполняет Lua-файл, возвращая значения, которые вернул чанк. +fn run_file(lua: &Lua, path: &Path) -> mlua::Result { + let source = std::fs::read_to_string(path).map_err(|err| { + mlua::Error::RuntimeError(format!( + "не удалось прочитать сценарий {}: {err}", + path.display() + )) + })?; + + lua.load(&source) + .set_name(path.to_string_lossy().into_owned()) + .eval() +} diff --git a/src/syntax.rs b/src/syntax.rs new file mode 100644 index 0000000..2beca30 --- /dev/null +++ b/src/syntax.rs @@ -0,0 +1,522 @@ +//! Интеграция tree-sitter: разбор текста и подсветка синтаксиса. +//! +//! Модуль не зависит от UI-кода: [`SyntaxState::parse`] возвращает обычное +//! дерево tree-sitter, поэтому его можно использовать не только для цветов, +//! но и для навигации по структуре кода, поиска узлов и проверки ошибок. +//! +//! Подсветка — это список байтовых диапазонов с [`HighlightStyle`]. Как именно +//! их раскрашивать, решает редактор (см. `ui::code_editor`). +//! +//! Поддерживаемые языки: Rust, Go, JavaScript, JSON, Python. Подсветка +//! выключена, пока для документа явно не выбран язык (см. `AppApi::set_language`). + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::sync::OnceLock; + +use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator, Tree}; + +/// Язык, для которого подключена грамматика tree-sitter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Language { + Rust, + Go, + JavaScript, + Json, + Python, +} + +impl Language { + /// Все поддерживаемые языки. + #[allow(dead_code)] + // Часть публичного интерфейса модуля: удобно для перебора языков снаружи. + pub const ALL: [Self; 5] = [ + Self::Rust, + Self::Go, + Self::JavaScript, + Self::Json, + Self::Python, + ]; + + /// Разбирает название языка из API/сценария. + /// + /// Основные имена — `rust`, `go`, `js`, `json`, `python`; короткие + /// (`rs`, `golang`, `javascript`, `py`) и регистр тоже принимаются. + pub fn from_name(name: &str) -> Option { + match name.trim().to_ascii_lowercase().as_str() { + "rust" | "rs" => Some(Self::Rust), + "go" | "golang" => Some(Self::Go), + "js" | "javascript" | "node" => Some(Self::JavaScript), + "json" => Some(Self::Json), + "python" | "py" => Some(Self::Python), + _ => None, + } + } + + /// Каноническое имя языка (то, что возвращает `AppApi::language`). + pub fn name(self) -> &'static str { + match self { + Self::Rust => "rust", + Self::Go => "go", + Self::JavaScript => "js", + Self::Json => "json", + Self::Python => "python", + } + } + + /// Грамматика tree-sitter для языка. + fn grammar(self) -> tree_sitter::Language { + match self { + Self::Rust => tree_sitter_rust::LANGUAGE.into(), + Self::Go => tree_sitter_go::LANGUAGE.into(), + Self::JavaScript => tree_sitter_javascript::LANGUAGE.into(), + Self::Json => tree_sitter_json::LANGUAGE.into(), + Self::Python => tree_sitter_python::LANGUAGE.into(), + } + } + + /// Запрос подсветки для языка. + fn query_source(self) -> &'static str { + match self { + Self::Rust => RUST_QUERY, + Self::Go => GO_QUERY, + Self::JavaScript => JS_QUERY, + Self::Json => JSON_QUERY, + Self::Python => PYTHON_QUERY, + } + } +} + +/// Семантическая категория участка кода. +/// +/// Редактор сам выбирает цвет для каждой категории, поэтому тема оформления +/// не зависит от грамматик. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HighlightStyle { + Keyword, + String, + Escape, + Number, + Comment, + Function, + Type, + Property, + Constant, + Operator, + Punctuation, + Variable, +} + +impl HighlightStyle { + /// Числовая метка для компактного буфера при разборе пересечений. + fn tag(self) -> u8 { + match self { + Self::Keyword => 1, + Self::String => 2, + Self::Escape => 3, + Self::Number => 4, + Self::Comment => 5, + Self::Function => 6, + Self::Type => 7, + Self::Property => 8, + Self::Constant => 9, + Self::Operator => 10, + Self::Punctuation => 11, + Self::Variable => 12, + } + } + + fn from_tag(tag: u8) -> Option { + Some(match tag { + 1 => Self::Keyword, + 2 => Self::String, + 3 => Self::Escape, + 4 => Self::Number, + 5 => Self::Comment, + 6 => Self::Function, + 7 => Self::Type, + 8 => Self::Property, + 9 => Self::Constant, + 10 => Self::Operator, + 11 => Self::Punctuation, + 12 => Self::Variable, + _ => return None, + }) + } + + /// Приоритет при совпадении диапазонов: более специфичная категория + /// должна перекрывать более общую (например, имя функции — свойство). + fn priority(self) -> u8 { + match self { + Self::Variable | Self::Punctuation | Self::Operator | Self::String => 1, + Self::Number | Self::Constant | Self::Property => 2, + Self::Escape | Self::Type => 3, + Self::Function => 4, + Self::Keyword | Self::Comment => 5, + } + } +} + +/// Участок текста с назначенным стилем (байтовые смещения, `end` не включён). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HighlightSpan { + pub start: usize, + pub end: usize, + pub style: HighlightStyle, +} + +/// Состояние разбора одного документа: переиспользуемый парсер и кэш подсветки. +/// +/// Кэш сравнивает язык и хеш текста, поэтому при наборе текста подсветка +/// пересчитывается, а в остальные кадры берётся готовый результат. +#[derive(Default)] +pub struct SyntaxState { + parser: Option, + cache: Option, +} + +impl SyntaxState { + /// Разбирает текст в дерево tree-sitter. + /// + /// Это универсальная точка входа: дерево можно обходить для поиска + /// определений, проверки ошибок и любых других задач, не связанных с + /// подсветкой. Возвращает `None`, если грамматика не установилась. + pub fn parse(&mut self, language: Language, text: &str) -> Option { + let parser = self.parser.get_or_insert_with(Parser::new); + if let Err(err) = parser.set_language(&language.grammar()) { + eprintln!("tree-sitter: не удалось установить язык {language:?}: {err}"); + return None; + } + parser.parse(text, None) + } + + /// Подсвеченные участки текста для языка. + /// + /// Результат кэшируется по языку и содержимому, поэтому вызывать метод + /// каждый кадр дёшево, пока текст не изменился. + pub fn spans(&mut self, language: Language, text: &str) -> &[HighlightSpan] { + let signature = Signature { + language, + len: text.len(), + hash: hash_text(text), + }; + + let fresh = self + .cache + .as_ref() + .is_some_and(|cache| cache.signature == signature); + + if !fresh { + // Подсветка строится поверх того же дерева, которое отдаёт `parse`, + // поэтому разбор всегда один. + let spans = self + .parse(language, text) + .map(|tree| highlight_tree(language, text, &tree)) + .unwrap_or_default(); + self.cache = Some(HighlightCache { signature, spans }); + } + + &self + .cache + .as_ref() + .expect("кэш заполняется выше при промахе") + .spans + } +} + +/// Ключ кэша подсветки. +#[derive(Clone, Copy, PartialEq, Eq)] +struct Signature { + language: Language, + len: usize, + hash: u64, +} + +struct HighlightCache { + signature: Signature, + spans: Vec, +} + +/// Сырой захват запроса до разрешения пересечений. +struct RawSpan { + start: usize, + end: usize, + style: HighlightStyle, +} + +impl RawSpan { + fn priority(&self) -> u8 { + self.style.priority() + } +} + +/// Считает участки подсветки по готовому дереву tree-sitter. +fn highlight_tree(language: Language, text: &str, tree: &Tree) -> Vec { + let Some(query) = query(language) else { + return Vec::new(); + }; + + let capture_names = query.capture_names(); + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(query, tree.root_node(), text.as_bytes()); + + let mut raw = Vec::new(); + while let Some(matched) = matches.next() { + for capture in matched.captures { + let Some(style) = style_for_capture(capture_names[capture.index as usize]) else { + continue; + }; + let range = capture.node.byte_range(); + if range.start < range.end && range.end <= text.len() { + raw.push(RawSpan { + start: range.start, + end: range.end, + style, + }); + } + } + } + drop(matches); + + resolve_overlaps(text, raw) +} + +/// Раскрашивает байты «сверху вниз»: сначала крупные участки, затем более +/// мелкие и приоритетные. Так вложенные escape-последовательности попадают +/// внутрь строк, а имя функции перекрывает свойство с тем же диапазоном. +fn resolve_overlaps(text: &str, mut raw: Vec) -> Vec { + raw.sort_by(|a, b| { + (b.end - b.start) + .cmp(&(a.end - a.start)) + .then(a.priority().cmp(&b.priority())) + }); + + let mut cells = vec![0u8; text.len()]; + for span in raw { + cells[span.start..span.end].fill(span.style.tag()); + } + + let mut spans = Vec::new(); + let mut index = 0; + while index < cells.len() { + let tag = cells[index]; + let start = index; + while index < cells.len() && cells[index] == tag { + index += 1; + } + if let Some(style) = HighlightStyle::from_tag(tag) { + spans.push(HighlightSpan { + start, + end: index, + style, + }); + } + } + spans +} + +/// Сопоставляет имя захвата запроса (`@function.method`, `@string`, ...) +/// с категорией стиля. +fn style_for_capture(name: &str) -> Option { + // Ключи JSON-объекта грамматика помечает как строки — рисуем их как свойства. + if name == "string.special.key" { + return Some(HighlightStyle::Property); + } + + let head = name.split('.').next().unwrap_or(name); + Some(match head { + "keyword" => HighlightStyle::Keyword, + "string" | "text" => HighlightStyle::String, + "escape" => HighlightStyle::Escape, + "number" => HighlightStyle::Number, + "comment" => HighlightStyle::Comment, + "function" | "method" | "attribute" | "decorator" => HighlightStyle::Function, + "type" | "constructor" => HighlightStyle::Type, + "property" => HighlightStyle::Property, + "constant" | "boolean" => HighlightStyle::Constant, + "operator" => HighlightStyle::Operator, + "punctuation" => HighlightStyle::Punctuation, + "variable" | "parameter" => HighlightStyle::Variable, + _ => return None, + }) +} + +fn hash_text(text: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + text.hash(&mut hasher); + hasher.finish() +} + +/// Скомпилированный запрос подсветки (один на язык, создаётся при первом обращении). +fn query(language: Language) -> Option<&'static Query> { + static RUST: OnceLock> = OnceLock::new(); + static GO: OnceLock> = OnceLock::new(); + static JS: OnceLock> = OnceLock::new(); + static JSON: OnceLock> = OnceLock::new(); + static PYTHON: OnceLock> = OnceLock::new(); + + let slot = match language { + Language::Rust => &RUST, + Language::Go => &GO, + Language::JavaScript => &JS, + Language::Json => &JSON, + Language::Python => &PYTHON, + }; + + slot.get_or_init( + || match Query::new(&language.grammar(), language.query_source()) { + Ok(query) => Some(query), + Err(err) => { + eprintln!("tree-sitter: запрос подсветки для {language:?}: {err}"); + None + } + }, + ) + .as_ref() +} + +// Запросы намеренно не используют предикаты `#match?`: они не нужны для +// базовой подсветки и не должны зависеть от конкретной реализации tree-sitter. + +const RUST_QUERY: &str = r#" +(line_comment) @comment +(block_comment) @comment +(doc_comment) @comment + +(string_literal) @string +(raw_string_literal) @string +(char_literal) @string +(escape_sequence) @escape + +(integer_literal) @number +(float_literal) @number +(boolean_literal) @constant + +(type_identifier) @type +(primitive_type) @type +(field_identifier) @property +(lifetime) @type + +(function_item name: (identifier) @function) +(function_signature_item name: (identifier) @function) +(macro_invocation macro: (identifier) @function) +(call_expression function: (identifier) @function) +(call_expression function: (field_expression field: (field_identifier) @function)) +(metavariable) @variable + +[ + "as" "async" "await" "break" "const" "continue" "default" "dyn" "else" + "enum" "extern" "fn" "for" "gen" "if" "impl" "in" "let" "loop" "match" + "mod" "move" "pub" "raw" "ref" "return" "static" "struct" "trait" "type" + "union" "unsafe" "use" "where" "while" "yield" +] @keyword +(crate) @keyword +(mutable_specifier) @keyword +(self) @keyword +(super) @keyword +"#; + +const GO_QUERY: &str = r#" +(comment) @comment + +(interpreted_string_literal) @string +(raw_string_literal) @string +(rune_literal) @string +(escape_sequence) @escape + +(int_literal) @number +(float_literal) @number +(imaginary_literal) @number +(true) @constant +(false) @constant +(nil) @constant +(iota) @constant + +(type_identifier) @type +(field_identifier) @property + +(function_declaration name: (identifier) @function) +(method_declaration name: (field_identifier) @function) +(call_expression function: (identifier) @function) +(call_expression function: (selector_expression field: (field_identifier) @function)) + +[ + "break" "case" "chan" "const" "continue" "default" "defer" "else" + "fallthrough" "for" "func" "go" "goto" "if" "import" "interface" "map" + "package" "range" "return" "select" "struct" "switch" "type" "var" +] @keyword +"#; + +const JS_QUERY: &str = r#" +(comment) @comment + +(string) @string +(template_string) @string +(regex) @string +(escape_sequence) @escape +(number) @number +(true) @constant +(false) @constant +(null) @constant +(undefined) @constant + +(property_identifier) @property + +(function_declaration name: (identifier) @function) +(generator_function_declaration name: (identifier) @function) +(method_definition name: (property_identifier) @function) +(class_declaration name: (identifier) @type) +(call_expression function: (identifier) @function) +(call_expression function: (member_expression property: (property_identifier) @function)) + +[ + "as" "async" "await" "break" "case" "catch" "class" "const" "continue" + "debugger" "default" "delete" "do" "else" "export" "extends" "finally" + "for" "from" "function" "get" "if" "import" "in" "instanceof" "let" + "new" "of" "return" "set" "static" "switch" "target" "throw" "try" + "typeof" "var" "void" "while" "with" "yield" +] @keyword +(this) @keyword +(super) @keyword +"#; + +const JSON_QUERY: &str = r#" +(comment) @comment +(escape_sequence) @escape + +(pair key: (string) @string.special.key) +(string) @string +(number) @number +(true) @constant +(false) @constant +(null) @constant +"#; + +const PYTHON_QUERY: &str = r#" +(comment) @comment + +(string) @string +(concatenated_string) @string +(escape_sequence) @escape +(integer) @number +(float) @number +(none) @constant +(true) @constant +(false) @constant + +(attribute attribute: (identifier) @property) +(type (identifier) @type) + +(function_definition name: (identifier) @function) +(class_definition name: (identifier) @type) +(decorator) @function +(call function: (identifier) @function) +(call function: (attribute attribute: (identifier) @function)) + +[ + "and" "as" "assert" "async" "await" "break" "class" "continue" "def" + "del" "elif" "else" "except" "finally" "for" "from" "global" "if" + "import" "in" "is" "lambda" "match" "nonlocal" "not" "or" "pass" + "raise" "return" "try" "while" "with" "yield" +] @keyword +"#; diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..533b933 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,646 @@ +//! Проверки UI-логики на «сырых» событиях egui (без реального окна). + +use std::cell::RefCell; +use std::rc::Rc; + +use eframe::egui; + +use crate::api::AppApi; +use crate::app::MyApp; +use crate::editor::EditorState; +use crate::keymap::KeyMap; +use crate::script::ScriptEngine; +use crate::syntax::{HighlightStyle, Language, SyntaxState}; +use crate::ui::title_bar::TitleBarResponses; + +const SCREEN: egui::Vec2 = egui::vec2(600.0, 400.0); + +/// Один проход кадра для данного экземпляра приложения: +/// возвращает ответы заголовка и команды окна. +fn step_app( + ctx: &egui::Context, + app: &mut MyApp, + events: Vec, +) -> (TitleBarResponses, Vec) { + let raw = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, SCREEN)), + events, + ..Default::default() + }; + + let mut bar = None; + let mut out = ctx.run_ui(raw, |ui| { + bar = Some(app.show(ui)); + }); + + let commands = out + .viewport_output + .get(&egui::ViewportId::ROOT) + .map(|output| output.commands.clone()) + .unwrap_or_default(); + out.textures_delta.clear(); + + (bar.expect("заголовок отрисован"), commands) +} + +/// Один проход кадра на свежем приложении. +fn step( + ctx: &egui::Context, + events: Vec, +) -> (TitleBarResponses, Vec) { + let mut app = MyApp::default(); + step_app(ctx, &mut app, events) +} + +fn button_event(pos: egui::Pos2, pressed: bool) -> egui::Event { + egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: egui::Modifiers::default(), + } +} + +fn click_at(ctx: &egui::Context, pos: egui::Pos2) -> Vec { + let mut commands = Vec::new(); + commands.extend(step(ctx, vec![egui::Event::PointerMoved(pos)]).1); + commands.extend(step(ctx, vec![button_event(pos, true)]).1); + commands.extend(step(ctx, vec![]).1); + commands.extend(step(ctx, vec![button_event(pos, false)]).1); + commands.extend(step(ctx, vec![]).1); + commands +} + +fn press_at(ctx: &egui::Context, pos: egui::Pos2) -> Vec { + let mut commands = step(ctx, vec![egui::Event::PointerMoved(pos)]).1; + commands.extend(step(ctx, vec![button_event(pos, true)]).1); + commands +} + +/// Событие клавиатуры, у которого логическая клавиша может отличаться от +/// физической (как при смене раскладки). +fn key_event( + physical: egui::Key, + logical: egui::Key, + pressed: bool, + modifiers: egui::Modifiers, +) -> egui::Event { + egui::Event::Key { + key: logical, + physical_key: Some(physical), + pressed, + repeat: false, + modifiers, + } +} + +#[test] +fn title_bar_buttons_are_hoverable_and_clickable() { + let ctx = egui::Context::default(); + // прогреваем: egui считает раскладку по предыдущему кадру + step(&ctx, vec![]); + let (bar, _) = step(&ctx, vec![]); + + let close_pos = bar.close.rect.center(); + let maximize_pos = bar.maximize.rect.center(); + let minimize_pos = bar.minimize.rect.center(); + + // Кнопки должны находиться в заголовке, а не где-то ещё + assert!(bar.drag.rect.contains(close_pos)); + + // Наведение даёт подсветку + step(&ctx, vec![egui::Event::PointerMoved(close_pos)]); + let (hovered, _) = step(&ctx, vec![]); + assert!( + hovered.close.hovered(), + "кнопка «закрыть» не подсвечивается" + ); + + let close_cmds = click_at(&ctx, close_pos); + assert!( + close_cmds.contains(&egui::ViewportCommand::Close), + "клик по «закрыть» не отправил Close: {close_cmds:?}" + ); + + let max_cmds = click_at(&ctx, maximize_pos); + assert!( + max_cmds.contains(&egui::ViewportCommand::Maximized(true)), + "клик по «развернуть» не отправил Maximized: {max_cmds:?}" + ); + + let min_cmds = click_at(&ctx, minimize_pos); + assert!( + min_cmds.contains(&egui::ViewportCommand::Minimized(true)), + "клик по «свернуть» не отправил Minimized: {min_cmds:?}" + ); +} + +#[test] +fn dragging_title_bar_starts_window_drag_even_over_the_text() { + for (name, x_offset) in [("по тексту", 40.0), ("по пустому месту", 300.0)] + { + let ctx = egui::Context::default(); + step(&ctx, vec![]); + let (bar, _) = step(&ctx, vec![]); + + let start = egui::pos2(bar.drag.rect.left() + x_offset, bar.drag.rect.center().y); + let moved = start + egui::vec2(24.0, 12.0); + + step(&ctx, vec![egui::Event::PointerMoved(start)]); + let mut commands = step(&ctx, vec![button_event(start, true)]).1; + commands.extend(step(&ctx, vec![egui::Event::PointerMoved(moved)]).1); + + assert!( + commands.contains(&egui::ViewportCommand::StartDrag), + "драг заголовка {name} не инициировал перемещение окна: {commands:?}" + ); + } +} + +#[test] +fn edges_and_corners_start_resize() { + use egui::viewport::ResizeDirection as Dir; + + let cases = [ + (egui::pos2(300.0, 2.0), Dir::North), + (egui::pos2(300.0, SCREEN.y - 2.0), Dir::South), + (egui::pos2(2.0, 200.0), Dir::West), + (egui::pos2(SCREEN.x - 2.0, 200.0), Dir::East), + (egui::pos2(2.0, 2.0), Dir::NorthWest), + (egui::pos2(SCREEN.x - 2.0, 2.0), Dir::NorthEast), + (egui::pos2(SCREEN.x - 2.0, SCREEN.y - 2.0), Dir::SouthEast), + (egui::pos2(2.0, SCREEN.y - 2.0), Dir::SouthWest), + ]; + + for (pos, expected) in cases { + let ctx = egui::Context::default(); + step(&ctx, vec![]); + step(&ctx, vec![]); + let commands = press_at(&ctx, pos); + assert!( + commands.contains(&egui::ViewportCommand::BeginResize(expected)), + "в точке {pos:?} ожидался BeginResize({expected:?}), получено: {commands:?}" + ); + assert!( + !commands.contains(&egui::ViewportCommand::StartDrag), + "в точке {pos:?} вместо ресайза началось перемещение окна" + ); + } +} + +#[test] +fn editor_state_keeps_at_least_one_tab() { + let mut state = EditorState::default(); + assert_eq!(state.documents().len(), 2); + assert_eq!(state.active().name, "main.rs"); + + // Открытие и закрытие новых вкладок. + state.open_new(); + assert_eq!(state.documents().len(), 3); + assert_eq!(state.active_index(), 2); + + state.close(2); + assert_eq!(state.active_index(), 1); + + // Редактор не должен оставаться пустым. + while state.documents().len() > 1 { + state.close(0); + } + state.close(0); + assert_eq!(state.documents().len(), 1); + assert_eq!(state.active_index(), 0); +} + +#[test] +fn closing_tab_before_active_keeps_the_same_document_active() { + let mut state = EditorState::default(); + state.open_new(); + let name = state.active().name.clone(); + + state.close(0); + assert_eq!(state.active().name, name); + assert_eq!(state.active_index(), 1); +} + +#[test] +fn editing_marks_document_dirty_and_saving_clears_it() { + let mut state = EditorState::default(); + assert!(!state.active().is_dirty()); + + state.active_mut().content.push_str("\n// правка\n"); + assert!(state.active().is_dirty()); + + state.active_mut().mark_saved(); + assert!(!state.active().is_dirty()); +} + +#[test] +fn typing_into_editor_updates_active_document() { + let ctx = egui::Context::default(); + let mut app = MyApp::default(); + + // Прогреваем раскладку и кликаем в область редактора ниже панели вкладок. + step_app(&ctx, &mut app, vec![]); + let editor_pos = egui::pos2(SCREEN.x / 2.0, SCREEN.y - 80.0); + step_app(&ctx, &mut app, vec![egui::Event::PointerMoved(editor_pos)]); + step_app(&ctx, &mut app, vec![button_event(editor_pos, true)]); + step_app(&ctx, &mut app, vec![button_event(editor_pos, false)]); + + step_app(&ctx, &mut app, vec![egui::Event::Text("ZZZ".to_owned())]); + + let editor = app.editor(); + let doc = editor.active(); + assert!( + doc.content.contains("ZZZ"), + "введённый текст не попал в документ: {:?}", + doc.content + ); + assert!(doc.is_dirty(), "правка должна помечать документ изменённым"); +} + +#[test] +fn api_lists_tabs_and_closes_them_by_number() { + let ctx = egui::Context::default(); + let mut app = MyApp::default(); + step_app(&ctx, &mut app, vec![]); + + let api = AppApi::global(); + let tabs = api.tabs(); + assert_eq!( + tabs.iter() + .map(|tab| (tab.index, tab.title.as_str())) + .collect::>(), + vec![(0, "main.rs"), (1, "notes.md")] + ); + + // Несуществующий номер ничего не закрывает. + assert!(!api.close_tab(99)); + assert_eq!(api.tabs().len(), 2); + + assert!(api.close_tab(1)); + assert_eq!(api.tabs().len(), 1); + assert_eq!(app.editor().documents().len(), 1); +} + +#[test] +fn api_save_writes_active_tab_to_file_named_after_it() { + // Тест пишет во временный файл, а не в каталог проекта. + let mut state = EditorState::default(); + let path = std::env::temp_dir().join(format!( + "egui-rust-test-save-{}-{}.rs", + std::process::id(), + state.active().id + )); + let _ = std::fs::remove_file(&path); + + { + let doc = state.active_mut(); + doc.name = path.to_string_lossy().into_owned(); + doc.content = "fn main() {}\n".to_owned(); + } + assert!(state.active().is_dirty()); + + let editor = Rc::new(RefCell::new(state)); + let api = AppApi::install( + egui::Context::default(), + editor.clone(), + Rc::new(RefCell::new(KeyMap::default())), + ); + api.save().expect("сохранение должно удаться"); + + assert_eq!( + std::fs::read_to_string(&path).expect("файл должен существовать"), + "fn main() {}\n" + ); + assert!(!editor.borrow().active().is_dirty()); + + let _ = std::fs::remove_file(&path); +} + +#[test] +fn api_switches_tabs_forward_and_back_in_a_cycle() { + let editor = Rc::new(RefCell::new(EditorState::default())); + let api = AppApi::install( + egui::Context::default(), + editor.clone(), + Rc::new(RefCell::new(KeyMap::default())), + ); + + assert_eq!(api.active_tab_index(), 0); + + assert_eq!(api.next_tab(), 1); + assert_eq!(api.active_tab_index(), 1); + + // С последней вкладки следующая — первая. + assert_eq!(api.next_tab(), 0); + + // С первой вкладки предыдущая — последняя. + assert_eq!(api.previous_tab(), 1); + assert_eq!(api.previous_tab(), 0); + assert_eq!(editor.borrow().active_index(), 0); +} + +#[test] +fn lua_script_can_include_files_and_control_the_app() { + let ctx = egui::Context::default(); + let mut app = MyApp::default(); + // Первый кадр публикует AppApi, который использует сценарий. + step_app(&ctx, &mut app, vec![]); + + let dir = std::env::temp_dir().join(format!( + "egui-rust-test-lua-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("системные часы") + .as_nanos() + )); + std::fs::create_dir_all(&dir).expect("временный каталог сценариев"); + + let helper = dir.join("helper.lua"); + let main = dir.join("main.lua"); + std::fs::write( + &helper, + "function lua_config() app:set_active_text(\"from lua\") end\n", + ) + .expect("helper.lua"); + std::fs::write( + &main, + format!( + "include({:?})\nlua_config()\napp:next_tab()\napp:set_language(1, \"python\")\n", + helper.to_string_lossy() + ), + ) + .expect("main.lua"); + + let engine = ScriptEngine::new(AppApi::global()).expect("движок Lua"); + engine.run_file(&main).expect("сценарий должен выполниться"); + + let editor = app.editor(); + assert_eq!( + editor.documents()[0].content, + "from lua", + "include должен выполнить подключённый файл в общем окружении" + ); + assert_eq!( + editor.active_index(), + 1, + "app:next_tab() не переключил вкладку" + ); + assert_eq!( + editor.documents()[1].language, + Some(Language::Python), + "app:set_language() не включил подсветку из Lua" + ); + + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn lua_key_bindings_use_physical_keys_and_modifiers() { + let ctx = egui::Context::default(); + let mut app = MyApp::default(); + // Первый кадр публикует AppApi с картой клавиш приложения. + step_app(&ctx, &mut app, vec![]); + + let dir = std::env::temp_dir().join(format!( + "egui-rust-test-keys-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("системные часы") + .as_nanos() + )); + std::fs::create_dir_all(&dir).expect("временный каталог сценариев"); + + let script = dir.join("keys.lua"); + std::fs::write( + &script, + r#" + app:map_key("a", function() app:set_active_text("a pressed") end) + app:map_key("!1", function() app:set_active_text("1 released") end) + app:map_key("", function() app:set_active_text("shift c") end) + app:map_key("", function() app:set_active_text("ctrl z") end) + app:map_key("", function() app:set_active_text("alt r released") end) + "#, + ) + .expect("keys.lua"); + + let engine = ScriptEngine::new(AppApi::global()).expect("движок Lua"); + engine + .run_file(&script) + .expect("привязки должны назначиться"); + + let text = |app: &MyApp| app.editor().active().content.clone(); + + // Логическая клавиша Z, физическая A: срабатывает именно физическая. + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::A, + egui::Key::Z, + true, + egui::Modifiers::default(), + )], + ); + assert_eq!(text(&app), "a pressed"); + + // Shift+A не должна срабатывать как обычная A. + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::A, + egui::Key::A, + true, + egui::Modifiers { + shift: true, + ..Default::default() + }, + )], + ); + assert_eq!(text(&app), "a pressed"); + + // Отпускание цифры 1, логическая клавиша при этом другая. + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::Num1, + egui::Key::Num2, + false, + egui::Modifiers::default(), + )], + ); + assert_eq!(text(&app), "1 released"); + + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::C, + egui::Key::C, + true, + egui::Modifiers { + shift: true, + ..Default::default() + }, + )], + ); + assert_eq!(text(&app), "shift c"); + + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::Z, + egui::Key::Z, + true, + egui::Modifiers { + ctrl: true, + ..Default::default() + }, + )], + ); + assert_eq!(text(&app), "ctrl z"); + + // Отпускание R при зажатом Alt. + step_app( + &ctx, + &mut app, + vec![key_event( + egui::Key::R, + egui::Key::R, + false, + egui::Modifiers { + alt: true, + ..Default::default() + }, + )], + ); + assert_eq!(text(&app), "alt r released"); + + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn language_names_cover_all_supported_grammars() { + for (name, expected) in [ + ("rust", Language::Rust), + ("go", Language::Go), + ("js", Language::JavaScript), + ("json", Language::Json), + ("python", Language::Python), + ] { + assert_eq!(Language::from_name(name), Some(expected), "язык {name}"); + assert_eq!(Language::from_name(&name.to_uppercase()), Some(expected)); + } + + // Распространённые короткие имена. + assert_eq!(Language::from_name("rs"), Some(Language::Rust)); + assert_eq!(Language::from_name("golang"), Some(Language::Go)); + assert_eq!( + Language::from_name("javascript"), + Some(Language::JavaScript) + ); + assert_eq!(Language::from_name("py"), Some(Language::Python)); + + assert_eq!(Language::from_name("cobol"), None); +} + +#[test] +fn tree_sitter_highlights_all_supported_languages() { + let samples = [ + ( + Language::Rust, + "fn main() {\n let x = 42; // комментарий\n println!(\"hi\");\n}\n", + HighlightStyle::Keyword, + ), + ( + Language::Go, + "package main\n\nfunc main() {\n\tx := 42 // комментарий\n\t_ = x\n}\n", + HighlightStyle::Keyword, + ), + ( + Language::JavaScript, + "function main() {\n const x = 42; // comment\n console.log(\"hi\");\n}\n", + HighlightStyle::Keyword, + ), + ( + Language::Json, + "{\"name\": \"value\", \"count\": 42, \"ok\": true}\n", + HighlightStyle::Property, + ), + ( + Language::Python, + "def main():\n x = 42 # comment\n print(\"hi\")\n", + HighlightStyle::Keyword, + ), + ]; + + for (language, source, expected) in samples { + let mut syntax = SyntaxState::default(); + let styles: Vec<_> = syntax + .spans(language, source) + .iter() + .map(|span| span.style) + .collect(); + + assert!(!styles.is_empty(), "{language:?}: подсветка пуста"); + assert!( + styles.contains(&expected), + "{language:?}: нет стиля {expected:?} в {styles:?}" + ); + } +} + +#[test] +fn syntax_tree_is_available_for_tasks_beyond_highlighting() { + let mut syntax = SyntaxState::default(); + let tree = syntax + .parse(Language::Rust, "fn main() {}\n") + .expect("дерево должно разобраться"); + + let root = tree.root_node(); + assert_eq!(root.kind(), "source_file"); + + let mut cursor = root.walk(); + assert!( + root.children(&mut cursor) + .any(|node| node.kind() == "function_item"), + "в дереве должна находиться функция — его можно обходить, а не только красить" + ); +} + +#[test] +fn highlighting_is_off_by_default_and_enabled_through_api() { + let ctx = egui::Context::default(); + let mut app = MyApp::default(); + step_app(&ctx, &mut app, vec![]); + + assert!( + app.editor() + .documents() + .iter() + .all(|doc| doc.language.is_none()), + "подсветка должна быть выключена по умолчанию" + ); + + let api = AppApi::global(); + assert!(api.set_language(0, "rust")); + assert_eq!(api.language(0).as_deref(), Some("rust")); + assert_eq!(app.editor().documents()[0].language, Some(Language::Rust)); + + // Неизвестный язык и несуществующая вкладка не меняют состояние. + assert!(!api.set_language(0, "cobol")); + assert!(!api.set_language(99, "rust")); + assert_eq!(api.language(0).as_deref(), Some("rust")); + + // Кадр с включённой подсветкой должен отрисовываться без паники. + step_app(&ctx, &mut app, vec![]); + + assert!(api.clear_language(0)); + assert_eq!(api.language(0), None); + assert!(!api.clear_language(99)); +} diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs new file mode 100644 index 0000000..aeb6a4b --- /dev/null +++ b/src/ui/code_editor.rs @@ -0,0 +1,298 @@ +//! Область редактирования кода: нумерация строк и многострочное поле ввода. + +use eframe::egui; + +use crate::constants::CORNER_RADIUS; +use crate::editor::Document; +use crate::syntax::{HighlightStyle, Language, SyntaxState}; + +const FONT_SIZE: f32 = 13.0; +/// Отступы текста от краёв области (как у обычного `TextEdit`). +const MARGIN_X: i8 = 10; +const MARGIN_Y: i8 = 8; +/// Отступ номера строки от текста справа и минимальная ширина колонки. +const GUTTER_PADDING: f32 = 12.0; + +/// Рисует редактор активного документа. +/// +/// `focus` — запросить фокус у поля ввода (например, сразу после переключения вкладки). +pub fn render(ui: &mut egui::Ui, doc: &mut Document, focus: bool) { + let area = ui.available_rect_before_wrap(); + + // Фон на всю оставшуюся область; нижние углы повторяют скругление окна. + ui.painter().rect_filled( + area, + egui::CornerRadius { + nw: 0, + ne: 0, + sw: CORNER_RADIUS, + se: CORNER_RADIUS, + }, + ui.visuals().extreme_bg_color, + ); + + let font_id = egui::FontId::monospace(FONT_SIZE); + let char_width = ui.fonts_mut(|fonts| fonts.glyph_width(&font_id, '0')); + let line_height = + ui.fonts_mut(|fonts| fonts.row_height(&font_id)) + ui.spacing().extra_text_line_spacing; + + let line_count = doc.content.split('\n').count(); + let gutter_width = + (line_count.to_string().len() as f32 * char_width + GUTTER_PADDING * 2.0).max(30.0); + + // Сколько строк нужно, чтобы поле ввода заполнило пустую область целиком: + // клик ниже последней строки должен ставить курсор, а не попадать в пустоту. + let rows_to_fill = + (((area.height() - 2.0 * f32::from(MARGIN_Y)) / line_height).floor() as usize).max(1); + + let doc_id = doc.id; + let language = doc.language; + // Поля берём по отдельности: `content` нужен `TextEdit`, а `syntax` — + // подсветке, и это разные заимствования одного документа. + let content = &mut doc.content; + let syntax = &mut doc.syntax; + + egui::ScrollArea::vertical() + .id_salt(("code_scroll", doc_id)) + .auto_shrink([false, false]) + .show(ui, |ui| { + // Без отступов между гуттером и текстом: строка должна быть единой. + ui.spacing_mut().item_spacing = egui::Vec2::ZERO; + + let gutter_left = ui.cursor().left(); + + let output = ui + .horizontal_top(|ui| { + ui.add_space(gutter_width); + + let text_edit = egui::TextEdit::multiline(&mut *content) + .id_salt(("code_edit", doc_id)) + .font(font_id.clone()) + .frame(egui::Frame::NONE.inner_margin(egui::Margin { + left: MARGIN_X, + right: MARGIN_X, + top: MARGIN_Y, + bottom: MARGIN_Y, + })) + .desired_width(f32::INFINITY) + .desired_rows(rows_to_fill.max(line_count)); + + // Подсветка включается только для документов с выбранным + // языком; без него используется стандартный layouter. + if let Some(language) = language { + let theme = SyntaxTheme::from_visuals(ui.visuals()); + let mut layouter = + |ui: &egui::Ui, text: &dyn egui::TextBuffer, wrap_width: f32| { + let job = highlighted_layout_job( + text.as_str(), + language, + syntax, + &theme, + &font_id, + line_height, + wrap_width, + ); + ui.fonts_mut(|fonts| fonts.layout_job(job)) + }; + text_edit.layouter(&mut layouter).show(ui) + } else { + text_edit.show(ui) + } + }) + .inner; + + if focus { + output.response.response.request_focus(); + } + + paint_line_numbers( + ui, + &output, + content.as_str(), + &font_id, + gutter_left, + gutter_width, + ); + }); +} + +/// Собирает `LayoutJob` для подсвеченного текста. +/// +/// Форматирование повторяет стандартный layouter `TextEdit`, чтобы курсор, +/// перенос строк и номера строк работали так же, как без подсветки. +fn highlighted_layout_job( + text: &str, + language: Language, + syntax: &mut SyntaxState, + theme: &SyntaxTheme, + font_id: &egui::FontId, + line_height: f32, + wrap_width: f32, +) -> egui::text::LayoutJob { + let mut job = egui::text::LayoutJob::default(); + job.wrap.max_width = wrap_width; + job.keep_trailing_whitespace = true; + + let format = |color: egui::Color32| egui::TextFormat { + font_id: font_id.clone(), + color, + line_height: Some(line_height), + ..Default::default() + }; + + let mut cursor = 0; + for span in syntax.spans(language, text) { + if span.start > cursor { + job.append( + text.get(cursor..span.start).unwrap_or_default(), + 0.0, + format(theme.default), + ); + } + job.append( + text.get(span.start..span.end).unwrap_or_default(), + 0.0, + format(theme.color(span.style)), + ); + cursor = span.end; + } + if cursor < text.len() { + job.append( + text.get(cursor..).unwrap_or_default(), + 0.0, + format(theme.default), + ); + } + if text.is_empty() { + // Пустой абзац нужен, чтобы у поля была строка для курсора. + job.append("", 0.0, format(theme.default)); + } + + job +} + +/// Цвета подсветки, подобранные под текущую тему egui. +#[derive(Clone, Copy)] +struct SyntaxTheme { + default: egui::Color32, + keyword: egui::Color32, + string: egui::Color32, + escape: egui::Color32, + number: egui::Color32, + comment: egui::Color32, + function: egui::Color32, + type_: egui::Color32, + property: egui::Color32, + constant: egui::Color32, + operator: egui::Color32, + punctuation: egui::Color32, + variable: egui::Color32, +} + +impl SyntaxTheme { + fn from_visuals(visuals: &egui::Visuals) -> Self { + let default = visuals.text_color(); + + if visuals.dark_mode { + Self { + default, + keyword: egui::Color32::from_rgb(0xC6, 0x78, 0xDD), + string: egui::Color32::from_rgb(0x98, 0xC3, 0x79), + escape: egui::Color32::from_rgb(0x56, 0xB6, 0xC2), + number: egui::Color32::from_rgb(0xD1, 0x9A, 0x66), + comment: egui::Color32::from_rgb(0x7F, 0x84, 0x8E), + function: egui::Color32::from_rgb(0x61, 0xAF, 0xEF), + type_: egui::Color32::from_rgb(0xE5, 0xC0, 0x7B), + property: egui::Color32::from_rgb(0xE0, 0x6C, 0x75), + constant: egui::Color32::from_rgb(0xD1, 0x9A, 0x66), + operator: egui::Color32::from_rgb(0x56, 0xB6, 0xC2), + punctuation: egui::Color32::from_rgb(0xAB, 0xB2, 0xBF), + variable: default, + } + } else { + Self { + default, + keyword: egui::Color32::from_rgb(0xA6, 0x26, 0xA4), + string: egui::Color32::from_rgb(0x50, 0xA1, 0x4F), + escape: egui::Color32::from_rgb(0x01, 0x84, 0xBC), + number: egui::Color32::from_rgb(0x98, 0x68, 0x01), + comment: egui::Color32::from_rgb(0xA0, 0xA1, 0xA7), + function: egui::Color32::from_rgb(0x40, 0x78, 0xF2), + type_: egui::Color32::from_rgb(0xC1, 0x84, 0x01), + property: egui::Color32::from_rgb(0xE4, 0x56, 0x49), + constant: egui::Color32::from_rgb(0x98, 0x68, 0x01), + operator: egui::Color32::from_rgb(0x01, 0x84, 0xBC), + punctuation: egui::Color32::from_rgb(0x38, 0x3A, 0x42), + variable: default, + } + } + } + + fn color(&self, style: HighlightStyle) -> egui::Color32 { + match style { + HighlightStyle::Keyword => self.keyword, + HighlightStyle::String => self.string, + HighlightStyle::Escape => self.escape, + HighlightStyle::Number => self.number, + HighlightStyle::Comment => self.comment, + HighlightStyle::Function => self.function, + HighlightStyle::Type => self.type_, + HighlightStyle::Property => self.property, + HighlightStyle::Constant => self.constant, + HighlightStyle::Operator => self.operator, + HighlightStyle::Punctuation => self.punctuation, + HighlightStyle::Variable => self.variable, + } + } +} + +/// Рисует номера строк вдоль гуттера. +/// +/// Строки берутся из галереи `TextEdit`, поэтому номера совпадают с текстом даже +/// при переносе длинных строк: у перенесённой строки номер не повторяется. +fn paint_line_numbers( + ui: &egui::Ui, + output: &egui::widgets::text_edit::TextEditOutput, + content: &str, + font_id: &egui::FontId, + gutter_left: f32, + gutter_width: f32, +) { + let visuals = ui.visuals(); + let number_color = visuals.weak_text_color(); + let current_line_color = visuals.strong_text_color(); + + // Индекс курсора — в символах, а не в байтах. + let current_line = output.cursor_range.map(|range| { + content + .chars() + .take(range.primary.index.into()) + .filter(|&c| c == '\n') + .count() + }); + let has_focus = output.response.response.has_focus(); + + let right = gutter_left + gutter_width - GUTTER_PADDING; + let painter = ui.painter(); + + let mut line_number = 1; + let mut starts_new_line = true; + for row in &output.galley.rows { + if starts_new_line { + let is_current = has_focus && current_line == Some(line_number - 1); + painter.text( + egui::pos2(right, output.galley_pos.y + row.rect().center().y), + egui::Align2::RIGHT_CENTER, + line_number, + font_id.clone(), + if is_current { + current_line_color + } else { + number_color + }, + ); + line_number += 1; + } + starts_new_line = row.ends_with_newline; + } +} diff --git a/src/ui/icons.rs b/src/ui/icons.rs new file mode 100644 index 0000000..49a33f7 --- /dev/null +++ b/src/ui/icons.rs @@ -0,0 +1,93 @@ +//! Векторные иконки кнопок заголовка (без шрифтов — рисуются через `Painter`). + +use eframe::egui; + +/// Тип иконки, отображаемой на кнопке заголовка. +pub enum IconType { + Minimize, + Maximize, + Restore, + Close, + Plus, +} + +/// Кнопка с векторной иконкой внутри. +pub fn custom_icon_button(ui: &mut egui::Ui, size: egui::Vec2, icon: IconType) -> egui::Response { + let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click()); + + if response.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + } + + let visuals = ui.style().interact(&response); + + // Подсветка кнопки при наведении мыши + if response.hovered() || response.is_pointer_button_down_on() { + ui.painter().rect_filled(rect, 4.0, visuals.bg_fill); + } + + let stroke = egui::Stroke::new(1.5, visuals.fg_stroke.color); + let center = rect.center(); + let r = 5.0; // Размер иконки относительно центра + + match icon { + IconType::Minimize => { + ui.painter().line_segment( + [center - egui::vec2(r, 0.0), center + egui::vec2(r, 0.0)], + stroke, + ); + } + IconType::Maximize => { + ui.painter().rect_stroke( + egui::Rect::from_center_size(center, egui::vec2(r * 2.0, r * 2.0)), + 0.0, + stroke, + egui::StrokeKind::Middle, + ); + } + IconType::Restore => { + // Два перекрывающихся квадрата — символ «восстановить» + let back = egui::Rect::from_center_size( + center + egui::vec2(2.0, -2.0), + egui::vec2(r * 2.0, r * 2.0), + ); + let front = egui::Rect::from_center_size( + center + egui::vec2(-2.0, 2.0), + egui::vec2(r * 2.0, r * 2.0), + ); + // Заливаем «передний» квадрат фоном, чтобы задний не просвечивал сквозь него + let fill = if response.hovered() || response.is_pointer_button_down_on() { + visuals.bg_fill + } else { + ui.visuals().window_fill() + }; + ui.painter().rect_filled(front, 0.0, fill); + ui.painter() + .rect_stroke(back, 0.0, stroke, egui::StrokeKind::Middle); + ui.painter() + .rect_stroke(front, 0.0, stroke, egui::StrokeKind::Middle); + } + IconType::Close => { + ui.painter().line_segment( + [center - egui::vec2(r, r), center + egui::vec2(r, r)], + stroke, + ); + ui.painter().line_segment( + [center - egui::vec2(-r, r), center + egui::vec2(-r, r)], + stroke, + ); + } + IconType::Plus => { + ui.painter().line_segment( + [center - egui::vec2(4.0, 0.0), center + egui::vec2(4.0, 0.0)], + stroke, + ); + ui.painter().line_segment( + [center - egui::vec2(0.0, 4.0), center + egui::vec2(0.0, 4.0)], + stroke, + ); + } + } + + response +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..7e586e2 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,8 @@ +//! Виджеты окна: интегрированный заголовок, вкладки, редактор, иконки +//! и зоны изменения размера. + +pub mod code_editor; +pub mod icons; +pub mod resize_handles; +pub mod tabs; +pub mod title_bar; diff --git a/src/ui/resize_handles.rs b/src/ui/resize_handles.rs new file mode 100644 index 0000000..68a0db5 --- /dev/null +++ b/src/ui/resize_handles.rs @@ -0,0 +1,121 @@ +//! Невидимые зоны по периметру окна для изменения размера. + +use eframe::egui; + +use crate::api::AppApi; +use crate::constants::RESIZE_BORDER; + +/// Невидимые полосы и уголки по периметру окна: тянут за них — меняется размер окна. +/// +/// Регистрируются последними (в том же слое), поэтому выигрывают hit-test у содержимого. +/// Само изменение размера выполняет оконная система через `ViewportCommand::BeginResize`. +pub fn render(ui: &mut egui::Ui) { + use egui::viewport::ResizeDirection as Dir; + + let api = AppApi::global(); + let ctx = api.context(); + + // Развёрнутое окно уже занимает весь экран — тянуть нечего. + if api.is_maximized() { + return; + } + + let b = RESIZE_BORDER; + let rect = ctx.content_rect(); + if rect.width() <= 2.0 * b || rect.height() <= 2.0 * b { + return; + } + + let north = egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.right(), rect.top() + b)); + let south = egui::Rect::from_min_max( + egui::pos2(rect.left(), rect.bottom() - b), + rect.right_bottom(), + ); + let west = + egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.left() + b, rect.bottom())); + let east = egui::Rect::from_min_max( + egui::pos2(rect.right() - b, rect.top()), + rect.right_bottom(), + ); + + let edges = [ + ( + Dir::North, + egui::CursorIcon::ResizeVertical, + north, + "resize_n", + ), + ( + Dir::South, + egui::CursorIcon::ResizeVertical, + south, + "resize_s", + ), + ( + Dir::West, + egui::CursorIcon::ResizeHorizontal, + west, + "resize_w", + ), + ( + Dir::East, + egui::CursorIcon::ResizeHorizontal, + east, + "resize_e", + ), + ]; + + // Углы идут после рёбер: в них должен побеждать диагональный ресайз. + let corners = [ + ( + Dir::NorthWest, + egui::CursorIcon::ResizeNwSe, + egui::Rect::from_min_size(rect.left_top(), egui::Vec2::splat(b)), + "resize_nw", + ), + ( + Dir::NorthEast, + egui::CursorIcon::ResizeNeSw, + egui::Rect::from_min_size( + egui::pos2(rect.right() - b, rect.top()), + egui::Vec2::splat(b), + ), + "resize_ne", + ), + ( + Dir::SouthEast, + egui::CursorIcon::ResizeNwSe, + egui::Rect::from_min_size( + egui::pos2(rect.right() - b, rect.bottom() - b), + egui::Vec2::splat(b), + ), + "resize_se", + ), + ( + Dir::SouthWest, + egui::CursorIcon::ResizeNeSw, + egui::Rect::from_min_size( + egui::pos2(rect.left(), rect.bottom() - b), + egui::Vec2::splat(b), + ), + "resize_sw", + ), + ]; + + for (direction, cursor, handle_rect, id_salt) in edges.into_iter().chain(corners) { + // `Sense::drag()` (без click) — перетаскивание начинается сразу по нажатию, + // а именно этого требует `BeginResize`. + let response = ui.interact( + handle_rect, + ui.scope_id().with(id_salt), + egui::Sense::drag(), + ); + + if response.hovered() { + ctx.set_cursor_icon(cursor); + } + if response.drag_started() { + ctx.send_viewport_cmd(egui::ViewportCommand::BeginResize(direction)); + } + } +} diff --git a/src/ui/tabs.rs b/src/ui/tabs.rs new file mode 100644 index 0000000..85e90b9 --- /dev/null +++ b/src/ui/tabs.rs @@ -0,0 +1,228 @@ +//! Полоса вкладок над редактором. + +use eframe::egui; + +use crate::constants::TAB_BAR_HEIGHT; +use crate::editor::Document; +use crate::ui::icons::{IconType, custom_icon_button}; + +/// Действия пользователя с вкладками за текущий кадр. +#[derive(Default)] +pub struct TabBarActions { + /// Клик по вкладке: её нужно сделать активной. + pub clicked_tab: Option, + /// Клик по крестику: вкладку нужно закрыть. + pub closed_tab: Option, + /// Клик по «+»: нужно открыть новый документ. + pub add_clicked: bool, +} + +const TAB_MIN_WIDTH: f32 = 96.0; +const TAB_MAX_WIDTH: f32 = 220.0; +const TAB_PADDING_LEFT: f32 = 12.0; +const TAB_PADDING_RIGHT: f32 = 6.0; +const TAB_LABEL_GAP: f32 = 6.0; +const TAB_CLOSE_SIZE: f32 = 18.0; +const TAB_CORNER_RADIUS: u8 = 6; + +/// Рисует полосу вкладок и возвращает действия пользователя. +pub fn render(ui: &mut egui::Ui, documents: &[Document], active: usize) -> TabBarActions { + let mut actions = TabBarActions::default(); + + let bar_rect = egui::Rect::from_min_size( + ui.cursor().min, + egui::vec2(ui.available_width(), TAB_BAR_HEIGHT), + ); + + let visuals = ui.visuals().clone(); + + // Фон полосы совпадает с фоном заголовка, поэтому она выглядит его продолжением. + ui.painter() + .rect_filled(bar_rect, 0.0, visuals.window_fill()); + + // Тонкая линия-разделитель под вкладками. Активная вкладка перекрывает её + // и сливается с фоном редактора. + let separator = egui::Rect::from_min_max( + egui::pos2(bar_rect.left(), bar_rect.bottom() - 1.0), + bar_rect.right_bottom(), + ); + ui.painter().rect_filled( + separator, + 0.0, + visuals.widgets.noninteractive.bg_stroke.color, + ); + + ui.allocate_ui_with_layout( + egui::vec2(bar_rect.width(), TAB_BAR_HEIGHT), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + ui.spacing_mut().item_spacing.x = 2.0; + ui.add_space(6.0); + + for (index, doc) in documents.iter().enumerate() { + let tab = tab(ui, doc, index == active); + if tab.close_clicked { + actions.closed_tab = Some(index); + } else if tab.clicked { + actions.clicked_tab = Some(index); + } + } + + ui.add_space(4.0); + let add = custom_icon_button(ui, egui::vec2(20.0, 20.0), IconType::Plus); + if add.on_hover_text("Новая вкладка").clicked() { + actions.add_clicked = true; + } + }, + ); + + actions +} + +struct TabResponse { + clicked: bool, + close_clicked: bool, +} + +/// Одна вкладка: фон, название и кнопка закрытия. +fn tab(ui: &mut egui::Ui, doc: &Document, is_active: bool) -> TabResponse { + let font_id = egui::FontId::proportional(12.5); + let label_width = ui.fonts_mut(|fonts| { + fonts + .layout_no_wrap(doc.name.clone(), font_id.clone(), egui::Color32::WHITE) + .size() + .x + }); + let width = + (label_width + TAB_PADDING_LEFT + TAB_LABEL_GAP + TAB_CLOSE_SIZE + TAB_PADDING_RIGHT) + .clamp(TAB_MIN_WIDTH, TAB_MAX_WIDTH); + + let (rect, response) = + ui.allocate_exact_size(egui::vec2(width, TAB_BAR_HEIGHT), egui::Sense::click()); + if response.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + } + + let visuals = ui.visuals().clone(); + let corner = egui::CornerRadius { + nw: TAB_CORNER_RADIUS, + ne: TAB_CORNER_RADIUS, + sw: 0, + se: 0, + }; + + let fill = if is_active { + visuals.extreme_bg_color + } else if response.hovered() { + // Полтона от цвета hover-виджетов: вкладка должна подсвечиваться мягко. + visuals + .window_fill() + .lerp_to_gamma(visuals.widgets.hovered.weak_bg_fill, 0.5) + } else { + egui::Color32::TRANSPARENT + }; + ui.painter().rect_filled(rect, corner, fill); + + // Акцентная полоска у активной вкладки. + if is_active { + let stripe = egui::Rect::from_min_size(rect.left_top(), egui::vec2(rect.width(), 2.0)); + ui.painter() + .rect_filled(stripe, corner, visuals.selection.bg_fill); + } + + let close_rect = egui::Rect::from_center_size( + egui::pos2( + rect.right() - TAB_PADDING_RIGHT - TAB_CLOSE_SIZE / 2.0, + rect.center().y, + ), + egui::Vec2::splat(TAB_CLOSE_SIZE), + ); + + let text_color = if is_active { + visuals.strong_text_color() + } else if response.hovered() { + visuals.text_color() + } else { + visuals.weak_text_color() + }; + + // Название рисуем через `LayoutJob`, чтобы длинные имена обрезались многоточием. + let mut job = egui::text::LayoutJob::single_section( + doc.name.clone(), + egui::TextFormat::simple(font_id, text_color), + ); + job.wrap = egui::text::TextWrapping { + max_width: (close_rect.left() - TAB_LABEL_GAP - (rect.left() + TAB_PADDING_LEFT)).max(0.0), + max_rows: 1, + break_anywhere: true, + overflow_character: Some('…'), + }; + let galley = ui.fonts_mut(|fonts| fonts.layout_job(job)); + ui.painter().galley( + egui::pos2( + rect.left() + TAB_PADDING_LEFT, + rect.center().y - galley.size().y / 2.0, + ), + galley, + text_color, + ); + + // Кнопка закрытия регистрируется после вкладки, поэтому выигрывает у неё hit-test + // в своей области (так же, как кнопки заголовка регистрируются после drag-зоны). + let close = close_button( + ui, + close_rect, + ui.scope_id().with(("tab_close", doc.id)), + is_active || response.hovered(), + doc.is_dirty(), + ); + + TabResponse { + clicked: response.clicked(), + close_clicked: close.clicked(), + } +} + +/// Маленький крестик вкладки; у изменённого документа вместо него — точка. +fn close_button( + ui: &mut egui::Ui, + rect: egui::Rect, + id: egui::Id, + show_cross: bool, + dirty: bool, +) -> egui::Response { + // Невидимый крестик не должен ловить клики: иначе клик по пустому месту + // закрывал бы вкладку. + let sense = if show_cross { + egui::Sense::click() + } else { + egui::Sense::hover() + }; + let response = ui.interact(rect, id, sense); + + let visuals = ui.visuals(); + let painter = ui.painter(); + + if response.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + painter.rect_filled(rect, 4.0, visuals.widgets.hovered.bg_fill); + } + + let center = rect.center(); + if dirty && !response.hovered() { + painter.circle_filled(center, 3.0, visuals.selection.bg_fill); + } else if show_cross { + let stroke = egui::Stroke::new(1.2, visuals.weak_text_color()); + let r = 3.5; + painter.line_segment( + [center + egui::vec2(-r, r), center + egui::vec2(r, -r)], + stroke, + ); + painter.line_segment( + [center + egui::vec2(-r, -r), center + egui::vec2(r, r)], + stroke, + ); + } + + response +} diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs new file mode 100644 index 0000000..312a74a --- /dev/null +++ b/src/ui/title_bar.rs @@ -0,0 +1,121 @@ +//! Интегрированный в окно заголовок: перетаскивание и кнопки управления. + +use eframe::egui; + +use crate::api::AppApi; +use crate::constants::TITLE_BAR_HEIGHT; +use crate::ui::icons::{IconType, custom_icon_button}; + +/// Ответы виджетов заголовка: нужны, чтобы отличать клик по кнопке от драга окна. +pub struct TitleBarResponses { + pub close: egui::Response, + pub maximize: egui::Response, + pub minimize: egui::Response, + pub drag: egui::Response, +} + +impl TitleBarResponses { + fn any_button_hovered(&self) -> bool { + self.close.hovered() || self.maximize.hovered() || self.minimize.hovered() + } +} + +/// Рисует заголовок окна с именем активного документа и применяет действия +/// кнопок через [`AppApi`]. +pub fn render(ui: &mut egui::Ui, document_name: &str) -> TitleBarResponses { + let api = AppApi::global(); + let ctx = api.context(); + let is_maximized = api.is_maximized(); + + let bar_rect = egui::Rect::from_min_size( + ui.cursor().min, + egui::vec2(ui.available_width(), TITLE_BAR_HEIGHT), + ); + + // ВАЖНО: зона перетаскивания регистрируется ПЕРВОЙ. + // В egui hit-test побеждает виджет, добавленный последним, а `Response::hovered()` + // получают только виджеты из `hits.click` / `hits.drag`. Если зарегистрировать + // drag-зону после кнопок, она станет и клик-, и drag-целью — кнопки перестанут + // получать и клики, и подсветку при наведении. + let drag = ui.interact( + bar_rect, + ui.scope_id().with("title_bar_drag"), + egui::Sense::click_and_drag(), + ); + + let mut close = None; + let mut maximize = None; + let mut minimize = None; + + ui.allocate_ui_with_layout( + egui::vec2(ui.available_width(), TITLE_BAR_HEIGHT), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + ui.spacing_mut().item_spacing = egui::vec2(4.0, 0.0); + + // Текст заголовка (слева). + // `selectable(false)` обязателен: иначе лейбл получает Sense::click_and_drag, + // регистрируется поверх drag-зоны и перехватывает перетаскивание окна, + // а текст начинает выделяться мышкой. + ui.add_space(12.0); + ui.add( + egui::Label::new( + egui::RichText::new(format!("{document_name} — Custom Window App")) + .size(13.0) + .strong(), + ) + .selectable(false), + ); + + // Блок кнопок управления окном (смещаем в правый край через right_to_left) + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.add_space(8.0); // Отступ справа от крайней кнопки + + let button_size = egui::vec2(24.0, 24.0); + + // Порядок важен: в right_to_left первая добавленная кнопка — крайняя правая. + close = Some(custom_icon_button(ui, button_size, IconType::Close)); + let max_icon = if is_maximized { + IconType::Restore + } else { + IconType::Maximize + }; + maximize = Some(custom_icon_button(ui, button_size, max_icon)); + minimize = Some(custom_icon_button(ui, button_size, IconType::Minimize)); + }); + }, + ); + + let responses = TitleBarResponses { + close: close.expect("кнопка закрытия создаётся всегда"), + maximize: maximize.expect("кнопка разворота создаётся всегда"), + minimize: minimize.expect("кнопка сворачивания создаётся всегда"), + drag, + }; + + // Действия кнопок — только через глобальный API. + if responses.close.clicked() { + api.close(); + } + if responses.maximize.clicked() { + api.toggle_maximize(); + } + if responses.minimize.clicked() { + api.minimize(); + } + + // Перетаскивать окно можно за любую свободную часть заголовка (включая текст). + if !is_maximized + && !responses.any_button_hovered() + && responses.drag.drag_started_by(egui::PointerButton::Primary) + { + ctx.send_viewport_cmd(egui::ViewportCommand::StartDrag); + } + + // Двойной клик по заголовку — стандартное разворачивание/восстановление + if responses.drag.double_clicked() && !responses.any_button_hovered() { + api.toggle_maximize(); + } + + responses +}