From 64642b404d2d84eea8428bc7aa3f51b7772fc02d Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Thu, 12 Jun 2025 20:23:52 +0200 Subject: [PATCH] Add App --- .gitignore | 2 + Cargo.lock | 3629 +++++++++++++++++ Cargo.toml | 50 + README.md | 1 + Trunk.toml | 2 + assets/icon-1024.png | Bin 0 -> 86916 bytes assets/icon-256.png | Bin 0 -> 19366 bytes assets/icon.svg | 62 + assets/manifest.json | 22 + assets/sw.js | 25 + index.html | 135 + src/app.rs | 357 ++ src/constants.rs | 1 + src/custom_code_block.rs | 135 + src/easy_mark/easy_mark_highlighter.rs | 243 ++ src/easy_mark/easy_mark_parser.rs | 346 ++ src/easy_mark/mod.rs | 7 + src/file_editor.rs | 356 ++ src/lib.rs | 14 + src/main.rs | 71 + src/painting.rs | 698 ++++ src/preferences.rs | 72 + src/rasterizer.rs | 277 ++ ...ustom_code_block__test__iter_markdown.snap | 18 + ...__painting__test__handwriting example.snap | 19 + ...kr__rasterizer__test__px_bounding_box.snap | 33 + src/text_editor.rs | 125 + src/util.rs | 30 + 28 files changed, 6730 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 Trunk.toml create mode 100644 assets/icon-1024.png create mode 100644 assets/icon-256.png create mode 100644 assets/icon.svg create mode 100644 assets/manifest.json create mode 100644 assets/sw.js create mode 100644 index.html create mode 100644 src/app.rs create mode 100644 src/constants.rs create mode 100644 src/custom_code_block.rs create mode 100644 src/easy_mark/easy_mark_highlighter.rs create mode 100644 src/easy_mark/easy_mark_parser.rs create mode 100644 src/easy_mark/mod.rs create mode 100644 src/file_editor.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/painting.rs create mode 100644 src/preferences.rs create mode 100644 src/rasterizer.rs create mode 100644 src/snapshots/inkr__custom_code_block__test__iter_markdown.snap create mode 100644 src/snapshots/inkr__painting__test__handwriting example.snap create mode 100644 src/snapshots/inkr__rasterizer__test__px_bounding_box.snap create mode 100644 src/text_editor.rs create mode 100644 src/util.rs 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 0000000000000000000000000000000000000000..2e62c983d00ee4667e08dbb105747f89eaecf3d7 GIT binary patch literal 86916 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 DzS^iD literal 0 HcmV?d00001 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/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..80431f9 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,357 @@ +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, +}; + +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(default)] +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)); + }); + + 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| { + 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(()) + } +}