commit c7d0f5ceeae9b89fe1be903aa71eca775cee1ada Author: Joakim Hulthe Date: Thu Jun 12 20:14:59 2025 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..283c01d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..81c139c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3629 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "accesskit" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "becf0eb5215b6ecb0a739c31c21bd83c4f326524c9b46b7e882d77559b60a529" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.16", + "once_cell", + "serde", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "arboard" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[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 = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.0", + "log", + "polling", + "rustix", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[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" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +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 0.9.4", + "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 0.9.4", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + +[[package]] +name = "ecolor" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "bytemuck", + "emath", + "serde", +] + +[[package]] +name = "eframe" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow 0.31.1 (git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9)", + "glow", + "glutin", + "glutin-winit", + "home", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "profiling", + "raw-window-handle", + "ron", + "serde", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.59.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.9.0", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "ron", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "ahash", + "arboard", + "bytemuck", + "egui", + "log", + "profiling", + "raw-window-handle", + "serde", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "epaint" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "serde", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "git+https://github.com/emilk/egui?rev=f2ce6424f3a32f47308fb9871d540c01377b2cd9#f2ce6424f3a32f47308fb9871d540c01377b2cd9" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[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 = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848" +dependencies = [ + "bitflags 2.9.0", + "cfg_aliases", + "cgl", + "core-foundation 0.9.4", + "dispatch", + "glutin_egl_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", +] + +[[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_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.9.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" +dependencies = [ + "bitflags 2.9.0", + "gpu-descriptor-types", + "hashbrown", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inkr" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "eframe", + "egui", + "egui_glow 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger", + "eyre", + "half", + "insta", + "log", + "rand", + "rfd", + "serde", + "wasm-bindgen-futures", + "zerocopy 0.8.25", +] + +[[package]] +name = "insta" +version = "1.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "jiff" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "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 = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", + "redox_syscall 0.5.11", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +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 = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.9.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.9.0", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash", + "spirv", + "strum", + "termcolor", + "thiserror 2.0.12", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "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.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +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.9.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-core-graphics", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "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.2", + "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.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.0", + "dispatch2 0.3.0", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.0", + "dispatch2 0.3.0", + "objc2 0.6.1", + "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.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[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.9.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "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.2", + "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.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.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.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "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.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "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.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "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 = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.11", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.25", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" + +[[package]] +name = "quick-xml" +version = "0.37.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "block2 0.6.1", + "dispatch2 0.2.0", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[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 = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "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.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "termcolor" +version = "1.4.1" +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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.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 = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +dependencies = [ + "bitflags 2.9.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +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.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e" +dependencies = [ + "core-foundation 0.10.0", + "home", + "jni", + "log", + "ndk-context", + "objc2 0.6.1", + "objc2-foundation 0.3.1", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "wgpu" +version = "24.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35904fb00ba2d2e0a4d002fcbbb6e1b89b574d272a50e5fc95f6e81cf281c245" +dependencies = [ + "arrayvec", + "bitflags 2.9.0", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c25545d479b47d3f0a8e373aceb2060b67c6eb841b24ac8c32348151c7a0c" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.9.0", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror 2.0.12", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.9.0", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror 2.0.12", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.9.0", + "js-sys", + "log", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.9.0", + "block2 0.5.1", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.0", + "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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive 0.8.25", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5bcbf74 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "inkr" +version = "0.1.0" +authors = [] +edition = "2024" + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] + +[features] +pinenote = [] + +[dependencies] +egui = "0.31" +eframe = { version = "0.31", default-features = false, features = [ + "glow", # alt: "wgpu". + "persistence", + "wayland", +] } +log = "0.4.27" +serde = { version = "1.0.219", features = ["derive"] } +egui_glow = "0.31.1" +rfd = { version = "0.15.3", default-features = false, features = ["gtk3"] } +rand = "0.9.1" +eyre = "0.6.12" +half = "2.6.0" +zerocopy = { version = "0.8.25", features = ["derive", "std"] } +base64 = "0.22.1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +env_logger = "0.11.8" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4.50" + +[patch.crates-io] +egui = { git = "https://github.com/emilk/egui", rev = "f2ce6424f3a32f47308fb9871d540c01377b2cd9" } +eframe = { git = "https://github.com/emilk/egui", rev = "f2ce6424f3a32f47308fb9871d540c01377b2cd9" } + +[dev-dependencies] +insta = { version = "1.43.1", features = ["yaml"] } +# egui = { path = "../egui/crates/egui" } +# eframe = { path = "../egui/crates/eframe" } + +[profile.release] +opt-level = 2 # fast and small wasm + +[profile.dev.package."*"] +opt-level = 2 # optimize dependencies in debug-builds diff --git a/README.md b/README.md new file mode 100644 index 0000000..84528a1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Inkr diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..bd6c484 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1,2 @@ +[build] +filehash = false diff --git a/assets/icon-1024.png b/assets/icon-1024.png new file mode 100644 index 0000000..2e62c98 Binary files /dev/null and b/assets/icon-1024.png differ diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000..bd09244 Binary files /dev/null and b/assets/icon-256.png differ diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..80eae35 --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,62 @@ + + + + diff --git a/assets/manifest.json b/assets/manifest.json new file mode 100644 index 0000000..8a12a62 --- /dev/null +++ b/assets/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Inkr", + "short_name": "inkr", + "icons": [ + { + "src": "./assets/icon-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./assets/icon-1024.png", + "sizes": "1024x1024", + "type": "image/png" + } + ], + "lang": "en-US", + "id": "/index.html", + "start_url": "./index.html", + "display": "standalone", + "background_color": "black", + "theme_color": "black" +} diff --git a/assets/sw.js b/assets/sw.js new file mode 100644 index 0000000..f905234 --- /dev/null +++ b/assets/sw.js @@ -0,0 +1,25 @@ +var cacheName = 'inkr'; +var filesToCache = [ + './', + './index.html', + './inkr.js', + './inkr.wasm', +]; + +/* Start the service worker and cache all of the app's content */ +self.addEventListener('install', function(e) { + e.waitUntil( + caches.open(cacheName).then(function(cache) { + return cache.addAll(filesToCache); + }) + ); +}); + +/* Serve cached content when offline */ +self.addEventListener('fetch', function(e) { + e.respondWith( + caches.match(e.request).then(function(response) { + return response || fetch(e.request); + }) + ); +}); diff --git a/fonts/Iosevka-Thin.ttc b/fonts/Iosevka-Thin.ttc new file mode 100644 index 0000000..b3475cc Binary files /dev/null and b/fonts/Iosevka-Thin.ttc differ diff --git a/fonts/IosevkaAile-Regular.ttc b/fonts/IosevkaAile-Regular.ttc new file mode 100644 index 0000000..e8a6284 Binary files /dev/null and b/fonts/IosevkaAile-Regular.ttc differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..ac1db28 --- /dev/null +++ b/index.html @@ -0,0 +1,135 @@ + + + + + + + + + Inkr + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Loading… +

+
+
+ + + + + + + diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..4b85a03 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,362 @@ +use std::{ + fs, + path::PathBuf, + sync::{Arc, mpsc}, + thread, +}; + +use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender}; +use egui::{ + Align, Button, Color32, FontData, FontDefinitions, PointerButton, RichText, ScrollArea, Stroke, +}; + +/// We derive Deserialize/Serialize so we can persist app state on shutdown. +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(default)] // if we add new fields, give them default values when deserializing old state +pub struct App { + preferences: Preferences, + + #[serde(skip)] + actions_tx: mpsc::Sender, + #[serde(skip)] + actions_rx: mpsc::Receiver, + + tabs: Vec<(TabId, Tab)>, + open_tab_index: Option, + + next_tab_id: TabId, +} + +#[derive(serde::Deserialize, serde::Serialize)] +enum Tab { + File(FileEditor), +} + +impl Tab { + pub fn title(&self) -> &str { + match self { + Tab::File(file_editor) => file_editor.title(), + } + } +} + +pub type TabId = usize; + +pub enum Action { + OpenFile(FileEditor), + MoveFile(TabId, PathBuf), + CloseTab(TabId), + // TODO + //ShowError { + // error: RichText + //}, +} + +impl Default for App { + fn default() -> Self { + let (actions_tx, actions_rx) = mpsc::channel(); + Self { + preferences: Preferences::default(), + actions_tx, + actions_rx, + tabs: vec![(1, Tab::File(FileEditor::new("note.md")))], + open_tab_index: None, + next_tab_id: 2, + } + } +} + +impl App { + /// Called once before the first frame. + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let mut fonts = FontDefinitions::empty(); + fonts.font_data = [ + //( + // "IosevkaAile-Thin", + // include_bytes!("../fonts/IosevkaAile-Thin.ttc").as_slice(), + //), + //( + // "IosevkaAile-ExtraLight", + // include_bytes!("../fonts/IosevkaAile-ExtraLight.ttc").as_slice(), + //), + //( + // "IosevkaAile-Light", + // include_bytes!("../fonts/IosevkaAile-Light.ttc").as_slice(), + //), + ( + "IosevkaAile-Regular", + include_bytes!("../fonts/IosevkaAile-Regular.ttc").as_slice(), + ), + //( + // "IosevkaAile-Medium", + // include_bytes!("../fonts/IosevkaAile-Medium.ttc").as_slice(), + //), + //( + // "IosevkaAile-Bold", + // include_bytes!("../fonts/IosevkaAile-Bold.ttc").as_slice(), + //), + ( + "Iosevka-Thin", + include_bytes!("../fonts/Iosevka-Thin.ttc").as_slice(), + ), + //( + // "Iosevka-ExtraLight", + // include_bytes!("../fonts/Iosevka-ExtraLight.ttc").as_slice(), + //), + //( + // "Iosevka-Light", + // include_bytes!("../fonts/Iosevka-Light.ttc").as_slice(), + //), + //( + // "Iosevka-Medium", + // include_bytes!("../fonts/Iosevka-Medium.ttc").as_slice(), + //), + //( + // "Iosevka-Regular", + // include_bytes!("../fonts/Iosevka-Regular.ttc").as_slice(), + //), + //( + // "Iosevka-Heavy", + // include_bytes!("../fonts/Iosevka-Heavy.ttc").as_slice(), + //), + ] + .into_iter() + .map(|(name, data)| (name.to_string(), Arc::new(FontData::from_static(data)))) + .collect(); + + fonts.families.insert( + egui::FontFamily::Proportional, + vec!["IosevkaAile-Regular".into()], + ); + + fonts + .families + .insert(egui::FontFamily::Monospace, vec!["Iosevka-Thin".into()]); + + cc.egui_ctx.set_fonts(fonts); + + cc.egui_ctx.style_mut(|style| { + // TODO: change color of text in TextEdit + style.visuals.widgets.noninteractive.fg_stroke = + Stroke::new(1.0, Color32::from_rgb(200, 200, 200)); + }); + + // Load previous app state (if any). + // Note that you must enable the `persistence` feature for this to work. + if let Some(storage) = cc.storage { + return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); + } + + Default::default() + } + + fn actions_tx(&self, ctx: &egui::Context) -> GuiSender { + GuiSender::new(self.actions_tx.clone(), ctx) + } + + fn handle_action(&mut self, action: Action) { + match action { + Action::OpenFile(file_editor) => { + self.open_tab(Tab::File(file_editor)); + } + Action::MoveFile(tab_id, new_path) => { + let tab = self.tabs.iter_mut().find(|(id, _)| &tab_id == id); + let Some((_, tab)) = tab else { return }; + let Tab::File(editor) = tab; // else { return }; + + editor.set_path(new_path); + } + Action::CloseTab(id) => { + // TODO: check if the file is dirty and ask to save it first? + self.tabs.retain(|(tab_id, _)| &id != tab_id); + } + } + } +} + +impl eframe::App for App { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, eframe::APP_KEY, self); + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.preferences.apply(ctx); + + while let Ok(action) = self.actions_rx.try_recv() { + self.handle_action(action); + } + + if self.open_tab_index >= Some(self.tabs.len()) { + self.open_tab_index = Some(self.tabs.len().saturating_sub(1)); + } + + //ctx.input_mut(|input| { + // if input.consume_key(Modifiers::CTRL, Key::H) { + // self.buffer.push(BufferItem::Painting(Default::default())); + // } + //}); + + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + // The top panel is often a good place for a menu bar: + + egui::containers::menu::Bar::new().ui(ui, |ui| { + // NOTE: no File->Quit on web pages! + ui.menu_button("Menu ⚙", |ui| { + ui.label(RichText::new("Action").weak()); + + if ui.button("New File").clicked() { + let file = FileEditor::new("note.md"); + self.open_tab(Tab::File(file)); + } + + if ui.button("Open File").clicked() { + let actions_tx = self.actions_tx(ui.ctx()); + thread::spawn(move || { + let file = rfd::FileDialog::new().pick_file(); + + let Some(file_path) = file else { return }; + + let text = match fs::read_to_string(&file_path) { + Ok(text) => text, + Err(e) => { + log::error!("Failed to read {file_path:?}: {e}"); + return; + } + }; + + let editor = FileEditor::from_file(file_path, &text); + let _ = actions_tx.send(Action::OpenFile(editor)); + }); + } + + if ui.button("Open Folder").clicked() { + log::error!("Open Folder not implemented"); + } + + if ui + .add_enabled(self.open_tab_index.is_some(), Button::new("Close File")) + .clicked() + { + if let Some(i) = self.open_tab_index.take() { + self.tabs.remove(i); + } + } + + let open_file = + self.open_tab_index + .and_then(|i| self.tabs.get(i)) + .and_then(|(id, tab)| match tab { + Tab::File(file_editor) => Some((*id, file_editor)), + }); + + let open_file_with_path = open_file + .clone() + .and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor))); + + if ui + .add_enabled(open_file_with_path.is_some(), Button::new("Save")) + .clicked() + { + if let Some((file_path, file_editor)) = open_file_with_path { + let text = file_editor.to_string(); + let file_path = file_path.to_owned(); + std::thread::spawn(move || { + if let Err(e) = fs::write(file_path, text.as_bytes()) { + log::error!("{e}"); + }; + }); + } + } + + if ui + .add_enabled(open_file.is_some(), Button::new("Save As")) + .clicked() + { + let actions_tx = self.actions_tx(ui.ctx()); + let (tab_id, editor) = + open_file.expect("We checked that open_file is_some"); + let text = editor.to_string(); + std::thread::spawn(move || { + let Some(file_path) = rfd::FileDialog::new().save_file() else { + return; + }; + + if let Err(e) = fs::write(&file_path, text.as_bytes()) { + log::error!("{e}"); + return; + }; + + let _ = actions_tx.send(Action::MoveFile(tab_id, file_path)); + }); + } + + ui.add_space(8.0); + + self.preferences.show(ui); + + ui.add_space(8.0); + + if cfg!(not(target_arch = "wasm32")) && ui.button("Quit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + + ui.add_space(16.0); + + ui.add_space(16.0); + + ScrollArea::horizontal().show(ui, |ui| { + for (i, (tab_id, tab)) in self.tabs.iter().enumerate() { + let selected = self.open_tab_index == Some(i); + let mut button = Button::new(tab.title()).selected(selected); + + let dirty = i == 0; // TODO: mark as dirty when contents hasn't been saved + if dirty { + button = button.right_text(RichText::new("*").strong()) + } + + let response = ui.add(button); + + if response.clicked() { + self.open_tab_index = Some(i); + } else if response.clicked_by(PointerButton::Secondary) { + let _ = self.actions_tx(ui.ctx()).send(Action::CloseTab(*tab_id)); + } + } + }); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + if let Some(Tab::File(file_editor)) = self + .open_tab_index + .and_then(|i| self.tabs.get_mut(i)) + .map(|(_tab_id, tab)| tab) + { + file_editor.show(ui, &self.preferences); + } + + ui.with_layout(egui::Layout::bottom_up(Align::LEFT), |ui| { + egui::warn_if_debug_build(ui); + }); + }); + } +} + +impl App { + /// Figure out where we should insert the next tab. + fn insert_tab_at(&self) -> usize { + match self.open_tab_index { + None => 0, + Some(i) => (i + 1).min(self.tabs.len()), + } + } + + /// Open a [Tab]. + fn open_tab(&mut self, tab: Tab) { + let i = self.insert_tab_at(); + let id = self.next_tab_id; + self.next_tab_id += 1; + self.tabs.insert(i, (id, tab)); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..af94cd2 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1 @@ +pub const MAX_NOTE_WIDTH: f32 = 600.0; diff --git a/src/custom_code_block.rs b/src/custom_code_block.rs new file mode 100644 index 0000000..dd0c274 --- /dev/null +++ b/src/custom_code_block.rs @@ -0,0 +1,135 @@ +use std::{ + fmt::{self, Display, Write}, + iter, +}; + +const TICKS: &str = "```"; +const NL_TICKS: &str = "\n```"; + +/// Wrap a [Display] in markdown code-block ticks ([TICKS]) +pub fn to_custom_code_block(key: &str, content: impl Display) -> String { + let mut out = String::new(); + write_custom_code_block(&mut out, key, content).unwrap(); + out +} + +/// Wrap a [Display] in markdown code-block ticks ([TICKS]) +pub fn write_custom_code_block(mut w: impl Write, key: &str, content: impl Display) -> fmt::Result { + write!(w, "{TICKS}{key}\n{content}\n{TICKS}") +} + +/// Try to unwrap a string from within markdown code-block ticks ([TICKS]) +pub fn try_from_custom_code_block<'a>(key: &str, code_block: &'a str) -> Option<&'a str> { + code_block + .trim() + .strip_prefix(TICKS)? + .strip_prefix(key)? + .strip_prefix("\n")? + .strip_suffix(TICKS)? + .strip_suffix("\n") +} + +#[derive(Debug, Clone, Copy)] +pub enum MdItem<'a> { + /// A line of regular markdown, but not a code block. + Line(&'a str), + + /// A markdown code block + CodeBlock { + /// The key or language of the code block. + key: &'a str, + + /// Everything in-between the ticks. + content: &'a str, + + /// The entire code-block, including ticks. + span: &'a str, + }, +} + +/// Iterate over code-blocks in a markdown string +pub fn iter_lines_and_code_blocks(mut md: &str) -> impl Iterator> { + iter::from_fn(move || { + if md.is_empty() { + return None; + } + + if !md.starts_with(TICKS) { + // line does not start with ticks, return a normal line. + let line; + if let Some(i) = md.find('\n') { + let i = i + 1; + line = &md[..i]; + md = &md[i..]; + } else { + line = md; + md = ""; + } + return Some(MdItem::Line(line)); + } + + let mut i = TICKS.len(); + let from_key = &md[i..]; + + let Some((key, from_content)) = from_key.split_once('\n') else { + // no more newlines, return the remaining string as the final line. + let rest = md; + md = ""; + return Some(MdItem::Line(rest)); + }; + i += key.len() + "\n".len(); + + let Some(end) = from_content.find(NL_TICKS) else { + // no closing ticks, return a line instead. + let line; + if let Some(i) = md.find('\n') { + let i = i + 1; + line = &md[..i]; + md = &md[i..]; + } else { + line = md; + md = ""; + } + return Some(MdItem::Line(line)); + }; + let content = &from_content[..end]; + i += end + NL_TICKS.len(); + + if md[i..].starts_with("\n") { + i += 1; + }; + let span = &md[..i]; + md = &md[i..]; + + Some(MdItem::CodeBlock { key, content, span }) + }) +} + +#[cfg(test)] +mod test { + use super::iter_lines_and_code_blocks; + + #[test] + fn iter_markdown() { + let markdown = r#" +# Hello world +## Subheader +- 1 +```foo + whatever + some code + Hi mom! +``` + +```` # wrong number of ticks, but that's ok + ``` # indented ticks +``` + +``` # no closing ticks + "#; + + let list: Vec<_> = iter_lines_and_code_blocks(markdown).collect(); + insta::assert_snapshot!(markdown); + insta::assert_debug_snapshot!(list); + } +} diff --git a/src/easy_mark/easy_mark_highlighter.rs b/src/easy_mark/easy_mark_highlighter.rs new file mode 100644 index 0000000..41584d9 --- /dev/null +++ b/src/easy_mark/easy_mark_highlighter.rs @@ -0,0 +1,243 @@ +use egui::text::{CCursorRange, LayoutJob}; + +use crate::easy_mark::easy_mark_parser; + +/// Highlight easymark, memoizing previous output to save CPU. +/// +/// In practice, the highlighter is fast enough not to need any caching. +#[derive(Default)] +pub struct MemoizedHighlighter { + style: egui::Style, + code: String, + output: LayoutJob, +} + +impl MemoizedHighlighter { + pub fn highlight( + &mut self, + egui_style: &egui::Style, + code: &str, + cursor: Option, + ) -> LayoutJob { + if (&self.style, self.code.as_str()) != (egui_style, code) { + self.style = egui_style.clone(); + code.clone_into(&mut self.code); + self.output = highlight_easymark(egui_style, code, cursor); + } + self.output.clone() + } +} + +pub fn highlight_easymark( + egui_style: &egui::Style, + mut text: &str, + + // TODO: hide special characters where cursor isn't + _cursor: Option, +) -> LayoutJob { + let mut job = LayoutJob::default(); + let mut style = easy_mark_parser::Style::default(); + let mut start_of_line = true; + + const CODE_INDENT: f32 = 10.0; + + while !text.is_empty() { + if start_of_line && text.starts_with("```") { + let astyle = format_from_style( + egui_style, + &easy_mark_parser::Style { + code: true, + ..Default::default() + }, + ); + + // Render the initial backticks as spaces + text = &text[3..]; + job.append(" ", CODE_INDENT, astyle.clone()); + + match text.find("\n```") { + Some(n) => { + for line in text[..n + 1].lines() { + job.append(line, CODE_INDENT, astyle.clone()); + job.append("\n", 0.0, astyle.clone()); + } + // Render the final backticks as spaces + job.append(" ", CODE_INDENT, astyle); + text = &text[n + 4..]; + } + None => { + job.append(text, 0.0, astyle.clone()); + text = ""; + } + }; + style = Default::default(); + continue; + } + + if text.starts_with('`') { + style.code = true; + let end = text[1..] + .find(&['`', '\n'][..]) + .map_or_else(|| text.len(), |i| i + 2); + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); + text = &text[end..]; + style.code = false; + continue; + } + + let skip; + + // zero-width space + let _zws = "\u{200b}"; + + let mut apply_basic_style = + |text: &mut &str, + style: &mut easy_mark_parser::Style, + access: fn(&mut easy_mark_parser::Style) -> &mut bool| { + let skip = if *access(style) { + // Include the character that is ending this style: + job.append(&text[..1], 0.0, format_from_style(egui_style, style)); + *text = &text[1..]; + 0 + } else { + 1 + }; + *access(style) ^= true; + skip + }; + + if text.starts_with('*') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.strong); + } else if text.starts_with('/') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.italics); + } else if text.starts_with('_') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.underline); + } else if text.starts_with('$') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.small); + } else if text.starts_with('~') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.strikethrough); + } else if text.starts_with('^') { + skip = apply_basic_style(&mut text, &mut style, |style| &mut style.raised); + } else if text.starts_with('\\') && text.len() >= 2 { + skip = 2; + } else if start_of_line && text.starts_with(' ') { + // we don't preview indentation, because it is confusing + skip = 1; + } else if start_of_line && text.starts_with("###### ") { + style.heading = true; + skip = 7; + } else if start_of_line && text.starts_with("##### ") { + style.heading = true; + skip = 6; + } else if start_of_line && text.starts_with("#### ") { + style.heading = true; + skip = 5; + } else if start_of_line && text.starts_with("### ") { + style.heading = true; + skip = 4; + } else if start_of_line && text.starts_with("## ") { + style.heading = true; + skip = 3; + } else if start_of_line && text.starts_with("# ") { + style.heading = true; + skip = 2; + } else if start_of_line && text.starts_with("> ") { + style.quoted = true; + skip = 2; + // we don't preview indentation, because it is confusing + } else if start_of_line && text.trim_start().starts_with("- ") { + job.append("• ", 0.0, format_from_style(egui_style, &style)); + text = &text[2..]; + skip = 0; + // we don't preview indentation, because it is confusing + } else { + skip = 0; + } + // Note: we don't preview underline, strikethrough and italics because it confuses things. + + // Swallow everything up to the next special character: + let line_end = text[skip..] + .find('\n') + .map_or_else(|| text.len(), |i| (skip + i + 1)); + let end = text[skip..] + .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..]) + .map_or_else(|| text.len(), |i| (skip + i).max(1)); + + if line_end <= end { + job.append( + &text[..line_end], + 0.0, + format_from_style(egui_style, &style), + ); + text = &text[line_end..]; + start_of_line = true; + style = Default::default(); + } else { + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); + text = &text[end..]; + start_of_line = false; + } + } + + job +} + +fn format_from_style( + egui_style: &egui::Style, + emark_style: &easy_mark_parser::Style, +) -> egui::text::TextFormat { + use egui::{Align, Color32, Stroke, TextStyle}; + + let color = if emark_style.strong || emark_style.heading { + egui_style.visuals.strong_text_color() + } else if emark_style.quoted { + egui_style.visuals.weak_text_color() + } else { + egui_style.visuals.text_color() + }; + + let text_style = if emark_style.heading { + TextStyle::Heading + } else if emark_style.code { + TextStyle::Monospace + } else if emark_style.small | emark_style.raised { + TextStyle::Small + } else { + TextStyle::Body + }; + + let background = if emark_style.code { + egui_style.visuals.code_bg_color + } else { + Color32::TRANSPARENT + }; + + let underline = if emark_style.underline { + Stroke::new(1.0, color) + } else { + Stroke::NONE + }; + + let strikethrough = if emark_style.strikethrough { + Stroke::new(1.0, color) + } else { + Stroke::NONE + }; + + let valign = if emark_style.raised { + Align::TOP + } else { + Align::BOTTOM + }; + + egui::text::TextFormat { + font_id: text_style.resolve(egui_style), + color, + background, + italics: emark_style.italics, + underline, + strikethrough, + valign, + ..Default::default() + } +} diff --git a/src/easy_mark/easy_mark_parser.rs b/src/easy_mark/easy_mark_parser.rs new file mode 100644 index 0000000..b12b3f4 --- /dev/null +++ b/src/easy_mark/easy_mark_parser.rs @@ -0,0 +1,346 @@ +//! A parser for `EasyMark`: a very simple markup language. +//! +//! WARNING: `EasyMark` is subject to change. +// +//! # `EasyMark` design goals: +//! 1. easy to parse +//! 2. easy to learn +//! 3. similar to markdown + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Item<'a> { + /// `\n` + // TODO(emilk): add Style here so empty heading still uses up the right amount of space. + Newline, + + /// Text + Text(Style, &'a str), + + /// title, url + Hyperlink(Style, &'a str, &'a str), + + /// leading space before e.g. a [`Self::BulletPoint`]. + Indentation(usize), + + /// > + QuoteIndent, + + /// - a point well made. + BulletPoint, + + /// 1. numbered list. The string is the number(s). + NumberedPoint(&'a str), + + /// --- + Separator, + + /// language, code + CodeBlock(&'a str, &'a str), +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Style { + /// # heading (large text) + pub heading: bool, + + /// > quoted (slightly dimmer color or other font style) + pub quoted: bool, + + /// `code` (monospace, some other color) + pub code: bool, + + /// self.strong* (emphasized, e.g. bold) + pub strong: bool, + + /// _underline_ + pub underline: bool, + + /// ~strikethrough~ + pub strikethrough: bool, + + /// /italics/ + pub italics: bool, + + /// $small$ + pub small: bool, + + /// ^raised^ + pub raised: bool, +} + +/// Parser for the `EasyMark` markup language. +pub struct Parser<'a> { + /// The remainder of the input text + s: &'a str, + + /// Are we at the start of a line? + start_of_line: bool, + + /// Current self.style. Reset after a newline. + style: Style, +} + +impl<'a> Parser<'a> { + pub fn new(s: &'a str) -> Self { + Self { + s, + start_of_line: true, + style: Style::default(), + } + } + + /// `1. `, `42. ` etc. + fn numbered_list(&mut self) -> Option> { + let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count(); + if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) { + let number = &self.s[..n_digits]; + self.s = &self.s[(n_digits + 2)..]; + self.start_of_line = false; + return Some(Item::NumberedPoint(number)); + } + None + } + + // ```{language}\n{code}``` + fn code_block(&mut self) -> Option> { + if let Some(language_start) = self.s.strip_prefix("```") { + if let Some(newline) = language_start.find('\n') { + let language = &language_start[..newline]; + let code_start = &language_start[newline + 1..]; + if let Some(end) = code_start.find("\n```") { + let code = &code_start[..end].trim(); + self.s = &code_start[end + 4..]; + self.start_of_line = false; + return Some(Item::CodeBlock(language, code)); + } else { + self.s = ""; + return Some(Item::CodeBlock(language, code_start)); + } + } + } + None + } + + // `code` + fn inline_code(&mut self) -> Option> { + if let Some(rest) = self.s.strip_prefix('`') { + self.s = rest; + self.start_of_line = false; + self.style.code = true; + let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(end) = rest_of_line.find('`') { + let item = Item::Text(self.style, &self.s[..end]); + self.s = &self.s[end + 1..]; + self.style.code = false; + return Some(item); + } else { + let end = rest_of_line.len(); + let item = Item::Text(self.style, rest_of_line); + self.s = &self.s[end..]; + self.style.code = false; + return Some(item); + } + } + None + } + + /// `` or `[link](url)` + fn url(&mut self) -> Option> { + if self.s.starts_with('<') { + let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(url_end) = this_line.find('>') { + let url = &self.s[1..url_end]; + self.s = &self.s[url_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, url, url)); + } + } + + // [text](url) + if self.s.starts_with('[') { + let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(bracket_end) = this_line.find(']') { + let text = &this_line[1..bracket_end]; + if this_line[bracket_end + 1..].starts_with('(') { + if let Some(parens_end) = this_line[bracket_end + 2..].find(')') { + let parens_end = bracket_end + 2 + parens_end; + let url = &self.s[bracket_end + 2..parens_end]; + self.s = &self.s[parens_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, text, url)); + } + } + } + } + None + } +} + +impl<'a> Iterator for Parser<'a> { + type Item = Item<'a>; + + fn next(&mut self) -> Option { + loop { + if self.s.is_empty() { + return None; + } + + // \n + if self.s.starts_with('\n') { + self.s = &self.s[1..]; + self.start_of_line = true; + self.style = Style::default(); + return Some(Item::Newline); + } + + // Ignore line break (continue on the same line) + if self.s.starts_with("\\\n") && self.s.len() >= 2 { + self.s = &self.s[2..]; + self.start_of_line = false; + continue; + } + + // \ escape (to show e.g. a backtick) + if self.s.starts_with('\\') && self.s.len() >= 2 { + let text = &self.s[1..2]; + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::Text(self.style, text)); + } + + if self.start_of_line { + // leading space (indentation) + if self.s.starts_with(' ') { + let length = self.s.find(|c| c != ' ').unwrap_or(self.s.len()); + self.s = &self.s[length..]; + self.start_of_line = true; // indentation doesn't count + return Some(Item::Indentation(length)); + } + + // # Heading + if let Some(after) = self.s.strip_prefix("# ") { + self.s = after; + self.start_of_line = false; + self.style.heading = true; + continue; + } + + // > quote + if let Some(after) = self.s.strip_prefix("> ") { + self.s = after; + self.start_of_line = true; // quote indentation doesn't count + self.style.quoted = true; + return Some(Item::QuoteIndent); + } + + // - bullet point + if self.s.starts_with("- ") { + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::BulletPoint); + } + + // `1. `, `42. ` etc. + if let Some(item) = self.numbered_list() { + return Some(item); + } + + // --- separator + if let Some(after) = self.s.strip_prefix("---") { + self.s = after.trim_start_matches('-'); // remove extra dashes + self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline + self.start_of_line = false; + return Some(Item::Separator); + } + + // ```{language}\n{code}``` + if let Some(item) = self.code_block() { + return Some(item); + } + } + + // `code` + if let Some(item) = self.inline_code() { + return Some(item); + } + + if let Some(rest) = self.s.strip_prefix('*') { + self.s = rest; + self.start_of_line = false; + self.style.strong = !self.style.strong; + continue; + } + if let Some(rest) = self.s.strip_prefix('_') { + self.s = rest; + self.start_of_line = false; + self.style.underline = !self.style.underline; + continue; + } + if let Some(rest) = self.s.strip_prefix('~') { + self.s = rest; + self.start_of_line = false; + self.style.strikethrough = !self.style.strikethrough; + continue; + } + if let Some(rest) = self.s.strip_prefix('/') { + self.s = rest; + self.start_of_line = false; + self.style.italics = !self.style.italics; + continue; + } + if let Some(rest) = self.s.strip_prefix('$') { + self.s = rest; + self.start_of_line = false; + self.style.small = !self.style.small; + continue; + } + if let Some(rest) = self.s.strip_prefix('^') { + self.s = rest; + self.start_of_line = false; + self.style.raised = !self.style.raised; + continue; + } + + // `` or `[link](url)` + if let Some(item) = self.url() { + return Some(item); + } + + // Swallow everything up to the next special character: + let end = self + .s + .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..]) + .map_or_else(|| self.s.len(), |special| special.max(1)); + + let item = Item::Text(self.style, &self.s[..end]); + self.s = &self.s[end..]; + self.start_of_line = false; + return Some(item); + } + } +} + +#[test] +fn test_easy_mark_parser() { + let items: Vec<_> = Parser::new("~strikethrough `code`~").collect(); + assert_eq!( + items, + vec![ + Item::Text( + Style { + strikethrough: true, + ..Default::default() + }, + "strikethrough " + ), + Item::Text( + Style { + code: true, + strikethrough: true, + ..Default::default() + }, + "code" + ), + ] + ); +} diff --git a/src/easy_mark/mod.rs b/src/easy_mark/mod.rs new file mode 100644 index 0000000..96f13de --- /dev/null +++ b/src/easy_mark/mod.rs @@ -0,0 +1,7 @@ +//! Experimental markup language + +mod easy_mark_highlighter; +pub mod easy_mark_parser; + +pub use easy_mark_highlighter::{MemoizedHighlighter, highlight_easymark}; +pub use easy_mark_parser as parser; diff --git a/src/file_editor.rs b/src/file_editor.rs new file mode 100644 index 0000000..8469c8b --- /dev/null +++ b/src/file_editor.rs @@ -0,0 +1,356 @@ +use std::{ + cmp::Ordering, + fmt::{self, Display}, + ops::{Div as _, Sub as _}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use egui::{ + Align, Button, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, Widget as _, vec2, +}; + +use crate::{ + custom_code_block::{MdItem, iter_lines_and_code_blocks}, + painting::{self, Handwriting, HandwritingStyle}, + preferences::Preferences, + text_editor::MdTextEdit, +}; + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct FileEditor { + title: String, + pub path: Option, + pub buffer: Vec, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub enum BufferItem { + Text(MdTextEdit), + Handwriting(Handwriting), +} + +impl FileEditor { + pub fn new(title: impl Into) -> Self { + let buffer = vec![BufferItem::Text(MdTextEdit::new())]; + Self { + title: title.into(), + path: None, + buffer, + } + } + + pub fn from_file(file_path: PathBuf, contents: &str) -> Self { + let file_title = file_path + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_else(|| String::from("untitled.md")); + Self { + title: file_title, + path: Some(file_path), + ..FileEditor::from(contents) + } + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() + } + + pub fn show(&mut self, ui: &mut Ui, preferences: &Preferences) { + ui.vertical_centered_justified(|ui| { + ui.heading(&self.title); + + const MAX_NOTE_WIDTH: f32 = 600.0; + + ui.horizontal(|ui| { + ui.label("new"); + if ui.button("text").clicked() { + self.buffer.push(BufferItem::Text(Default::default())); + } + if ui.button("writing").clicked() { + self.buffer + .push(BufferItem::Handwriting(Default::default())); + } + }); + + ScrollArea::vertical().show(ui, |ui| { + ui.horizontal(|ui| { + let side_padding = ui.available_width().sub(MAX_NOTE_WIDTH).max(0.0).div(2.0); + ui.add_space(side_padding); + ui.vertical(|ui| { + ui.set_max_width(MAX_NOTE_WIDTH); + self.show_contents(ui, preferences); + }); + ui.add_space(side_padding); + }); + }); + }); + } + + fn show_contents(&mut self, ui: &mut Ui, preferences: &Preferences) { + if self.buffer.is_empty() { + self.buffer.push(BufferItem::Text(Default::default())); + } + + struct DraggingItem { + index: usize, + } + + let mut drop_from_to: Option<(usize, usize)> = None; + let is_dragging = DragAndDrop::has_payload_of_type::(ui.ctx()); + let drag_zone_height = 10.0; + + // Iterate over buffer items using `retain` so that we can handle deletions + let mut i = 0usize..; + let len = self.buffer.len(); + self.buffer.retain_mut(|item| { + let i = i.next().unwrap(); + let is_first = i == 0; + let is_last = i == len - 1; + + let mut retain = true; + + if is_dragging { + let (_, drop) = ui.dnd_drop_zone::(Frame::NONE, |ui| { + ui.set_min_size(vec2(ui.available_width(), drag_zone_height)); + }); + if let Some(drop) = drop { + drop_from_to = Some((drop.index, i)); + } + } else { + // the dnd_drop_zone adds 3pts work of extra space + ui.add_space(drag_zone_height + 3.0); + } + + ui.horizontal(|ui| { + // We don't know how tall the buffer item will be, so we'll reserve + // some horizontal space here and come back to drawing the dragger + // later. + let (dragger_id, mut dragger_rect) = ui.allocate_space(Vec2::new(20.0, 1.0)); + + // Leave some space at the end for the delete button.. + let w = ui.available_width(); + let item_size = Vec2::new(w - 20.0, 0.0); + + let item_response = ui.allocate_ui(item_size, |ui| match item { + BufferItem::Text(text_edit) => { + text_edit.ui(ui); + } + BufferItem::Handwriting(painting) => { + let style = HandwritingStyle { + animate: preferences.animations, + ..HandwritingStyle::from_theme(ui.ctx().theme()) + }; + painting.ui(&style, ui); + } + }); + + // Delete-button + if ui.button("x").clicked() { + retain = false; + ui.ctx().request_repaint(); + } + + // Draw the dragger using the height from the buffer item + dragger_rect.set_height(item_response.response.rect.height()); + + // Controls for moving the buffer item + ui.allocate_new_ui( + UiBuilder::new() + .max_rect(dragger_rect) + .layout(Layout::top_down(Align::Center)), + |ui| { + let up_button_response = ui.add_enabled(!is_first, Button::new("⇡")); + if up_button_response.clicked() { + drop_from_to = Some((i, i - 1)); + } + + ui.dnd_drag_source(dragger_id, DraggingItem { index: i }, |ui| { + Button::new("≡") + .min_size( + // Use all available height, save for the height taken up by + // the up/down buttons + padding. Assume down-button is the + // equally tall as the up-button. + dragger_rect.size() + - Vec2::Y * (up_button_response.rect.height() * 2.0 + 4.0), + ) + .ui(ui); + }); + + if ui.add_enabled(!is_last, Button::new("⇣")).clicked() { + drop_from_to = Some((i, i + 2)); + } + }, + ); + }); + + retain + }); + + if is_dragging { + let (_, drop) = ui.dnd_drop_zone::(Frame::NONE, |ui| { + ui.set_min_size(vec2(ui.available_width(), drag_zone_height)); + }); + if let Some(drop) = drop { + drop_from_to = Some((drop.index, self.buffer.len())); + } + } else { + // the dnd_drop_zone adds 3.0pts work of extra space + ui.add_space(drag_zone_height + 3.0); + } + + // Handle drag-and-dropping buffer items + // TODO: make sure nothing was removed from self.buffer this frame + if let Some((from, to)) = drop_from_to { + if from < self.buffer.len() { + match from.cmp(&to) { + Ordering::Greater => { + let item = self.buffer.remove(from); + self.buffer.insert(to, item); + } + Ordering::Less => { + let item = self.buffer.remove(from); + self.buffer.insert(to - 1, item); + } + Ordering::Equal => {} + } + } + } + } + + pub fn set_path(&mut self, new_path: PathBuf) { + let Some(title) = new_path.file_name() else { + log::error!("No filename in path {new_path:?}"); + return; + }; + self.title = title.to_string_lossy().to_string(); + self.path = Some(new_path); + } +} + +impl Display for BufferItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BufferItem::Text(md_text_edit) => Display::fmt(md_text_edit, f), + BufferItem::Handwriting(handwriting) => Display::fmt(handwriting, f), + } + } +} + +impl Display for FileEditor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + for item in &self.buffer { + if !first { + writeln!(f)?; + } + + first = false; + + write!(f, "{item}")?; + } + Ok(()) + } +} + +impl From<&str> for FileEditor { + fn from(s: &str) -> Self { + let mut editor = FileEditor::new("note.md"); + let buffer = &mut editor.buffer; + + let push_text = |buffer: &mut Vec, text| match buffer.last_mut() { + Some(BufferItem::Text(text_edit)) => text_edit.text.push_str(text), + _ => { + let mut text_edit = MdTextEdit::new(); + text_edit.text.push_str(text); + buffer.push(BufferItem::Text(text_edit)); + } + }; + + for item in iter_lines_and_code_blocks(s) { + match item { + MdItem::Line(line) => push_text(buffer, line), + MdItem::CodeBlock { key, content, span } => match key { + painting::CODE_BLOCK_KEY => match Handwriting::from_str(span) { + Ok(handwriting) => { + if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() { + if text_edit.text.ends_with('\n') { + text_edit.text.pop(); + if text_edit.text.is_empty() { + buffer.pop(); + } + } + }; + buffer.push(BufferItem::Handwriting(handwriting)) + } + Err(e) => { + log::error!("Failed to decode handwriting {content:?}: {e}"); + push_text(buffer, span); + } + }, + _ => push_text(buffer, span), + }, + } + } + + editor + } +} + +#[cfg(test)] +mod test { + + use crate::file_editor::BufferItem; + + use super::FileEditor; + + #[test] + fn from_str_and_back_1() { + let markdown = r#" +# Hello world! + +This is some text. +Here's some handwriting: +```handwriting +DgB0UUlNeFFJTX9RUE2pUYZNDlIATotSjk4AUwxPaFODT89T608UVBtQL1QqUDtULlBDVDFQSVQuUA== +``` + +And here's some more text :D +``` +with a regular code-block! +```"#; + + println!("{markdown}"); + println!(); + println!(); + println!("{markdown:?}"); + println!(); + println!(); + + let file_editor = FileEditor::from(markdown); + + for item in &file_editor.buffer { + match item { + BufferItem::Text(md_text_edit) => { + println!("{:?}", md_text_edit.text); + } + BufferItem::Handwriting(_) => { + println!(""); + } + } + } + + println!(); + println!(); + + let serialized = file_editor.to_string(); + assert_eq!( + markdown, serialized, + "FileEditor should preserve formatting" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..38ec018 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +#![warn(clippy::all, rust_2018_idioms)] + +pub mod app; +pub mod constants; +pub mod custom_code_block; +pub mod easy_mark; +pub mod file_editor; +pub mod painting; +pub mod preferences; +pub mod rasterizer; +pub mod text_editor; +pub mod util; + +pub use app::App; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3a49ae9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +// When compiling natively: +#[cfg(not(target_arch = "wasm32"))] +fn main() -> eframe::Result { + env_logger::init(); + + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([400.0, 300.0]) + .with_min_inner_size([300.0, 220.0]) + .with_icon( + // NOTE: Adding an icon is optional + eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]) + .expect("Failed to load icon"), + ), + ..Default::default() + }; + eframe::run_native( + "Inkr", + native_options, + Box::new(|cc| Ok(Box::new(inkr::App::new(cc)))), + ) +} + +// When compiling to web using trunk: +#[cfg(target_arch = "wasm32")] +fn main() { + use eframe::wasm_bindgen::JsCast as _; + + // Redirect `log` message to `console.log` and friends: + eframe::WebLogger::init(log::LevelFilter::Debug).ok(); + + let web_options = eframe::WebOptions::default(); + + wasm_bindgen_futures::spawn_local(async { + let document = web_sys::window() + .expect("No window") + .document() + .expect("No document"); + + let canvas = document + .get_element_by_id("the_canvas_id") + .expect("Failed to find the_canvas_id") + .dyn_into::() + .expect("the_canvas_id was not a HtmlCanvasElement"); + + let start_result = eframe::WebRunner::new() + .start( + canvas, + web_options, + Box::new(|cc| Ok(Box::new(eframe_template::TemplateApp::new(cc)))), + ) + .await; + + // Remove the loading text and spinner: + if let Some(loading_text) = document.get_element_by_id("loading_text") { + match start_result { + Ok(_) => { + loading_text.remove(); + } + Err(e) => { + loading_text.set_inner_html( + "

The app has crashed. See the developer console for details.

", + ); + panic!("Failed to start eframe: {e:?}"); + } + } + } + }); +} diff --git a/src/painting.rs b/src/painting.rs new file mode 100644 index 0000000..3080d7a --- /dev/null +++ b/src/painting.rs @@ -0,0 +1,698 @@ +use std::{ + fmt::{self, Display}, + iter, mem, + str::FromStr, + sync::Arc, + time::Instant, +}; + +use base64::{Engine, prelude::BASE64_STANDARD}; +use egui::{ + Color32, ColorImage, CornerRadius, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense, + Shape, Stroke, TextureHandle, Theme, Ui, Vec2, + emath::{self, TSTransform}, + epaint::{Brush, RectShape, TessellationOptions, Tessellator, Vertex}, + load::SizedTexture, +}; +use eyre::{Context, bail}; +use eyre::{OptionExt, eyre}; +use half::f16; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::{ + custom_code_block::try_from_custom_code_block, + rasterizer::{self, rasterize, rasterize_onto}, +}; +use crate::{custom_code_block::write_custom_code_block, util::random_id}; + +const HANDWRITING_MIN_HEIGHT: f32 = 100.0; +const HANDWRITING_BOTTOM_PADDING: f32 = 80.0; +const HANDWRITING_MARGIN: f32 = 0.05; +const HANDWRITING_LINE_SPACING: f32 = 36.0; + +pub const CODE_BLOCK_KEY: &str = "handwriting"; + +type StrokeBlendMode = rasterizer::blend::Normal; + +const TESSELATION_OPTIONS: TessellationOptions = TessellationOptions { + feathering: true, + feathering_size_in_pixels: 1.0, + coarse_tessellation_culling: true, + prerasterized_discs: true, + round_text_to_pixels: true, + round_line_segments_to_pixels: true, + round_rects_to_pixels: true, + debug_paint_text_rects: false, + debug_paint_clip_rects: false, + debug_ignore_clip_rects: false, + bezier_tolerance: 0.1, + epsilon: 1.0e-5, + parallel_tessellation: true, + validate_meshes: false, +}; + +pub struct HandwritingStyle { + pub stroke: Stroke, + pub bg_line_stroke: Stroke, + pub bg_color: Color32, + pub animate: bool, +} + +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct Handwriting { + #[serde(skip, default = "random_id")] + id: Id, + + strokes: Vec>, + + /// The stroke that is currently being drawed. + #[serde(skip)] + current_stroke: Vec, + + /// The lines that have not been blitted to `texture` yet. + #[serde(skip)] + unblitted_lines: Vec<[Pos2; 2]>, + + height: f32, + desired_height: f32, + + /// Tesselated mesh of all strokes + #[serde(skip)] + mesh: Arc, + + #[serde(skip)] + texture: Option, + + #[serde(skip)] + image: ColorImage, + + #[serde(skip)] + refresh_texture: bool, + + /// Context of the last mesh render. + #[serde(skip)] + last_mesh_ctx: Option, +} + +/// Context of a mesh render. +#[derive(Clone, Copy, PartialEq)] +struct MeshContext { + /// Need to update the mesh when the stroke color changes. + pub ui_theme: Theme, + + pub pixels_per_point: f32, + + /// Canvas size in points + pub size: Vec2, + + pub stroke: Stroke, +} + +/// Get [Painting::texture], initializing it if necessary. +macro_rules! texture { + ($self_:expr, $ui:expr, $mesh_context:expr) => {{ + let ui: &Ui = $ui; + let mesh_context: &MeshContext = $mesh_context; + let image_size = mesh_context.pixel_size(); + + let new_image = || { + let image = ColorImage::new(image_size, Color32::TRANSPARENT); + ui.ctx() + .load_texture("handwriting", image, Default::default()) + }; + + let texture = $self_.texture.get_or_insert_with(new_image); + + if texture.size() != image_size { + $self_.refresh_texture = true; + // TODO: don't redraw the entire mesh, just blit the old texture onto the new one + *texture = new_image() + }; + + texture + }}; +} + +impl MeshContext { + /// Calculate canvas size in pixels + pub fn pixel_size(&self) -> [usize; 2] { + let Vec2 { x, y } = self.size * self.pixels_per_point; + [x, y].map(|f| f.ceil() as usize) + } +} + +impl Default for Handwriting { + fn default() -> Self { + Self { + id: random_id(), + strokes: Default::default(), + current_stroke: Default::default(), + height: HANDWRITING_MIN_HEIGHT, + desired_height: HANDWRITING_MIN_HEIGHT, + mesh: Default::default(), + texture: None, + image: ColorImage::new([0, 0], Color32::WHITE), + refresh_texture: true, + last_mesh_ctx: None, + unblitted_lines: Default::default(), + } + } +} + +impl Handwriting { + pub fn ui_control( + &mut self, + style: Option<&mut HandwritingStyle>, + ui: &mut egui::Ui, + ) -> egui::Response { + ui.horizontal(|ui| { + if let Some(style) = style { + ui.label("Stroke:"); + ui.add(&mut style.stroke); + ui.separator(); + } + + if ui.button("Clear Painting").clicked() { + self.strokes.clear(); + self.refresh_texture = true; + } + + ui.add_enabled_ui(!self.strokes.is_empty(), |ui| { + if ui.button("Undo").clicked() { + self.strokes.pop(); + self.refresh_texture = true; + } + }); + + let vertex_count: usize = self.mesh.indices.len() / 3; + ui.label(format!("vertices: {vertex_count}")); + }) + .response + } + + fn commit_current_line(&mut self) { + debug_assert!(!self.current_stroke.is_empty()); + self.strokes.push(mem::take(&mut self.current_stroke)); + } + + pub fn ui_content(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> egui::Response { + if style.animate { + self.height = ui.ctx().animate_value_with_time( + self.id.with("height animation"), + self.desired_height, + 0.4, + ); + } else { + self.height = self.desired_height; + } + + let size = Vec2::new(ui.available_width(), self.height); + let (response, painter) = ui.allocate_painter(size, Sense::drag()); + + let mut response = response + //.on_hover_cursor(CursorIcon::Crosshair) + //.on_hover_and_drag_cursor(CursorIcon::None) + ; + + let size = response.rect.size(); + + let to_screen = emath::RectTransform::from_to( + //Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), + Rect::from_min_size(Pos2::ZERO, size), + response.rect, + ); + let from_screen = to_screen.inverse(); + + let is_drawing = response.interact_pointer_pos().is_some(); + let was_drawing = !self.current_stroke.is_empty(); + + if !is_drawing { + // commit current line + if was_drawing { + self.commit_current_line(); + response.mark_changed(); + } + + // recalculate how tall the widget should be + let lines_max_y = self + .strokes + .iter() + .flatten() + .map(|p| p.y + HANDWRITING_BOTTOM_PADDING) + .fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y)); + + if self.desired_height != lines_max_y { + self.desired_height = lines_max_y; + response.mark_changed(); + } + } else { + let events = ui.ctx().input(|input| { + // If we are getting both MouseMoved and PointerMoved events, ignore the first. + let mut events = input.raw.events.iter().peekable(); + iter::from_fn(move || { + let next = events.next()?; + let Some(peek) = events.peek() else { + return Some(next); + }; + + match next { + Event::PointerMoved(..) if matches!(peek, Event::MouseMoved(..)) => { + let _ = events.next(); // drop the MouseMoved event + Some(next) + } + Event::MouseMoved(..) if matches!(peek, Event::PointerMoved(..)) => { + // return the peeked PointerMoved instead + Some(events.next().expect("next is some")) + } + _ => Some(next), + } + }) + .cloned() + .filter(|event| { + // FIXME: pinenote: PointerMoved are duplicated after the MouseMoved events + cfg!(not(feature = "pinenote")) || !matches!(event, Event::PointerMoved(..)) + }) + .collect::>() + }); + + for event in events { + let last_canvas_pos = self.current_stroke.last(); + + match event { + Event::PointerMoved(new_position) => { + let new_canvas_pos = from_screen * new_position; + if let Some(&last_canvas_pos) = last_canvas_pos { + if last_canvas_pos != new_canvas_pos { + self.push_to_stroke(new_canvas_pos); + response.mark_changed(); + } + } + } + + Event::MouseMoved(mut delta) => { + if delta.length() == 0.0 { + continue; + } + + // FIXME: pinenote: MouseMovement delta does *not* take into account screen + // scaling and rotation, so unless you've scaling=1 and no rotation, the + // MouseMoved values will be all wrong. + if cfg!(feature = "pinenote") { + delta /= 1.8; + delta = -delta.rot90(); + } + + if let Some(&last_canvas_pos) = last_canvas_pos { + self.push_to_stroke(last_canvas_pos + delta); + response.mark_changed(); + } else { + println!("Got `MouseMoved`, but have no previous pos"); + } + } + + Event::PointerButton { + pos, + button, + pressed, + modifiers: _, + } => match (button, pressed) { + (PointerButton::Primary, true) => { + if last_canvas_pos.is_none() { + self.current_stroke.push(from_screen * pos); + } + } + (PointerButton::Primary, false) => { + if last_canvas_pos.is_some() { + self.push_to_stroke(from_screen * pos); + self.commit_current_line(); + response.mark_changed(); + } + + // Stop reading events. + // TODO: In theory, we can get multiple press->draw->release series + // in the same frame. Should handle this. + break; + } + (_, _) => continue, + }, + + // Stop drawing after pointer disappears or the window is unfocused + // TODO: In theory, we can get multiple press->draw->release series + // in the same frame. Should handle this. + Event::PointerGone | Event::WindowFocused(false) => { + if !self.current_stroke.is_empty() { + self.commit_current_line(); + break; + } + } + + Event::WindowFocused(true) + | Event::Copy + | Event::Cut + | Event::Paste(..) + | Event::Text(..) + | Event::Key { .. } + | Event::Zoom(..) + | Event::Ime(..) + | Event::Touch { .. } + | Event::MouseWheel { .. } + | Event::Screenshot { .. } => continue, + } + } + } + + (1..) + .map(|n| n as f32 * HANDWRITING_LINE_SPACING) + .take_while(|&y| y < size.y) + .map(|y| { + let l = to_screen * Pos2::new(HANDWRITING_MARGIN * size.x, y); + let r = to_screen * Pos2::new((1.0 - HANDWRITING_MARGIN) * size.x, y); + Shape::hline(l.x..=r.x, l.y, style.bg_line_stroke) + }) + .for_each(|shape| { + painter.add(shape); + }); + + let mesh_rect = response + .rect + .with_max_y(response.rect.min.y + self.desired_height); + let new_context = MeshContext { + ui_theme: ui.ctx().theme(), + pixels_per_point: ui.pixels_per_point(), + size: mesh_rect.size(), + stroke: style.stroke, + }; + + if Some(&new_context) != self.last_mesh_ctx.as_ref() { + self.refresh_texture = true; + } + + if self.refresh_texture { + // rasterize the entire texture from scratch + self.refresh_texture(style, new_context, ui); + self.unblitted_lines.clear(); + } else if !self.unblitted_lines.is_empty() { + // only rasterize the new lines onto the existing texture + for [from, to] in std::mem::take(&mut self.unblitted_lines) { + self.draw_line_to_texture(from, to, &new_context, ui); + } + self.unblitted_lines.clear(); + } + + //painter.add(self.mesh.clone()); + + if let Some(texture) = &self.texture { + let texture = SizedTexture::new(texture.id(), texture.size_vec2()); + let shape = RectShape { + rect: mesh_rect, + corner_radius: CornerRadius::ZERO, + fill: Color32::WHITE, + stroke: Stroke::NONE, + stroke_kind: egui::StrokeKind::Inside, + round_to_pixels: None, + blur_width: 0.0, + brush: Some(Arc::new(Brush { + fill_texture_id: texture.id, + uv: Rect { + min: Pos2::ZERO, + max: Pos2::new(1.0, 1.0), + }, + })), + }; + painter.add(shape); + } + + response + } + + fn refresh_texture( + &mut self, + style: &HandwritingStyle, + mesh_context: MeshContext, + ui: &mut Ui, + ) { + self.last_mesh_ctx = Some(mesh_context); + + self.refresh_texture = false; + + let start_time = Instant::now(); + + let mut tesselator = Tessellator::new( + mesh_context.pixels_per_point, + TESSELATION_OPTIONS, + Default::default(), // we don't tesselate fonts + vec![], + ); + + let mesh = Arc::make_mut(&mut self.mesh); + mesh.clear(); + + self.strokes + .iter() + .chain([&self.current_stroke]) + .filter(|stroke| stroke.len() >= 2) + .map(|stroke| { + //let points: Vec = stroke.iter().map(|&p| to_screen * p).collect(); + egui::Shape::line(stroke.clone(), style.stroke) + }) + .for_each(|shape| { + tesselator.tessellate_shape(shape, mesh); + }); + + let texture = texture!(self, ui, &mesh_context); + let triangles = mesh_triangles(&self.mesh); + + let [px_x, px_y] = mesh_context.pixel_size(); + let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); + self.image = rasterize::(px_x, px_y, point_to_pixel, triangles); + texture.set(self.image.clone(), Default::default()); + + let elapsed = start_time.elapsed(); + println!("refreshed mesh in {:.3}s", elapsed.as_secs_f32()); + } + + pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) { + ui.vertical_centered_justified(|ui| { + self.ui_control(None, ui); + + //ui.label("Paint with your mouse/touch!"); + Frame::canvas(ui.style()) + .corner_radius(20.0) + .stroke(Stroke::new(5.0, Color32::from_black_alpha(40))) + .fill(style.bg_color) + .show(ui, |ui| { + self.ui_content(style, ui); + }); + }); + } + + fn push_to_stroke(&mut self, new_canvas_pos: Pos2) { + if let Some(&last_canvas_pos) = self.current_stroke.last() { + if last_canvas_pos == new_canvas_pos { + return; + } + + self.unblitted_lines.push([last_canvas_pos, new_canvas_pos]); + } + + self.current_stroke.push(new_canvas_pos); + } + + /// Draw a single line onto the existing texture. + fn draw_line_to_texture( + &mut self, + from: Pos2, + to: Pos2, + mesh_context: &MeshContext, + ui: &mut Ui, + ) { + let mut tesselator = Tessellator::new( + mesh_context.pixels_per_point, + TESSELATION_OPTIONS, + Default::default(), // we don't tesselate fonts + vec![], + ); + + let mut mesh = Mesh::default(); + let line = egui::Shape::line_segment([from, to], mesh_context.stroke); + tesselator.tessellate_shape(line, &mut mesh); + + self.draw_mesh_to_texture(&mesh, mesh_context, ui); + } + + /// Draw a single mesh onto the existing texture. + fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext, ui: &mut Ui) { + let triangles = mesh_triangles(mesh); + let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); + rasterize_onto::(&mut self.image, point_to_pixel, triangles); + texture!(self, ui, mesh_context).set(self.image.clone(), Default::default()); + } + + pub fn strokes(&self) -> &[Vec] { + &self.strokes + } + + #[cfg(test)] + pub fn example() -> Self { + Handwriting { + strokes: vec![ + vec![ + Pos2::new(-1.0, 1.0), + Pos2::new(3.0, 1.0), + Pos2::new(3.0, 3.0), + Pos2::new(1.5, 2.0), + Pos2::new(0.0, 0.0), + ], + vec![ + Pos2::new(3.0, 3.0), + Pos2::new(-1.0, 1.0), + Pos2::new(0.0, 0.0), + Pos2::new(3.0, 1.0), + ], + ], + ..Default::default() + } + } +} + +impl Display for Handwriting { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut raw = vec![]; + + for stroke in &self.strokes { + raw.push((stroke.len() as u16).to_le_bytes()); + for position in stroke { + let x = half::f16::from_f32(position.x); + let y = half::f16::from_f32(position.y); + raw.push(x.to_bits().to_le_bytes()); + raw.push(y.to_bits().to_le_bytes()); + } + } + + let raw = raw.as_slice().as_bytes(); + + write_custom_code_block(f, CODE_BLOCK_KEY, BASE64_STANDARD.encode(raw)) + } +} + +impl FromStr for Handwriting { + type Err = eyre::Report; + + fn from_str(s: &str) -> Result { + let s = try_from_custom_code_block(CODE_BLOCK_KEY, s) + .ok_or_eyre("Not a valid ```handwriting-block")?; + + let bytes = BASE64_STANDARD + .decode(s) + .wrap_err("Failed to decode painting data from base64")?; + + #[allow(non_camel_case_types)] + type u16_le = [u8; 2]; + + #[allow(non_camel_case_types)] + type f16_le = [u8; 2]; + + #[derive(FromBytes, KnownLayout, Immutable)] + #[repr(C, packed)] + struct Stroke { + pub len: u16_le, + pub positions: [f16_le], + } + + let mut bytes = &bytes[..]; + let mut strokes = vec![]; + + while !bytes.is_empty() { + let header_len = size_of::(); + if bytes.len() < header_len { + bail!("Invalid remaining length: {}", bytes.len()); + } + + let stroke = Stroke::ref_from_bytes(&bytes[..header_len]).expect("length is correct"); + let len = usize::from(u16::from_le_bytes(stroke.len)); + let len = len * size_of::() * 2; + + if bytes.len() < len { + bail!("Invalid remaining length: {}", bytes.len()); + } + + let (stroke, rest) = bytes.split_at(header_len + len); + bytes = rest; + let stroke = Stroke::ref_from_bytes(stroke) + .map_err(|e| eyre!("Failed to decode stroke bytes: {e}"))?; + + let mut positions = stroke + .positions + .iter() + .map(|&position| f16::from_bits(u16::from_le_bytes(position))); + + let mut stroke = vec![]; + while let Some(x) = positions.next() { + let Some(y) = positions.next() else { + unreachable!("len is a multiple of two"); + }; + stroke.push(Pos2::new(x.into(), y.into())); + } + strokes.push(stroke); + } + + Ok(Handwriting { + strokes, + ..Default::default() + }) + } +} + +impl HandwritingStyle { + pub fn from_theme(theme: Theme) -> Self { + let stroke_color; + let bg_color; + let line_color; + + match theme { + Theme::Dark => { + stroke_color = Color32::WHITE; + bg_color = Color32::from_gray(30); + line_color = Color32::from_rgb(100, 100, 100); + } + Theme::Light => { + stroke_color = Color32::BLACK; + bg_color = Color32::WHITE; + line_color = Color32::from_rgb(130, 130, 130); // TODO + } + } + + HandwritingStyle { + stroke: Stroke::new(1.6, stroke_color), + bg_color, + bg_line_stroke: Stroke::new(0.5, line_color), + animate: true, + } + } +} + +fn mesh_triangles(mesh: &Mesh) -> impl Iterator { + mesh.triangles() + .map(|indices| indices.map(|i| &mesh.vertices[i as usize])) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::Handwriting; + + #[test] + fn serialize_handwriting() { + let handwriting = Handwriting::example(); + insta::assert_debug_snapshot!("handwriting example", handwriting.strokes); + + let serialized = handwriting.to_string(); + insta::assert_snapshot!("serialized handwriting", serialized); + + let deserialized = + Handwriting::from_str(&serialized).expect("Handwriting must de/serialize correctly"); + insta::assert_debug_snapshot!("deserialized handwriting", deserialized.strokes); + } +} diff --git a/src/preferences.rs b/src/preferences.rs new file mode 100644 index 0000000..ef62090 --- /dev/null +++ b/src/preferences.rs @@ -0,0 +1,72 @@ +use egui::{Color32, Context, RichText, Theme, Ui, Visuals}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct Preferences { + /// Enable animations + pub animations: bool, + + /// Enable high-contrast theme + pub high_contrast: bool, + + #[serde(skip)] + has_applied_theme: bool, +} + +impl Default for Preferences { + fn default() -> Self { + Self { + animations: true, + high_contrast: false, + has_applied_theme: false, + } + } +} + +impl Preferences { + /// Apply preferences, if they haven't already been applied. + pub fn apply(&mut self, ctx: &Context) { + if !self.has_applied_theme { + self.has_applied_theme = true; + + if self.high_contrast { + // widgets.active: color of headers in textedit + // widgets.inactive: color of button labels + // widgets.hovered: color of hovered button labels + // widgets.noninteractive: color of labels and normal textedit text + let mut dark_visuals = Visuals::dark(); + let mut light_visuals = Visuals::light(); + + dark_visuals.widgets.noninteractive.fg_stroke.color = Color32::WHITE; + dark_visuals.widgets.inactive.fg_stroke.color = Color32::WHITE; + dark_visuals.widgets.hovered.fg_stroke.color = Color32::WHITE; + + light_visuals.widgets.noninteractive.fg_stroke.color = Color32::BLACK; + light_visuals.widgets.inactive.fg_stroke.color = Color32::BLACK; + light_visuals.widgets.hovered.fg_stroke.color = Color32::BLACK; + + ctx.set_visuals_of(Theme::Dark, dark_visuals); + ctx.set_visuals_of(Theme::Light, light_visuals); + } else { + ctx.set_visuals_of(Theme::Dark, Visuals::dark()); + ctx.set_visuals_of(Theme::Light, Visuals::light()); + } + } + } + + /// Show preference switches + pub fn show(&mut self, ui: &mut Ui) { + ui.label(RichText::new("Prefs").weak()); + + ui.toggle_value(&mut self.animations, "Animations"); + + let high_contrast_toggle = ui.toggle_value(&mut self.high_contrast, "High Contrast"); + if high_contrast_toggle.clicked() { + self.has_applied_theme = false; + self.apply(ui.ctx()); + } + + egui::widgets::global_theme_preference_buttons(ui); + } +} diff --git a/src/rasterizer.rs b/src/rasterizer.rs new file mode 100644 index 0000000..4fe2bd0 --- /dev/null +++ b/src/rasterizer.rs @@ -0,0 +1,277 @@ +use core::f32; +use egui::{Color32, ColorImage, Pos2, Rect, Vec2, emath::TSTransform, epaint::Vertex}; + +pub trait BlendFn { + fn blend(a: Color32, b: Color32) -> Color32; +} + +pub mod blend { + pub struct Normal; + pub struct Add; + pub struct Multiply; +} + +/// Rasterize some triangles onto a new image, +/// +/// Triangle positions must be in image-local point-coords. +/// `width` and `height` are in pixel coords. +pub fn rasterize<'a, Blend: BlendFn>( + width: usize, + height: usize, + point_to_pixel: TSTransform, + triangles: impl Iterator, +) -> ColorImage { + let mut image = ColorImage::new([width, height], Color32::TRANSPARENT); + rasterize_onto::(&mut image, point_to_pixel, triangles); + image +} + +/// Rasterize some triangles onto an image, +/// +/// Triangle positions must be in image-local point-coords. +pub fn rasterize_onto<'a, Blend: BlendFn>( + image: &mut ColorImage, + point_to_pixel: TSTransform, + triangles: impl Iterator, +) { + let width = image.width(); + let height = image.height(); + + let mut set_pixel = |x: usize, y: usize, color| { + let pixel = &mut image.pixels[y * width + x]; + *pixel = Blend::blend(*pixel, color); + }; + + let image_box = PxBoundingBox { + x_from: 0, + y_from: 0, + x_to: width, + y_to: height, + }; + + let pixel_to_point = point_to_pixel.inverse(); + + for triangle in triangles { + let [a, b, c] = triangle; + if triangle_area(a.pos, b.pos, c.pos) == 0.0 { + continue; + } + + // Check all pixels within the triangle's bounding box. + let bounding_box = + triangle_bounding_box(&triangle, point_to_pixel).intersection(&image_box); + + // TODO: consider subdividing the triangle if it's very large. + let pixels = pixels_in_box(bounding_box); + + for [x, y] in pixels { + // Calculate point-coordinate of the pixel + let pt_pos = pixel_to_point * Pos2::new(x as f32, y as f32); + + let point_in_triangle = point_in_triangle(pt_pos, triangle); + + // If the pixel is within the triangle, fill it in. + if point_in_triangle.inside { + let c0 = triangle[0] + .color + .linear_multiply(point_in_triangle.weights[0]); + let c1 = triangle[1] + .color + .linear_multiply(point_in_triangle.weights[1]); + let c2 = triangle[2] + .color + .linear_multiply(point_in_triangle.weights[2]); + + let color = c0 + c1 + c2; + + set_pixel(x, y, color); + } + } + } +} + +/// A bounding box, measured in pixels. +#[derive(Debug, PartialEq, Eq)] +struct PxBoundingBox { + pub x_from: usize, + pub y_from: usize, + pub x_to: usize, + pub y_to: usize, +} + +impl PxBoundingBox { + pub fn intersection(&self, other: &PxBoundingBox) -> PxBoundingBox { + PxBoundingBox { + x_from: self.x_from.max(other.x_from), + y_from: self.y_from.max(other.y_from), + x_to: self.x_to.min(other.x_to), + y_to: self.y_to.min(other.y_to), + } + } +} + +fn triangle_bounding_box(triangle: &[&Vertex; 3], point_to_pixel: TSTransform) -> PxBoundingBox { + // calculate bounding box in point coords + let mut rect = Rect::NOTHING; + for vertex in triangle { + rect.min = rect.min.min(vertex.pos); + rect.max = rect.max.max(vertex.pos); + } + + // convert bounding box to pixel coords + let rect = point_to_pixel.mul_rect(rect); + PxBoundingBox { + x_from: rect.min.x.floor() as usize, + y_from: rect.min.y.floor() as usize, + x_to: rect.max.x.ceil() as usize, + y_to: rect.max.y.ceil() as usize, + } +} + +/// Calculate the perpendicular vector (90 degrees from the given vector) +fn perpendicular(v: Vec2) -> Vec2 { + Vec2::new(v.y, -v.x) +} + +#[derive(Clone, Debug)] +struct PointInTriangle { + /// Is the point inside the triangle? + inside: bool, + + /// Normalized weights describing the vicinity between the point and the three verticies of + /// thre triangle. + weights: [f32; 3], +} + +/// Calculate whether a point is within a triangle, and the relative vicinities between the point +/// and each triangle vertex. The triangle must have a non-zero area. +fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle { + let [a, b, c] = triangle; + + let sides = [[b, c], [c, a], [a, b]]; + + // For each side of the triangle, imagine a new triangle consisting of the side and `point`. + // Calculate the areas of those triangles. + let areas = sides.map(|[start, end]| signed_triangle_area(start.pos, end.pos, point)); + + // Use the areas to determine the side of the line at which the point exists. + // If the area is positive, the point is on the right side of the triangle line. + let [side_ab, side_bc, side_ca] = areas.map(|area| area >= 0.0); + + // Total area of the traingle. + let triangle_area: f32 = areas.iter().sum(); + + // egui does not wind the triangles in a consistent order, otherwise we might check if the + // point is on a *specific* side of each line. As it is, we just check if the point is on the + // same side of each line. + let inside = side_ab == side_bc && side_bc == side_ca; + + // Normalize the weights. + let weights = areas.map(|area| area / triangle_area); + + PointInTriangle { inside, weights } +} + +/// Calculate the area of a triangle. +fn triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 { + signed_triangle_area(a, b, c).abs() +} + +/// Calculate the area of a triangle. +/// +/// The area will be positive if the triangle is wound clockwise, and negative otherwise. +fn signed_triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 { + // Vector of an arbitrary "base" side of the triangle. + let base = c - a; + let base_perp = perpendicular(base); + let diagonal = c - b; + base_perp.dot(diagonal) / 2.0 +} + +/// Iterate over every pixel coordinate in a box. +#[inline(always)] +fn pixels_in_box( + PxBoundingBox { + x_from, + y_from, + x_to, + y_to, + }: PxBoundingBox, +) -> impl ExactSizeIterator { + struct IterWithLen(I, usize); + impl ExactSizeIterator for IterWithLen {} + impl Iterator for IterWithLen { + type Item = I::Item; + + fn size_hint(&self) -> (usize, Option) { + (self.1, Some(self.1)) + } + + fn next(&mut self) -> Option { + self.0.next() + } + } + + let len = (x_from..x_to).len() * (y_from..y_to).len(); + let iter = (x_from..x_to).flat_map(move |x| (y_from..y_to).map(move |y| [x, y])); + + debug_assert_eq!(len, iter.clone().count()); + + IterWithLen(iter, len) +} + +#[cfg(test)] +mod test { + use egui::{Color32, Pos2, Vec2, emath::TSTransform, epaint::Vertex}; + + use super::triangle_bounding_box; + + #[test] + fn px_bounding_box() { + let triangle = [ + Vertex { + pos: Pos2::new(56.3, 18.9), + uv: Default::default(), + color: Color32::WHITE, + }, + Vertex { + pos: Pos2::new(56.4, 19.6), + uv: Default::default(), + color: Color32::WHITE, + }, + Vertex { + pos: Pos2::new(55.8, 20.5), + uv: Default::default(), + color: Color32::WHITE, + }, + ]; + + let pixels_per_point = 2.0; + let point_to_pixel = TSTransform { + scaling: pixels_per_point, + translation: Vec2::new(-55.8, -18.9) * pixels_per_point, + }; + + let bounding_box = triangle_bounding_box(&triangle.each_ref(), point_to_pixel); + + insta::assert_debug_snapshot!((triangle, point_to_pixel, bounding_box)); + } +} + +impl BlendFn for blend::Normal { + fn blend(a: Color32, b: Color32) -> Color32 { + a.blend(b) + } +} + +impl BlendFn for blend::Add { + fn blend(a: Color32, b: Color32) -> Color32 { + a + b + } +} + +impl BlendFn for blend::Multiply { + fn blend(a: Color32, b: Color32) -> Color32 { + a * b + } +} diff --git a/src/snapshots/inkr__custom_code_block__test__iter_markdown.snap b/src/snapshots/inkr__custom_code_block__test__iter_markdown.snap new file mode 100644 index 0000000..6719f61 --- /dev/null +++ b/src/snapshots/inkr__custom_code_block__test__iter_markdown.snap @@ -0,0 +1,18 @@ +--- +source: src/custom_code_block.rs +expression: markdown +--- +# Hello world +## Subheader +- 1 +```foo + whatever + some code + Hi mom! +``` + +```` # wrong number of ticks, but that's ok + ``` # indented ticks +``` + +``` # no closing ticks diff --git a/src/snapshots/inkr__painting__test__handwriting example.snap b/src/snapshots/inkr__painting__test__handwriting example.snap new file mode 100644 index 0000000..37175d4 --- /dev/null +++ b/src/snapshots/inkr__painting__test__handwriting example.snap @@ -0,0 +1,19 @@ +--- +source: src/painting.rs +expression: handwriting.strokes +--- +[ + [ + [-1.0 1.0], + [3.0 1.0], + [3.0 3.0], + [1.5 2.0], + [0.0 0.0], + ], + [ + [3.0 3.0], + [-1.0 1.0], + [0.0 0.0], + [3.0 1.0], + ], +] diff --git a/src/snapshots/inkr__rasterizer__test__px_bounding_box.snap b/src/snapshots/inkr__rasterizer__test__px_bounding_box.snap new file mode 100644 index 0000000..30240a0 --- /dev/null +++ b/src/snapshots/inkr__rasterizer__test__px_bounding_box.snap @@ -0,0 +1,33 @@ +--- +source: src/rasterizer.rs +expression: "(triangle, point_to_pixel, bounding_box)" +--- +( + [ + Vertex { + pos: [56.3 18.9], + uv: [0.0 0.0], + color: #FF_FF_FF_FF, + }, + Vertex { + pos: [56.4 19.6], + uv: [0.0 0.0], + color: #FF_FF_FF_FF, + }, + Vertex { + pos: [55.8 20.5], + uv: [0.0 0.0], + color: #FF_FF_FF_FF, + }, + ], + TSTransform { + scaling: 2.0, + translation: [-111.6 -37.8], + }, + PxBoundingBox { + x_from: 0, + y_from: 0, + x_to: 2, + y_to: 4, + }, +) diff --git a/src/text_editor.rs b/src/text_editor.rs new file mode 100644 index 0000000..09ebd47 --- /dev/null +++ b/src/text_editor.rs @@ -0,0 +1,125 @@ +use std::{ + convert::Infallible, + fmt::{self, Display}, + iter::repeat_n, +}; + +use egui::{ + Color32, InputState, Key, Modifiers, TextBuffer, TextEdit, Ui, Vec2, text::CCursorRange, +}; + +use crate::easy_mark::MemoizedHighlighter; + +#[derive(Default, serde::Deserialize, serde::Serialize)] +pub struct MdTextEdit { + pub text: String, + + #[serde(skip)] + highlighter: MemoizedHighlighter, + + #[serde(skip)] + focused: bool, + + #[serde(skip)] + cursor: Option, +} + +impl MdTextEdit { + pub fn new() -> Self { + MdTextEdit::default() + } + + pub fn from_text(text: String) -> Self { + MdTextEdit { + text, + ..Default::default() + } + } + + pub fn ui(&mut self, ui: &mut Ui) { + let Self { + text, + highlighter, + focused, + cursor, + } = self; + + let w = ui.available_width(); + + let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, _wrap_width: f32| { + let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str(), *cursor); + layout_job.wrap.max_width = w - 10.0; + ui.fonts(|f| f.layout_job(layout_job)) + }; + + if *focused { + ui.input_mut(|input| { + handle_tab_input(text, input, *cursor); + }); + } + + let text_edit = TextEdit::multiline(text) + .layouter(&mut layouter) + .background_color(Color32::TRANSPARENT) + .desired_rows(1) + .min_size(Vec2::new(w, 0.0)) + .lock_focus(true) + .show(ui); + + *focused = text_edit.response.has_focus(); + + if *cursor != text_edit.cursor_range { + *cursor = text_edit.cursor_range; + //ui.ctx().request_repaint(); + } + } +} + +fn handle_tab_input( + text: &mut String, + input: &mut InputState, + cursor: Option, +) -> Option { + let break_if_not = || (); + let cursor = cursor.and_then(|c| c.single())?; + + let do_unindent = input.consume_key(Modifiers::SHIFT, Key::Tab); + let do_indent = input.consume_key(Modifiers::NONE, Key::Tab); + + (do_unindent || do_indent).then(break_if_not)?; + + let row_n = text + .chars() + .take(cursor.index) + .filter(|&c| c == '\n') + .count(); + + let row = text.lines().nth(row_n)?; + let (indent, content) = row.split_once("- ")?; + indent.trim().is_empty().then(break_if_not)?; + + let indents = indent.chars().count() / 2; + let indents = if do_indent { + indents + 1 + } else if indents == 0 { + return None; + } else { + indents.saturating_sub(1) + }; + *text = text + .lines() + .take(row_n) + .flat_map(|line| [line, "\n"]) + .chain(repeat_n(" ", indents)) + .chain([format!("- {content}\n").as_str()]) + .chain(text.lines().skip(row_n + 1).flat_map(|line| [line, "\n"])) + .collect(); + + None +} + +impl Display for MdTextEdit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.text) + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..01fefe3 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,30 @@ +use std::sync::mpsc; + +use egui::Id; +use rand::{Rng, rng}; + +pub fn random_id() -> Id { + Id::new(rng().random::()) +} + +/// An [mpsc::Sender] where the receiver is the GUI. +#[derive(Clone)] +pub struct GuiSender { + tx: mpsc::Sender, + ctx: egui::Context, +} + +impl GuiSender { + pub fn new(tx: mpsc::Sender, ctx: &egui::Context) -> Self { + Self { + tx, + ctx: ctx.clone(), + } + } + + pub fn send(&self, t: T) -> Result<(), mpsc::SendError> { + self.tx.send(t)?; + self.ctx.request_repaint(); + Ok(()) + } +}