From 48da21e062ea5ed4c1c4be7e808f952d3f7dc291 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 13 Nov 2025 18:12:21 -0500 Subject: [PATCH] Toolbar --- lightningbeam-ui/Cargo.lock | 1486 ++++++++++++++++- lightningbeam-ui/Cargo.toml | 8 + .../lightningbeam-core/src/lib.rs | 2 + .../lightningbeam-core/src/pane.rs | 137 ++ .../lightningbeam-core/src/tool.rs | 79 + .../lightningbeam-editor/Cargo.toml | 8 + lightningbeam-ui/lightningbeam-editor/PLAN.md | 461 +++++ .../lightningbeam-editor/src/main.rs | 1317 ++++++++++++++- .../lightningbeam-editor/src/menu.rs | 563 +++++++ .../src/panes/infopanel.rs | 45 + .../lightningbeam-editor/src/panes/mod.rs | 145 ++ .../src/panes/node_editor.rs | 45 + .../src/panes/outliner.rs | 47 + .../src/panes/piano_roll.rs | 45 + .../src/panes/preset_browser.rs | 45 + .../lightningbeam-editor/src/panes/stage.rs | 47 + .../src/panes/timeline.rs | 57 + .../lightningbeam-editor/src/panes/toolbar.rs | 235 +++ src-tauri/Cargo.lock | 1281 +++++++++++++- src-tauri/Cargo.toml | 8 + src-tauri/src/frame_ws_async.rs | 230 +++ src-tauri/src/lib.rs | 178 ++ src-tauri/src/render_window.rs | 161 ++ src-tauri/src/renderer.rs | 293 ++++ src-tauri/src/shaders/gradient.wgsl | 26 + 25 files changed, 6812 insertions(+), 137 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-core/src/pane.rs create mode 100644 lightningbeam-ui/lightningbeam-core/src/tool.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/PLAN.md create mode 100644 lightningbeam-ui/lightningbeam-editor/src/menu.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/node_editor.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/outliner.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/preset_browser.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs create mode 100644 src-tauri/src/frame_ws_async.rs create mode 100644 src-tauri/src/render_window.rs create mode 100644 src-tauri/src/renderer.rs create mode 100644 src-tauri/src/shaders/gradient.wgsl diff --git a/lightningbeam-ui/Cargo.lock b/lightningbeam-ui/Cargo.lock index a546db4..e26325e 100644 --- a/lightningbeam-ui/Cargo.lock +++ b/lightningbeam-ui/Cargo.lock @@ -127,6 +127,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -163,6 +172,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arboard" version = "3.6.1" @@ -180,6 +201,17 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -349,6 +381,29 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[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" @@ -412,6 +467,41 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[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.6.0" @@ -427,6 +517,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -438,6 +534,15 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "block" @@ -476,6 +581,12 @@ dependencies = [ "piper", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.19.0" @@ -514,6 +625,31 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.13.0" @@ -583,6 +719,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -635,6 +781,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.6.0" @@ -753,12 +905,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -786,6 +972,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "digest" version = "0.10.7" @@ -951,6 +1143,21 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_extras" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1" +dependencies = [ + "ahash", + "egui", + "enum-map", + "image", + "log", + "mime_guess2", + "resvg 0.37.0", +] + [[package]] name = "egui_glow" version = "0.29.1" @@ -968,6 +1175,12 @@ dependencies = [ "winit", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.29.1" @@ -983,6 +1196,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1027,6 +1261,26 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1079,12 +1333,47 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1094,6 +1383,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -1110,6 +1409,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "foldhash" version = "0.1.5" @@ -1125,6 +1430,29 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.21.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1161,12 +1489,32 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-intrusive" version = "0.5.0" @@ -1237,6 +1585,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[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 = "generic-array" version = "0.14.9" @@ -1280,6 +1686,48 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[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" @@ -1291,6 +1739,53 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[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.13.1" @@ -1381,6 +1876,17 @@ 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" @@ -1433,6 +1939,58 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[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 = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "guillotiere" version = "0.6.2" @@ -1443,6 +2001,17 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1473,6 +2042,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[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.5.2" @@ -1601,11 +2182,44 @@ checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "moxcms", "num-traits", "png 0.18.0", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "immutable-chunkmap" version = "2.1.2" @@ -1625,6 +2239,26 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1663,6 +2297,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.82" @@ -1673,6 +2313,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -1690,6 +2341,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -1712,12 +2372,28 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.9" @@ -1739,6 +2415,25 @@ dependencies = [ "redox_syscall 0.5.18", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "lightningbeam-core" version = "0.1.0" @@ -1752,10 +2447,14 @@ name = "lightningbeam-editor" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", + "image", "kurbo 0.11.3", "lightningbeam-core", + "muda", "peniko 0.5.0", "pollster", + "resvg 0.42.0", "serde", "serde_json", "vello", @@ -1808,6 +2507,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1817,6 +2525,16 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1856,6 +2574,24 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf", + "phf_shared", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1876,6 +2612,26 @@ dependencies = [ "pxfm", ] +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "png 0.17.16", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "22.1.0" @@ -1936,6 +2692,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -1955,6 +2717,62 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1980,7 +2798,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.110", @@ -2297,7 +3115,32 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ - "ttf-parser", + "ttf-parser 0.25.1", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[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]] @@ -2363,6 +3206,56 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.110", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", + "unicase", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.10" @@ -2482,13 +3375,57 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -2505,6 +3442,19 @@ name = "profiling" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.110", +] [[package]] name = "pxfm" @@ -2515,6 +3465,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.30.0" @@ -2585,12 +3550,88 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "read-fonts" version = "0.22.7" @@ -2625,6 +3666,57 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes 0.13.0", + "tiny-skia", + "usvg 0.37.0", +] + +[[package]] +name = "resvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.42.0", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2637,6 +3729,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2669,6 +3770,22 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.20" @@ -2709,6 +3826,12 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2763,6 +3886,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2795,6 +3927,36 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skrifa" version = "0.22.3" @@ -2924,6 +4086,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "svg_fmt" @@ -2931,6 +4096,26 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo 0.9.5", + "siphasher 0.3.11", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher 1.0.1", +] + [[package]] name = "syn" version = "1.0.109" @@ -2964,6 +4149,25 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "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 = "tempfile" version = "3.23.0" @@ -3026,6 +4230,20 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3037,6 +4255,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png 0.17.16", "tiny-skia-path", ] @@ -3061,6 +4280,42 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3070,6 +4325,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.23.7" @@ -3077,9 +4356,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -3088,7 +4367,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -3123,6 +4402,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + [[package]] name = "ttf-parser" version = "0.25.1" @@ -3155,18 +4440,60 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -3191,12 +4518,94 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.7", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.1", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo 0.9.5", + "log", + "roxmltree 0.19.0", + "simplecss", + "siphasher 0.3.11", + "svgtypes 0.13.0", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes 0.13.0", + "tiny-skia-path", +] + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vello" version = "0.3.0" @@ -3242,6 +4651,12 @@ dependencies = [ "vello_encoding", ] +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -3502,6 +4917,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wgpu" version = "22.1.0" @@ -4017,6 +5438,15 @@ dependencies = [ "xkbcommon-dl", ] +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.13" @@ -4038,6 +5468,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -4111,6 +5551,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yoke" version = "0.8.1" @@ -4202,7 +5648,7 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.110", @@ -4307,6 +5753,30 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.2.0" @@ -4326,7 +5796,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.110", diff --git a/lightningbeam-ui/Cargo.toml b/lightningbeam-ui/Cargo.toml index 6f680a0..e9a8aee 100644 --- a/lightningbeam-ui/Cargo.toml +++ b/lightningbeam-ui/Cargo.toml @@ -8,6 +8,7 @@ members = [ [workspace.dependencies] # UI Framework (using eframe for simplified integration) eframe = { version = "0.29", default-features = true, features = ["wgpu"] } +egui_extras = { version = "0.29", features = ["image", "svg"] } # GPU Rendering vello = "0.3" @@ -18,9 +19,16 @@ peniko = "0.5" # Windowing winit = "0.30" +# Native menus +muda = "0.15" + # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +# Image loading +image = "0.25" +resvg = "0.42" + # Utilities pollster = "0.3" diff --git a/lightningbeam-ui/lightningbeam-core/src/lib.rs b/lightningbeam-ui/lightningbeam-core/src/lib.rs index aad8051..e01c5b0 100644 --- a/lightningbeam-ui/lightningbeam-core/src/lib.rs +++ b/lightningbeam-ui/lightningbeam-core/src/lib.rs @@ -2,3 +2,5 @@ // Shared data structures and types pub mod layout; +pub mod pane; +pub mod tool; diff --git a/lightningbeam-ui/lightningbeam-core/src/pane.rs b/lightningbeam-ui/lightningbeam-core/src/pane.rs new file mode 100644 index 0000000..f0f8884 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/pane.rs @@ -0,0 +1,137 @@ +/// Pane system for the layout manager +/// +/// Each pane has: +/// - An icon button (top-left) for pane type selection +/// - Optional header with controls (e.g., Timeline playback controls) +/// - Content area (main pane body) + +use serde::{Deserialize, Serialize}; + +/// Pane type enum matching the layout system +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PaneType { + /// Main animation canvas + Stage, + /// Frame-based timeline (matches timelineV2 from JS, but just "timeline" in Rust) + #[serde(rename = "timelineV2")] + Timeline, + /// Tool selection bar + Toolbar, + /// Property/info panel + Infopanel, + /// Layer hierarchy + #[serde(rename = "outlineer")] + Outliner, + /// MIDI piano roll editor + PianoRoll, + /// Node-based editor + NodeEditor, + /// Preset/asset browser + PresetBrowser, +} + +impl PaneType { + /// Get display name for the pane type + pub fn display_name(self) -> &'static str { + match self { + PaneType::Stage => "Stage", + PaneType::Timeline => "Timeline", + PaneType::Toolbar => "Toolbar", + PaneType::Infopanel => "Info Panel", + PaneType::Outliner => "Outliner", + PaneType::PianoRoll => "Piano Roll", + PaneType::NodeEditor => "Node Editor", + PaneType::PresetBrowser => "Preset Browser", + } + } + + /// Get SVG icon file name for the pane type + /// Path is relative to ~/Dev/Lightningbeam-2/src/assets/ + /// TODO: Move assets to lightningbeam-editor/assets/icons/ before release + pub fn icon_file(self) -> &'static str { + match self { + PaneType::Stage => "stage.svg", + PaneType::Timeline => "timeline.svg", + PaneType::Toolbar => "toolbar.svg", + PaneType::Infopanel => "infopanel.svg", + PaneType::Outliner => "stage.svg", // TODO: needs own icon + PaneType::PianoRoll => "piano-roll.svg", + PaneType::NodeEditor => "node-editor.svg", + PaneType::PresetBrowser => "stage.svg", // TODO: needs own icon + } + } + + /// Parse pane type from string name (case-insensitive) + /// Accepts both JS names (timelineV2) and Rust names (timeline) + pub fn from_name(name: &str) -> Option { + match name.to_lowercase().as_str() { + "stage" => Some(PaneType::Stage), + "timeline" | "timelinev2" => Some(PaneType::Timeline), + "toolbar" => Some(PaneType::Toolbar), + "infopanel" => Some(PaneType::Infopanel), + "outlineer" | "outliner" => Some(PaneType::Outliner), + "pianoroll" => Some(PaneType::PianoRoll), + "nodeeditor" => Some(PaneType::NodeEditor), + "presetbrowser" => Some(PaneType::PresetBrowser), + _ => None, + } + } + + /// Get all available pane types + pub fn all() -> &'static [PaneType] { + &[ + PaneType::Stage, + PaneType::Timeline, + PaneType::Toolbar, + PaneType::Infopanel, + PaneType::Outliner, + PaneType::NodeEditor, + PaneType::PianoRoll, + PaneType::PresetBrowser, + ] + } + + /// Get the string name for this pane type (used in JSON) + pub fn to_name(self) -> &'static str { + match self { + PaneType::Stage => "stage", + PaneType::Timeline => "timelineV2", // JSON uses timelineV2 + PaneType::Toolbar => "toolbar", + PaneType::Infopanel => "infopanel", + PaneType::Outliner => "outlineer", // JSON uses outlineer + PaneType::PianoRoll => "pianoRoll", + PaneType::NodeEditor => "nodeEditor", + PaneType::PresetBrowser => "presetBrowser", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pane_type_from_name() { + assert_eq!(PaneType::from_name("stage"), Some(PaneType::Stage)); + assert_eq!(PaneType::from_name("Stage"), Some(PaneType::Stage)); + assert_eq!(PaneType::from_name("STAGE"), Some(PaneType::Stage)); + // Accept both JS name (timelineV2) and Rust name (timeline) + assert_eq!(PaneType::from_name("timelineV2"), Some(PaneType::Timeline)); + assert_eq!(PaneType::from_name("timeline"), Some(PaneType::Timeline)); + assert_eq!(PaneType::from_name("invalid"), None); + } + + #[test] + fn test_pane_type_display() { + assert_eq!(PaneType::Stage.display_name(), "Stage"); + assert_eq!(PaneType::Timeline.display_name(), "Timeline"); + } + + #[test] + fn test_pane_type_icons() { + assert_eq!(PaneType::Stage.icon_file(), "stage.svg"); + assert_eq!(PaneType::Timeline.icon_file(), "timeline.svg"); + assert_eq!(PaneType::NodeEditor.icon_file(), "node-editor.svg"); + } +} diff --git a/lightningbeam-ui/lightningbeam-core/src/tool.rs b/lightningbeam-ui/lightningbeam-core/src/tool.rs new file mode 100644 index 0000000..31546b2 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/tool.rs @@ -0,0 +1,79 @@ +/// Tool system for the toolbar +/// +/// Defines the available drawing/editing tools + +use serde::{Deserialize, Serialize}; + +/// Drawing and editing tools +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Tool { + /// Selection tool - select and move objects + Select, + /// Draw/Pen tool - freehand drawing + Draw, + /// Transform tool - scale, rotate, skew + Transform, + /// Rectangle shape tool + Rectangle, + /// Ellipse/Circle shape tool + Ellipse, + /// Paint bucket - fill areas with color + PaintBucket, + /// Eyedropper - pick colors from the canvas + Eyedropper, +} + +impl Tool { + /// Get display name for the tool + pub fn display_name(self) -> &'static str { + match self { + Tool::Select => "Select", + Tool::Draw => "Draw", + Tool::Transform => "Transform", + Tool::Rectangle => "Rectangle", + Tool::Ellipse => "Ellipse", + Tool::PaintBucket => "Paint Bucket", + Tool::Eyedropper => "Eyedropper", + } + } + + /// Get SVG icon file name for the tool + pub fn icon_file(self) -> &'static str { + match self { + Tool::Select => "select.svg", + Tool::Draw => "draw.svg", + Tool::Transform => "transform.svg", + Tool::Rectangle => "rectangle.svg", + Tool::Ellipse => "ellipse.svg", + Tool::PaintBucket => "paint_bucket.svg", + Tool::Eyedropper => "eyedropper.svg", + } + } + + /// Get all available tools + pub fn all() -> &'static [Tool] { + &[ + Tool::Select, + Tool::Draw, + Tool::Transform, + Tool::Rectangle, + Tool::Ellipse, + Tool::PaintBucket, + Tool::Eyedropper, + ] + } + + /// Get keyboard shortcut hint + pub fn shortcut_hint(self) -> &'static str { + match self { + Tool::Select => "V", + Tool::Draw => "P", + Tool::Transform => "Q", + Tool::Rectangle => "R", + Tool::Ellipse => "E", + Tool::PaintBucket => "B", + Tool::Eyedropper => "I", + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/Cargo.toml b/lightningbeam-ui/lightningbeam-editor/Cargo.toml index 1e0803a..37a1b27 100644 --- a/lightningbeam-ui/lightningbeam-editor/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-editor/Cargo.toml @@ -8,6 +8,7 @@ lightningbeam-core = { path = "../lightningbeam-core" } # UI Framework eframe = { workspace = true } +egui_extras = { workspace = true } # GPU wgpu = { workspace = true } @@ -18,9 +19,16 @@ peniko = { workspace = true } # Windowing winit = { workspace = true } +# Native menus +muda = { workspace = true } + # Serialization serde = { workspace = true } serde_json = { workspace = true } +# Image loading +image = { workspace = true } +resvg = { workspace = true } + # Utilities pollster = { workspace = true } diff --git a/lightningbeam-ui/lightningbeam-editor/PLAN.md b/lightningbeam-ui/lightningbeam-editor/PLAN.md new file mode 100644 index 0000000..15a592f --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/PLAN.md @@ -0,0 +1,461 @@ +# Lightningbeam Rust UI - Implementation Plan + +## Project Overview + +**Goal**: Complete migration from JavaScript/Tauri to Rust/egui +- **Scope**: ~10,000+ lines of code migration +- **Source**: `~/Dev/Lightningbeam-2/src/` (JS models, actions, main.js) +- **Target**: Full-featured Rust animation editor with native performance +- **Motivation**: IPC overhead between Rust↔JS too slow for real-time performance + +## UI Boundaries + +**In Scope (This UI)**: +- Layout system (panes, splits, resize) +- Stage rendering (layer compositing with Vello) +- Timeline (frame scrubbing, keyframes) +- Tools (pen, select, transform) +- Property panels +- User interaction & editing + +**Out of Scope (External Systems)**: +- **Video import/export**: Handled by separate video processing module +- **Audio playback/processing**: Handled by `daw-backend` +- **File I/O**: Coordinated with backend systems +- **Plugin architecture**: TBD in separate crate + +## Technology Stack + +### UI Framework +- **Primary**: `eframe` 0.29 + `egui` (immediate-mode GUI) +- **Theming**: `egui-aesthetix` for professional appearance +- **Windowing**: `winit` 0.30 +- **Native Menus**: `muda` for OS-integrated menus (File, Edit, etc.) + +### Rendering +- **GPU**: `wgpu` 22 for low-level GPU access +- **2D Graphics**: `Vello` 0.3 for high-performance vector rendering + - 84% faster than iced in benchmarks + - Used for: Stage, Timeline, Node Editor, Virtual Piano +- **Architecture**: Layer-based rendering + - Each layer (2D animation, video, etc.) renders to texture + - Textures composited together on Stage canvas + +### Serialization +- **Format**: JSON (serde_json) +- **Compatibility**: Match existing JS JSON schema for layouts + +--- + +## Implementation Phases + +### ✅ Phase 1: Layout System (COMPLETE) + +**Status**: Fully implemented and tested + +**Features**: +- [x] Workspace structure (lightningbeam-core + lightningbeam-editor) +- [x] JSON layout loading (8 predefined layouts) +- [x] Recursive pane tree rendering +- [x] Layout switching via menu +- [x] Drag-to-resize dividers with visual feedback +- [x] Split operations with live preview +- [x] Join operations (remove splits) +- [x] Context menus on dividers +- [x] ESC/click-outside cancellation +- [x] Pane selection and type switching + +**Files**: +- `lightningbeam-core/src/layout.rs` - Core data structures +- `lightningbeam-editor/src/main.rs` - Rendering and interaction +- `assets/layouts.json` - Layout definitions + +--- + +### 🔄 Phase 2: Pane Architecture (CURRENT) + +**Goal**: Define proper pane abstraction with header + content sections + +**Requirements**: +1. **Pane Trait/Struct**: + ```rust + trait Pane { + fn header(&mut self, ui: &mut egui::Ui) -> Option; + fn content(&mut self, ui: &mut egui::Ui, rect: Rect); + fn name(&self) -> &str; + } + ``` + +2. **Header Section**: + - Optional controls (used by Timeline pane) + - Play/pause, zoom, frame counter + - Collapsible/expandable + +3. **Content Section**: + - Main pane body + - Custom rendering per pane type + - Can use egui widgets or custom GPU rendering + +4. **Integration**: + - Update `render_pane()` to use new trait + - Support dynamic pane instantiation + - Maintain layout tree structure + +**Deliverables**: +- [ ] Pane trait in `lightningbeam-core/src/pane.rs` +- [ ] Example placeholder pane implementation +- [ ] Update main.rs to use pane trait +- [ ] Document pane interface + +--- + +### Phase 3: Native Menu Integration + +**Goal**: OS-integrated menu bar using `muda` + +**Features**: +- [ ] File menu (New, Open, Save, Export) +- [ ] Edit menu (Undo, Redo, Cut, Copy, Paste) +- [ ] View menu (Layouts, Zoom, Panels) +- [ ] Help menu (Documentation, About) +- [ ] Platform-specific integration (macOS menu bar, Windows menu bar) +- [ ] Keyboard shortcuts + +**Dependencies**: +```toml +muda = "*" # Native menu system (what Tauri uses) +``` + +--- + +### Phase 4: Stage Pane Implementation + +**Goal**: Main canvas with Vello-based layer compositing + +**Architecture**: +``` +Stage Pane +├── wgpu Surface +├── Vello Scene +└── Layer Renderer + ├── Layer 1 → Texture + ├── Layer 2 → Texture + ├── Layer 3 → Texture (video from external) + └── Composite → Final render +``` + +**Features**: +- [ ] wgpu surface integration with egui +- [ ] Vello scene management +- [ ] Layer texture rendering +- [ ] Compositing pipeline +- [ ] Camera/viewport controls (pan, zoom) +- [ ] Selection visualization +- [ ] Tool integration (later phase) + +**Performance Targets**: +- 60 FPS at 1920x1080 +- Sub-16ms frame time +- Smooth layer compositing + +--- + +### Phase 5: Timeline Pane Implementation + +**Goal**: Frame-based animation timeline with Vello rendering + +**Features**: +- [ ] Frame scrubber with Vello +- [ ] Layer tracks +- [ ] Keyframe visualization +- [ ] Playback controls in header +- [ ] Zoom/pan timeline +- [ ] Frame selection +- [ ] Drag keyframes +- [ ] Multi-selection + +**Header Controls**: +- Play/pause button +- Current frame indicator +- FPS selector +- Zoom slider + +**Audio Integration**: +- Display audio waveforms (data from `daw-backend`) +- Sync playback with audio system +- No direct audio processing in UI + +--- + +### Phase 6: Additional Panes + +**Priority Order**: +1. **Toolbar** (simple, egui widgets) +2. **Info Panel** (property editor, egui widgets) +3. **Outliner** (layer hierarchy, egui tree) +4. **Node Editor** (Vello-based graph) +5. **Piano Roll** (MIDI editor, Vello) +6. **Preset Browser** (file list, egui) + +--- + +### Phase 7: Core Class Migration + +**Source**: `~/Dev/Lightningbeam-2/src/models/` + +#### Models to Migrate + +**From `~/Dev/Lightningbeam-2/src/models/`**: + +1. **root.js** (34 lines) + - Document root structure + - → `lightningbeam-core/src/document.rs` + +2. **layer.js** + - Layer types (2D, video, audio, etc.) + - Transform properties + - Visibility, opacity + - → `lightningbeam-core/src/layer.rs` + +3. **shapes.js** (752 lines) + - Path, Rectangle, Ellipse, Star, Polygon + - Stroke/fill properties + - Bezier curves + - → `lightningbeam-core/src/shape.rs` + +4. **animation.js** + - Keyframe data structures + - Interpolation curves + - → `lightningbeam-core/src/animation.rs` + +5. **graphics-object.js** + - Base graphics properties + - Transform matrices + - → `lightningbeam-core/src/graphics.rs` + +#### Actions to Migrate + +**From `~/Dev/Lightningbeam-2/src/actions/`**: + +1. **index.js** (2,615 lines) + - Drawing tools + - Transform operations + - Layer operations + - → `lightningbeam-core/src/actions/` + +2. **selection-actions.js** (166 lines) + - Selection state management + - → `lightningbeam-core/src/selection.rs` + +#### Migration Strategy + +1. **Rust-first design**: Leverage Rust's type system, don't just transliterate JS +2. **Serde compatibility**: Ensure classes can serialize/deserialize from existing JSON +3. **Performance**: Use `Arc>` for shared mutable state where needed +4. **Memory safety**: Eliminate runtime errors through compile-time checks + +--- + +### Phase 8: Tools & Interaction + +**After core classes are migrated**: + +- [ ] Pen tool (Bezier curves) +- [ ] Select tool (bounding box, direct selection) +- [ ] Transform tool (rotate, scale, skew) +- [ ] Shape tools (rectangle, ellipse, star, polygon) +- [ ] Text tool +- [ ] Eyedropper +- [ ] Zoom/pan + +--- + +### Phase 9: Feature Parity + +**Remaining features from JS version**: + +- [ ] Onion skinning +- [ ] Frame export (PNG sequence) +- [ ] Project save/load +- [ ] Undo/redo system +- [ ] Preferences/settings +- [ ] Keyboard shortcuts +- [ ] Help/documentation +- [ ] Clipboard operations (copy/paste) + +--- + +## Architecture Decisions + +### Why egui over iced? +- **Vello compatibility**: iced has issues with Vello integration +- **Immediate mode**: Simpler state management for complex UI +- **Maturity**: More stable and well-documented +- **Performance**: Good enough for our needs + +### Layer Rendering Strategy +```rust +// Conceptual pipeline +for layer in document.layers { + let texture = layer.render_to_texture(vello_renderer); + composite_textures.push(texture); +} +let final_image = compositor.blend(composite_textures); +stage.display(final_image); +``` + +### State Management +- **Document state**: `Arc>` - shared across panes +- **Selection state**: Event-based updates +- **UI state**: Local to each pane (egui handles this) + +### External System Integration + +**Video Layers**: +- UI requests frame texture from video processing module +- Module returns GPU texture handle +- UI composites texture with other layers + +**Audio Playback**: +- UI sends playback commands to `daw-backend` +- Backend handles audio processing/mixing +- UI displays waveforms (data from backend) + +--- + +## File Structure (Target) + +``` +lightningbeam-ui/ +├── lightningbeam-core/ # Pure Rust library +│ ├── src/ +│ │ ├── lib.rs +│ │ ├── layout.rs # ✅ Layout system +│ │ ├── pane.rs # ⏳ Pane trait +│ │ ├── document.rs # ❌ Document root +│ │ ├── layer.rs # ❌ Layer types +│ │ ├── shape.rs # ❌ Shape primitives +│ │ ├── animation.rs # ❌ Keyframes +│ │ ├── graphics.rs # ❌ Graphics base +│ │ ├── selection.rs # ❌ Selection state +│ │ └── actions/ # ❌ Action system +│ └── Cargo.toml +│ +├── lightningbeam-editor/ # egui application +│ ├── src/ +│ │ ├── main.rs # ✅ App + layout rendering +│ │ ├── menu.rs # ❌ Native menu integration (muda) +│ │ ├── panes/ # ⏳ Pane implementations +│ │ │ ├── stage.rs # ❌ Stage pane +│ │ │ ├── timeline.rs # ❌ Timeline pane +│ │ │ ├── toolbar.rs # ❌ Toolbar +│ │ │ ├── infopanel.rs # ❌ Info panel +│ │ │ └── outliner.rs # ❌ Outliner +│ │ ├── rendering/ # ❌ Vello integration +│ │ └── tools/ # ❌ Drawing tools +│ ├── assets/ +│ │ └── layouts.json # ✅ Layout definitions +│ └── Cargo.toml +│ +└── Cargo.toml # Workspace +``` + +Legend: ✅ Complete | ⏳ In Progress | ❌ Not Started + +--- + +## Dependencies Roadmap + +### Current (Phase 1) +```toml +eframe = "0.29" # UI framework +wgpu = "22" # GPU +vello = "0.3" # 2D rendering +kurbo = "0.11" # 2D geometry +peniko = "0.5" # 2D primitives +serde = "1.0" # Serialization +serde_json = "1.0" # JSON +``` + +### Phase 2-3 Additions +```toml +muda = "*" # Native OS menus +``` + +### Phase 4+ Additions +```toml +image = "*" # Image loading +egui-aesthetix = "*" # Theming +clipboard = "*" # Copy/paste +``` + +--- + +## Performance Goals + +### Rendering +- **Stage**: 60 FPS @ 1080p, 100+ layers +- **Timeline**: Smooth scrolling with 1000+ frames +- **Memory**: < 500MB for typical project + +### Benchmarks to Track +- Layer render time +- Composite time +- UI frame time +- Memory usage per layer + +--- + +## Testing Strategy + +1. **Unit tests**: Core classes (Layer, Shape, Animation) +2. **Integration tests**: Pane rendering +3. **Visual tests**: Screenshot comparison for rendering +4. **Performance tests**: Benchmark critical paths +5. **Manual testing**: UI interaction, edge cases + +--- + +## Migration Checklist + +### Before Deprecating JS Version +- [ ] All panes functional +- [ ] Core classes migrated +- [ ] Project save/load working +- [ ] Performance meets/exceeds JS version +- [ ] No critical bugs +- [ ] User testing complete +- [ ] Native menus integrated +- [ ] External system integration verified (video, audio) + +--- + +## Assets Management + +**Current Approach**: +- SVG icons referenced from `~/Dev/Lightningbeam-2/src/assets/` +- Allows pulling in new icons/changes from upstream during rewrite +- Using `egui_extras::RetainedImage` for cached SVG rendering + +**TODO (Before Release)**: +- [ ] Move assets to `lightningbeam-editor/assets/icons/` +- [ ] Update asset paths in code +- [ ] Set up asset bundling/embedding + +--- + +## Open Questions + +1. **Plugin system**: Support for extensions? +2. **Scripting**: Embed Lua/JavaScript for automation? +3. **Collaborative editing**: Future consideration? + +--- + +## References + +- **JS Source**: `~/Dev/Lightningbeam-2/src/` +- **egui docs**: https://docs.rs/egui +- **Vello docs**: https://docs.rs/vello +- **wgpu docs**: https://docs.rs/wgpu +- **muda docs**: https://docs.rs/muda diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 65e6752..ff633a8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -1,5 +1,14 @@ use eframe::egui; -use lightningbeam_core::layout::{LayoutDefinition, LayoutNode, PaneType}; +use lightningbeam_core::layout::{LayoutDefinition, LayoutNode}; +use lightningbeam_core::pane::PaneType; +use lightningbeam_core::tool::Tool; +use std::collections::HashMap; + +mod panes; +use panes::{PaneInstance, PaneRenderer, SharedPaneState}; + +mod menu; +use menu::{MenuAction, MenuSystem}; fn main() -> eframe::Result { println!("🚀 Starting Lightningbeam Editor..."); @@ -21,7 +30,7 @@ fn main() -> eframe::Result { eframe::run_native( "Lightningbeam Editor", options, - Box::new(move |_cc| Ok(Box::new(EditorApp::new(layouts)))), + Box::new(move |cc| Ok(Box::new(EditorApp::new(cc, layouts)))), ) } @@ -30,27 +39,440 @@ fn load_layouts() -> Vec { serde_json::from_str(json).expect("Failed to parse layouts.json") } -struct EditorApp { - layouts: Vec, - current_layout_index: usize, +/// Path to a node in the layout tree (indices of children) +type NodePath = Vec; + +#[derive(Default)] +struct DragState { + is_dragging: bool, + node_path: NodePath, + is_horizontal: bool, } -impl EditorApp { - fn new(layouts: Vec) -> Self { +/// Action to perform on the layout tree +enum LayoutAction { + SplitHorizontal(NodePath, f32), // path, percent + SplitVertical(NodePath, f32), // path, percent + RemoveSplit(NodePath), + EnterSplitPreviewHorizontal, + EnterSplitPreviewVertical, +} + +#[derive(Default)] +enum SplitPreviewMode { + #[default] + None, + Active { + is_horizontal: bool, + hovered_pane: Option, + split_percent: f32, + }, +} + +/// Icon cache for pane type icons +struct IconCache { + icons: HashMap, + assets_path: std::path::PathBuf, +} + +impl IconCache { + fn new() -> Self { + let assets_path = std::path::PathBuf::from( + std::env::var("HOME").unwrap_or_else(|_| "/home/skyler".to_string()) + ).join("Dev/Lightningbeam-2/src/assets"); + Self { - layouts, - current_layout_index: 0, + icons: HashMap::new(), + assets_path, } } - fn current_layout(&self) -> &LayoutDefinition { + fn get_or_load(&mut self, pane_type: PaneType) -> Option<&egui_extras::RetainedImage> { + if !self.icons.contains_key(&pane_type) { + // Load and cache the icon + let icon_path = self.assets_path.join(pane_type.icon_file()); + if let Ok(image) = egui_extras::RetainedImage::from_svg_bytes( + pane_type.icon_file(), + &std::fs::read(&icon_path).unwrap_or_default(), + ) { + self.icons.insert(pane_type, image); + } + } + self.icons.get(&pane_type) + } +} + +/// Icon cache for tool icons +struct ToolIconCache { + icons: HashMap, + assets_path: std::path::PathBuf, +} + +impl ToolIconCache { + fn new() -> Self { + let assets_path = std::path::PathBuf::from( + std::env::var("HOME").unwrap_or_else(|_| "/home/skyler".to_string()) + ).join("Dev/Lightningbeam-2/src/assets"); + + Self { + icons: HashMap::new(), + assets_path, + } + } + + fn get_or_load(&mut self, tool: Tool, ctx: &egui::Context) -> Option<&egui::TextureHandle> { + if !self.icons.contains_key(&tool) { + // Load SVG and rasterize at high resolution using resvg + let icon_path = self.assets_path.join(tool.icon_file()); + if let Ok(svg_data) = std::fs::read(&icon_path) { + // Rasterize at 3x size for crisp display (180px for 60px display) + let render_size = 180; + + if let Ok(tree) = resvg::usvg::Tree::from_data(&svg_data, &resvg::usvg::Options::default()) { + let pixmap_size = tree.size().to_int_size(); + let scale_x = render_size as f32 / pixmap_size.width() as f32; + let scale_y = render_size as f32 / pixmap_size.height() as f32; + let scale = scale_x.min(scale_y); + + let final_size = resvg::usvg::Size::from_wh( + pixmap_size.width() as f32 * scale, + pixmap_size.height() as f32 * scale, + ).unwrap_or(resvg::usvg::Size::from_wh(render_size as f32, render_size as f32).unwrap()); + + if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new( + final_size.width() as u32, + final_size.height() as u32, + ) { + let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + // Convert RGBA8 to egui ColorImage + let rgba_data = pixmap.data(); + let size = [pixmap.width() as usize, pixmap.height() as usize]; + let color_image = egui::ColorImage::from_rgba_unmultiplied(size, rgba_data); + + // Upload to GPU + let texture = ctx.load_texture( + tool.icon_file(), + color_image, + egui::TextureOptions::LINEAR, + ); + self.icons.insert(tool, texture); + } + } + } + } + self.icons.get(&tool) + } +} + +struct EditorApp { + layouts: Vec, + current_layout_index: usize, + current_layout: LayoutNode, // Mutable copy for editing + drag_state: DragState, + hovered_divider: Option<(NodePath, bool)>, // (path, is_horizontal) + selected_pane: Option, // Currently selected pane for editing + split_preview_mode: SplitPreviewMode, + icon_cache: IconCache, + tool_icon_cache: ToolIconCache, + selected_tool: Tool, // Currently selected drawing tool + fill_color: egui::Color32, // Fill color for drawing + stroke_color: egui::Color32, // Stroke color for drawing + pane_instances: HashMap, // Pane instances per path + menu_system: Option, // Native menu system + menu_initialized: bool, // Track if menu has been initialized with window +} + +impl EditorApp { + fn new(cc: &eframe::CreationContext, layouts: Vec) -> Self { + let current_layout = layouts[0].layout.clone(); + + // Initialize native menu system + let menu_system = MenuSystem::new().ok(); + + Self { + layouts, + current_layout_index: 0, + current_layout, + drag_state: DragState::default(), + hovered_divider: None, + selected_pane: None, + split_preview_mode: SplitPreviewMode::default(), + icon_cache: IconCache::new(), + tool_icon_cache: ToolIconCache::new(), + selected_tool: Tool::Select, // Default tool + fill_color: egui::Color32::from_rgb(100, 100, 255), // Default blue fill + stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke + pane_instances: HashMap::new(), // Initialize empty, panes created on-demand + menu_system, + menu_initialized: false, + } + } + + fn switch_layout(&mut self, index: usize) { + self.current_layout_index = index; + self.current_layout = self.layouts[index].layout.clone(); + } + + fn current_layout_def(&self) -> &LayoutDefinition { &self.layouts[self.current_layout_index] } + + fn apply_layout_action(&mut self, action: LayoutAction) { + match action { + LayoutAction::SplitHorizontal(path, percent) => { + split_node(&mut self.current_layout, &path, true, percent); + } + LayoutAction::SplitVertical(path, percent) => { + split_node(&mut self.current_layout, &path, false, percent); + } + LayoutAction::RemoveSplit(path) => { + remove_split(&mut self.current_layout, &path); + } + LayoutAction::EnterSplitPreviewHorizontal => { + self.split_preview_mode = SplitPreviewMode::Active { + is_horizontal: false, // horizontal divider = vertical grid (top/bottom) + hovered_pane: None, + split_percent: 50.0, + }; + } + LayoutAction::EnterSplitPreviewVertical => { + self.split_preview_mode = SplitPreviewMode::Active { + is_horizontal: true, // vertical divider = horizontal grid (left/right) + hovered_pane: None, + split_percent: 50.0, + }; + } + } + } + + fn handle_menu_action(&mut self, action: MenuAction) { + match action { + // File menu + MenuAction::NewFile => { + println!("Menu: New File"); + // TODO: Implement new file + } + MenuAction::NewWindow => { + println!("Menu: New Window"); + // TODO: Implement new window + } + MenuAction::Save => { + println!("Menu: Save"); + // TODO: Implement save + } + MenuAction::SaveAs => { + println!("Menu: Save As"); + // TODO: Implement save as + } + MenuAction::OpenFile => { + println!("Menu: Open File"); + // TODO: Implement open file + } + MenuAction::Revert => { + println!("Menu: Revert"); + // TODO: Implement revert + } + MenuAction::Import => { + println!("Menu: Import"); + // TODO: Implement import + } + MenuAction::Export => { + println!("Menu: Export"); + // TODO: Implement export + } + MenuAction::Quit => { + println!("Menu: Quit"); + std::process::exit(0); + } + + // Edit menu + MenuAction::Undo => { + println!("Menu: Undo"); + // TODO: Implement undo + } + MenuAction::Redo => { + println!("Menu: Redo"); + // TODO: Implement redo + } + MenuAction::Cut => { + println!("Menu: Cut"); + // TODO: Implement cut + } + MenuAction::Copy => { + println!("Menu: Copy"); + // TODO: Implement copy + } + MenuAction::Paste => { + println!("Menu: Paste"); + // TODO: Implement paste + } + MenuAction::Delete => { + println!("Menu: Delete"); + // TODO: Implement delete + } + MenuAction::SelectAll => { + println!("Menu: Select All"); + // TODO: Implement select all + } + MenuAction::SelectNone => { + println!("Menu: Select None"); + // TODO: Implement select none + } + MenuAction::Preferences => { + println!("Menu: Preferences"); + // TODO: Implement preferences dialog + } + + // Modify menu + MenuAction::Group => { + println!("Menu: Group"); + // TODO: Implement group + } + MenuAction::SendToBack => { + println!("Menu: Send to Back"); + // TODO: Implement send to back + } + MenuAction::BringToFront => { + println!("Menu: Bring to Front"); + // TODO: Implement bring to front + } + + // Layer menu + MenuAction::AddLayer => { + println!("Menu: Add Layer"); + // TODO: Implement add layer + } + MenuAction::AddVideoLayer => { + println!("Menu: Add Video Layer"); + // TODO: Implement add video layer + } + MenuAction::AddAudioTrack => { + println!("Menu: Add Audio Track"); + // TODO: Implement add audio track + } + MenuAction::AddMidiTrack => { + println!("Menu: Add MIDI Track"); + // TODO: Implement add MIDI track + } + MenuAction::DeleteLayer => { + println!("Menu: Delete Layer"); + // TODO: Implement delete layer + } + MenuAction::ToggleLayerVisibility => { + println!("Menu: Toggle Layer Visibility"); + // TODO: Implement toggle layer visibility + } + + // Timeline menu + MenuAction::NewKeyframe => { + println!("Menu: New Keyframe"); + // TODO: Implement new keyframe + } + MenuAction::NewBlankKeyframe => { + println!("Menu: New Blank Keyframe"); + // TODO: Implement new blank keyframe + } + MenuAction::DeleteFrame => { + println!("Menu: Delete Frame"); + // TODO: Implement delete frame + } + MenuAction::DuplicateKeyframe => { + println!("Menu: Duplicate Keyframe"); + // TODO: Implement duplicate keyframe + } + MenuAction::AddKeyframeAtPlayhead => { + println!("Menu: Add Keyframe at Playhead"); + // TODO: Implement add keyframe at playhead + } + MenuAction::AddMotionTween => { + println!("Menu: Add Motion Tween"); + // TODO: Implement add motion tween + } + MenuAction::AddShapeTween => { + println!("Menu: Add Shape Tween"); + // TODO: Implement add shape tween + } + MenuAction::ReturnToStart => { + println!("Menu: Return to Start"); + // TODO: Implement return to start + } + MenuAction::Play => { + println!("Menu: Play"); + // TODO: Implement play/pause + } + + // View menu + MenuAction::ZoomIn => { + println!("Menu: Zoom In"); + // TODO: Implement zoom in + } + MenuAction::ZoomOut => { + println!("Menu: Zoom Out"); + // TODO: Implement zoom out + } + MenuAction::ActualSize => { + println!("Menu: Actual Size"); + // TODO: Implement actual size (100% zoom) + } + MenuAction::RecenterView => { + println!("Menu: Recenter View"); + // TODO: Implement recenter view + } + MenuAction::NextLayout => { + println!("Menu: Next Layout"); + let next_index = (self.current_layout_index + 1) % self.layouts.len(); + self.switch_layout(next_index); + } + MenuAction::PreviousLayout => { + println!("Menu: Previous Layout"); + let prev_index = if self.current_layout_index == 0 { + self.layouts.len() - 1 + } else { + self.current_layout_index - 1 + }; + self.switch_layout(prev_index); + } + MenuAction::SwitchLayout(index) => { + println!("Menu: Switch to Layout {}", index); + if index < self.layouts.len() { + self.switch_layout(index); + } + } + + // Help menu + MenuAction::About => { + println!("Menu: About"); + // TODO: Implement about dialog + } + + // Lightningbeam menu (macOS) + MenuAction::Settings => { + println!("Menu: Settings"); + // TODO: Implement settings + } + MenuAction::CloseWindow => { + println!("Menu: Close Window"); + // TODO: Implement close window + } + } + } } impl eframe::App for EditorApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Check for native menu events + if let Some(menu_system) = &self.menu_system { + if let Some(action) = menu_system.check_events() { + self.handle_menu_action(action); + } + } + // Top menu bar + let mut layout_to_switch: Option = None; + let current_name = self.current_layout_def().name.clone(); + egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("Layout", |ui| { @@ -59,130 +481,767 @@ impl eframe::App for EditorApp { .selectable_label(i == self.current_layout_index, &layout.name) .clicked() { - self.current_layout_index = i; + layout_to_switch = Some(i); ui.close_menu(); } } }); ui.separator(); - ui.label(format!("Current: {}", self.current_layout().name)); + ui.label(format!("Current: {}", current_name)); }); }); + // Switch layout after menu closes to avoid borrow issues + if let Some(index) = layout_to_switch { + self.switch_layout(index); + } + // Main pane area + let mut layout_action: Option = None; egui::CentralPanel::default().show(ctx, |ui| { let available_rect = ui.available_rect_before_wrap(); - render_layout_node(ui, &self.current_layout().layout, available_rect); + + // Reset hovered divider each frame + self.hovered_divider = None; + + render_layout_node( + ui, + &mut self.current_layout, + available_rect, + &mut self.drag_state, + &mut self.hovered_divider, + &mut self.selected_pane, + &mut layout_action, + &mut self.split_preview_mode, + &mut self.icon_cache, + &mut self.tool_icon_cache, + &mut self.selected_tool, + &mut self.fill_color, + &mut self.stroke_color, + &mut self.pane_instances, + &Vec::new(), // Root path + ); + + // Set cursor based on hover state + if let Some((_, is_horizontal)) = self.hovered_divider { + if is_horizontal { + ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal); + } else { + ctx.set_cursor_icon(egui::CursorIcon::ResizeVertical); + } + } }); + + // Handle ESC key and click-outside to cancel split preview + if let SplitPreviewMode::Active { hovered_pane, .. } = &self.split_preview_mode { + let should_cancel = ctx.input(|i| { + // Cancel on ESC key + if i.key_pressed(egui::Key::Escape) { + return true; + } + // Cancel on click outside any pane + if i.pointer.primary_clicked() && hovered_pane.is_none() { + return true; + } + false + }); + + if should_cancel { + self.split_preview_mode = SplitPreviewMode::None; + } + } + + // Apply layout action after rendering to avoid borrow issues + if let Some(action) = layout_action { + self.apply_layout_action(action); + } } } -/// Recursively render a layout node -fn render_layout_node(ui: &mut egui::Ui, node: &LayoutNode, rect: egui::Rect) { +/// Recursively render a layout node with drag support +fn render_layout_node( + ui: &mut egui::Ui, + node: &mut LayoutNode, + rect: egui::Rect, + drag_state: &mut DragState, + hovered_divider: &mut Option<(NodePath, bool)>, + selected_pane: &mut Option, + layout_action: &mut Option, + split_preview_mode: &mut SplitPreviewMode, + icon_cache: &mut IconCache, + tool_icon_cache: &mut ToolIconCache, + selected_tool: &mut Tool, + fill_color: &mut egui::Color32, + stroke_color: &mut egui::Color32, + pane_instances: &mut HashMap, + path: &NodePath, +) { match node { LayoutNode::Pane { name } => { - render_pane(ui, name, rect); + render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path); } LayoutNode::HorizontalGrid { percent, children } => { + // Handle dragging + if drag_state.is_dragging && drag_state.node_path == *path { + if let Some(pointer_pos) = ui.input(|i| i.pointer.interact_pos()) { + // Calculate new percentage based on pointer position + let new_percent = ((pointer_pos.x - rect.left()) / rect.width() * 100.0) + .clamp(10.0, 90.0); // Clamp to prevent too small panes + *percent = new_percent; + } + } + // Split horizontally (left | right) - let split_x = rect.left() + (rect.width() * percent / 100.0); + let split_x = rect.left() + (rect.width() * *percent / 100.0); - let left_rect = egui::Rect::from_min_max( - rect.min, - egui::pos2(split_x, rect.max.y), - ); + let left_rect = egui::Rect::from_min_max(rect.min, egui::pos2(split_x, rect.max.y)); - let right_rect = egui::Rect::from_min_max( - egui::pos2(split_x, rect.min.y), - rect.max, - ); + let right_rect = + egui::Rect::from_min_max(egui::pos2(split_x, rect.min.y), rect.max); // Render children - render_layout_node(ui, &children[0], left_rect); - render_layout_node(ui, &children[1], right_rect); + let mut left_path = path.clone(); + left_path.push(0); + render_layout_node( + ui, + &mut children[0], + left_rect, + drag_state, + hovered_divider, + selected_pane, + layout_action, + split_preview_mode, + icon_cache, + tool_icon_cache, + selected_tool, + fill_color, + stroke_color, + pane_instances, + &left_path, + ); + + let mut right_path = path.clone(); + right_path.push(1); + render_layout_node( + ui, + &mut children[1], + right_rect, + drag_state, + hovered_divider, + selected_pane, + layout_action, + split_preview_mode, + icon_cache, + tool_icon_cache, + selected_tool, + fill_color, + stroke_color, + pane_instances, + &right_path, + ); + + // Draw divider with interaction + let divider_width = 8.0; + let divider_rect = egui::Rect::from_min_max( + egui::pos2(split_x - divider_width / 2.0, rect.min.y), + egui::pos2(split_x + divider_width / 2.0, rect.max.y), + ); + + let divider_id = ui.id().with(("divider", path)); + let response = ui.interact(divider_rect, divider_id, egui::Sense::click_and_drag()); + + // Check if pointer is over divider + if response.hovered() { + *hovered_divider = Some((path.clone(), true)); + } + + // Handle drag start + if response.drag_started() { + drag_state.is_dragging = true; + drag_state.node_path = path.clone(); + drag_state.is_horizontal = true; + } + + // Handle drag end + if response.drag_stopped() { + drag_state.is_dragging = false; + } + + // Context menu on right-click + response.context_menu(|ui| { + ui.set_min_width(180.0); + + if ui.button("Split Horizontal ->").clicked() { + *layout_action = Some(LayoutAction::EnterSplitPreviewHorizontal); + ui.close_menu(); + } + + if ui.button("Split Vertical |").clicked() { + *layout_action = Some(LayoutAction::EnterSplitPreviewVertical); + ui.close_menu(); + } + + ui.separator(); + + if ui.button("< Join Left").clicked() { + let mut path_keep_right = path.clone(); + path_keep_right.push(1); // Remove left, keep right child + *layout_action = Some(LayoutAction::RemoveSplit(path_keep_right)); + ui.close_menu(); + } + + if ui.button("Join Right >").clicked() { + let mut path_keep_left = path.clone(); + path_keep_left.push(0); // Remove right, keep left child + *layout_action = Some(LayoutAction::RemoveSplit(path_keep_left)); + ui.close_menu(); + } + + }); + + // Visual feedback + let divider_color = if response.hovered() || response.dragged() { + egui::Color32::from_gray(120) + } else { + egui::Color32::from_gray(60) + }; - // Draw divider ui.painter().vline( split_x, rect.y_range(), - egui::Stroke::new(2.0, egui::Color32::from_gray(60)), + egui::Stroke::new(2.0, divider_color), ); } LayoutNode::VerticalGrid { percent, children } => { + // Handle dragging + if drag_state.is_dragging && drag_state.node_path == *path { + if let Some(pointer_pos) = ui.input(|i| i.pointer.interact_pos()) { + // Calculate new percentage based on pointer position + let new_percent = ((pointer_pos.y - rect.top()) / rect.height() * 100.0) + .clamp(10.0, 90.0); // Clamp to prevent too small panes + *percent = new_percent; + } + } + // Split vertically (top / bottom) - let split_y = rect.top() + (rect.height() * percent / 100.0); + let split_y = rect.top() + (rect.height() * *percent / 100.0); - let top_rect = egui::Rect::from_min_max( - rect.min, - egui::pos2(rect.max.x, split_y), - ); + let top_rect = egui::Rect::from_min_max(rect.min, egui::pos2(rect.max.x, split_y)); - let bottom_rect = egui::Rect::from_min_max( - egui::pos2(rect.min.x, split_y), - rect.max, - ); + let bottom_rect = + egui::Rect::from_min_max(egui::pos2(rect.min.x, split_y), rect.max); // Render children - render_layout_node(ui, &children[0], top_rect); - render_layout_node(ui, &children[1], bottom_rect); + let mut top_path = path.clone(); + top_path.push(0); + render_layout_node( + ui, + &mut children[0], + top_rect, + drag_state, + hovered_divider, + selected_pane, + layout_action, + split_preview_mode, + icon_cache, + tool_icon_cache, + selected_tool, + fill_color, + stroke_color, + pane_instances, + &top_path, + ); + + let mut bottom_path = path.clone(); + bottom_path.push(1); + render_layout_node( + ui, + &mut children[1], + bottom_rect, + drag_state, + hovered_divider, + selected_pane, + layout_action, + split_preview_mode, + icon_cache, + tool_icon_cache, + selected_tool, + fill_color, + stroke_color, + pane_instances, + &bottom_path, + ); + + // Draw divider with interaction + let divider_height = 8.0; + let divider_rect = egui::Rect::from_min_max( + egui::pos2(rect.min.x, split_y - divider_height / 2.0), + egui::pos2(rect.max.x, split_y + divider_height / 2.0), + ); + + let divider_id = ui.id().with(("divider", path)); + let response = ui.interact(divider_rect, divider_id, egui::Sense::click_and_drag()); + + // Check if pointer is over divider + if response.hovered() { + *hovered_divider = Some((path.clone(), false)); + } + + // Handle drag start + if response.drag_started() { + drag_state.is_dragging = true; + drag_state.node_path = path.clone(); + drag_state.is_horizontal = false; + } + + // Handle drag end + if response.drag_stopped() { + drag_state.is_dragging = false; + } + + // Context menu on right-click + response.context_menu(|ui| { + ui.set_min_width(180.0); + + if ui.button("Split Horizontal ->").clicked() { + *layout_action = Some(LayoutAction::EnterSplitPreviewHorizontal); + ui.close_menu(); + } + + if ui.button("Split Vertical |").clicked() { + *layout_action = Some(LayoutAction::EnterSplitPreviewVertical); + ui.close_menu(); + } + + ui.separator(); + + if ui.button("^ Join Up").clicked() { + let mut path_keep_bottom = path.clone(); + path_keep_bottom.push(1); // Remove top, keep bottom child + *layout_action = Some(LayoutAction::RemoveSplit(path_keep_bottom)); + ui.close_menu(); + } + + if ui.button("Join Down v").clicked() { + let mut path_keep_top = path.clone(); + path_keep_top.push(0); // Remove bottom, keep top child + *layout_action = Some(LayoutAction::RemoveSplit(path_keep_top)); + ui.close_menu(); + } + + }); + + // Visual feedback + let divider_color = if response.hovered() || response.dragged() { + egui::Color32::from_gray(120) + } else { + egui::Color32::from_gray(60) + }; - // Draw divider ui.painter().hline( rect.x_range(), split_y, - egui::Stroke::new(2.0, egui::Color32::from_gray(60)), + egui::Stroke::new(2.0, divider_color), ); } } } /// Render a single pane with its content -fn render_pane(ui: &mut egui::Ui, pane_name: &str, rect: egui::Rect) { +fn render_pane( + ui: &mut egui::Ui, + pane_name: &mut String, + rect: egui::Rect, + selected_pane: &mut Option, + layout_action: &mut Option, + split_preview_mode: &mut SplitPreviewMode, + icon_cache: &mut IconCache, + tool_icon_cache: &mut ToolIconCache, + selected_tool: &mut Tool, + fill_color: &mut egui::Color32, + stroke_color: &mut egui::Color32, + pane_instances: &mut HashMap, + path: &NodePath, +) { let pane_type = PaneType::from_name(pane_name); - // Get color for pane type + // Define header and content areas + let header_height = 40.0; + let header_rect = egui::Rect::from_min_size( + rect.min, + egui::vec2(rect.width(), header_height), + ); + let content_rect = egui::Rect::from_min_size( + rect.min + egui::vec2(0.0, header_height), + egui::vec2(rect.width(), rect.height() - header_height), + ); + + // Draw header background + ui.painter().rect_filled( + header_rect, + 0.0, + egui::Color32::from_rgb(35, 35, 35), + ); + + // Draw content background let bg_color = if let Some(pane_type) = pane_type { pane_color(pane_type) } else { egui::Color32::from_rgb(40, 40, 40) }; + ui.painter().rect_filled(content_rect, 0.0, bg_color); - // Draw background - ui.painter().rect_filled(rect, 0.0, bg_color); - - // Draw border + // Draw border around entire pane + let border_color = egui::Color32::from_gray(80); + let border_width = 1.0; ui.painter().rect_stroke( rect, 0.0, - egui::Stroke::new(1.0, egui::Color32::from_gray(80)), + egui::Stroke::new(border_width, border_color), ); - // Draw pane label - let text = if let Some(pane_type) = pane_type { + // Draw header separator line + ui.painter().hline( + rect.x_range(), + header_rect.max.y, + egui::Stroke::new(1.0, egui::Color32::from_gray(50)), + ); + + // Render icon button in header (left side) + let icon_size = 24.0; + let icon_padding = 8.0; + let icon_button_rect = egui::Rect::from_min_size( + header_rect.min + egui::vec2(icon_padding, icon_padding), + egui::vec2(icon_size, icon_size), + ); + + // Draw icon button background + ui.painter().rect_filled( + icon_button_rect, + 4.0, + egui::Color32::from_rgba_premultiplied(50, 50, 50, 200), + ); + + // Load and render icon if available + if let Some(pane_type) = pane_type { + if let Some(icon) = icon_cache.get_or_load(pane_type) { + let icon_texture_id = icon.texture_id(ui.ctx()); + let icon_rect = icon_button_rect.shrink(2.0); // Small padding inside button + ui.painter().image( + icon_texture_id, + icon_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + } + + // Make icon button interactive (show pane type menu on click) + let icon_button_id = ui.id().with(("icon_button", path)); + let icon_response = ui.interact(icon_button_rect, icon_button_id, egui::Sense::click()); + + if icon_response.hovered() { + ui.painter().rect_stroke( + icon_button_rect, + 4.0, + egui::Stroke::new(1.0, egui::Color32::from_gray(180)), + ); + } + + // Show pane type selector menu on left click + let menu_id = ui.id().with(("pane_type_menu", path)); + if icon_response.clicked() { + ui.memory_mut(|mem| mem.toggle_popup(menu_id)); + } + + egui::popup::popup_below_widget(ui, menu_id, &icon_response, egui::PopupCloseBehavior::CloseOnClickOutside, |ui| { + ui.set_min_width(200.0); + ui.label("Select Pane Type:"); + ui.separator(); + + for pane_type_option in PaneType::all() { + // Load icon for this pane type + if let Some(icon) = icon_cache.get_or_load(*pane_type_option) { + ui.horizontal(|ui| { + // Show icon + let icon_texture_id = icon.texture_id(ui.ctx()); + let icon_size = egui::vec2(16.0, 16.0); + ui.add(egui::Image::new((icon_texture_id, icon_size))); + + // Show label with selection + if ui.selectable_label( + pane_type == Some(*pane_type_option), + pane_type_option.display_name() + ).clicked() { + *pane_name = pane_type_option.to_name().to_string(); + ui.memory_mut(|mem| mem.close_popup()); + } + }); + } else { + // Fallback if icon fails to load + if ui.selectable_label( + pane_type == Some(*pane_type_option), + pane_type_option.display_name() + ).clicked() { + *pane_name = pane_type_option.to_name().to_string(); + ui.memory_mut(|mem| mem.close_popup()); + } + } + } + }); + + // Draw pane title in header + let title_text = if let Some(pane_type) = pane_type { pane_type.display_name() } else { - pane_name + pane_name.as_str() }; - - let text_pos = rect.center() - egui::vec2(40.0, 10.0); + let title_pos = header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0, header_height / 2.0); ui.painter().text( - text_pos, + title_pos, egui::Align2::LEFT_CENTER, - text, - egui::FontId::proportional(16.0), - egui::Color32::WHITE, + title_text, + egui::FontId::proportional(14.0), + egui::Color32::from_gray(220), ); - // Draw pane name in corner - let corner_pos = rect.min + egui::vec2(8.0, 8.0); - ui.painter().text( - corner_pos, - egui::Align2::LEFT_TOP, - format!("[{}]", pane_name), - egui::FontId::monospace(10.0), - egui::Color32::from_gray(150), - ); + // TODO: Add pane-specific header controls here + // For example, Timeline pane would add playback controls + + // Make pane content clickable + let pane_id = ui.id().with(("pane", path)); + let response = ui.interact(content_rect, pane_id, egui::Sense::click()); + + // Handle split preview mode + if let SplitPreviewMode::Active { + is_horizontal, + hovered_pane, + split_percent, + } = split_preview_mode + { + // Check if mouse is over this pane + if let Some(pointer_pos) = ui.input(|i| i.pointer.hover_pos()) { + if rect.contains(pointer_pos) { + // Update hovered pane + *hovered_pane = Some(path.clone()); + + // Calculate split percentage based on mouse position + *split_percent = if *is_horizontal { + ((pointer_pos.x - rect.left()) / rect.width() * 100.0).clamp(10.0, 90.0) + } else { + ((pointer_pos.y - rect.top()) / rect.height() * 100.0).clamp(10.0, 90.0) + }; + + // Render split preview overlay + let grey_overlay = egui::Color32::from_rgba_premultiplied(128, 128, 128, 30); + + if *is_horizontal { + let split_x = rect.left() + (rect.width() * *split_percent / 100.0); + + // First half + let first_rect = egui::Rect::from_min_max( + rect.min, + egui::pos2(split_x, rect.max.y), + ); + ui.painter().rect_filled(first_rect, 0.0, grey_overlay); + + // Second half + let second_rect = egui::Rect::from_min_max( + egui::pos2(split_x, rect.min.y), + rect.max, + ); + ui.painter().rect_filled(second_rect, 0.0, grey_overlay); + + // Divider line + ui.painter().vline( + split_x, + rect.y_range(), + egui::Stroke::new(2.0, egui::Color32::BLACK), + ); + } else { + let split_y = rect.top() + (rect.height() * *split_percent / 100.0); + + // First half + let first_rect = egui::Rect::from_min_max( + rect.min, + egui::pos2(rect.max.x, split_y), + ); + ui.painter().rect_filled(first_rect, 0.0, grey_overlay); + + // Second half + let second_rect = egui::Rect::from_min_max( + egui::pos2(rect.min.x, split_y), + rect.max, + ); + ui.painter().rect_filled(second_rect, 0.0, grey_overlay); + + // Divider line + ui.painter().hline( + rect.x_range(), + split_y, + egui::Stroke::new(2.0, egui::Color32::BLACK), + ); + } + + // If clicked, perform the split + if response.clicked() { + if *is_horizontal { + *layout_action = Some(LayoutAction::SplitHorizontal(path.clone(), *split_percent)); + } else { + *layout_action = Some(LayoutAction::SplitVertical(path.clone(), *split_percent)); + } + // Exit preview mode + *split_preview_mode = SplitPreviewMode::None; + } + } + } + } else if response.clicked() { + *selected_pane = Some(path.clone()); + } + + // Render pane-specific content using trait-based system + if let Some(pane_type) = pane_type { + // Get or create pane instance for this path + // Check if we need a new instance (either doesn't exist or type changed) + let needs_new_instance = pane_instances + .get(path) + .map(|instance| instance.pane_type() != pane_type) + .unwrap_or(true); + + if needs_new_instance { + pane_instances.insert(path.clone(), PaneInstance::new(pane_type)); + } + + // Get the pane instance and render it + if let Some(pane_instance) = pane_instances.get_mut(path) { + // Create shared state + let mut shared = SharedPaneState { + tool_icon_cache, + icon_cache, + selected_tool, + fill_color, + stroke_color, + }; + + // Render pane header (if it has one) + let has_header = pane_instance.render_header(ui, &mut shared); + + // Adjust content rect if header was rendered + let final_content_rect = if has_header { + // Header was drawn by the pane, adjust content area + content_rect + } else { + content_rect + }; + + // Render pane content + pane_instance.render_content(ui, final_content_rect, path, &mut shared); + } + } else { + // Unknown pane type - draw placeholder + let content_text = "Unknown pane type"; + let text_pos = content_rect.center(); + ui.painter().text( + text_pos, + egui::Align2::CENTER_CENTER, + content_text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } +} + +/// Render toolbar with tool buttons +fn render_toolbar( + ui: &mut egui::Ui, + rect: egui::Rect, + tool_icon_cache: &mut ToolIconCache, + selected_tool: &mut Tool, + path: &NodePath, +) { + let button_size = 60.0; // 50% bigger (was 40.0) + let button_padding = 8.0; + let button_spacing = 4.0; + + // Calculate how many columns we can fit + let available_width = rect.width() - (button_padding * 2.0); + let columns = ((available_width + button_spacing) / (button_size + button_spacing)).floor() as usize; + let columns = columns.max(1); // At least 1 column + + let mut x = rect.left() + button_padding; + let mut y = rect.top() + button_padding; + let mut col = 0; + + for tool in Tool::all() { + let button_rect = egui::Rect::from_min_size( + egui::pos2(x, y), + egui::vec2(button_size, button_size), + ); + + // Check if this is the selected tool + let is_selected = *selected_tool == *tool; + + // Button background + let bg_color = if is_selected { + egui::Color32::from_rgb(70, 100, 150) // Highlighted blue + } else { + egui::Color32::from_rgb(50, 50, 50) + }; + ui.painter().rect_filled(button_rect, 4.0, bg_color); + + // Load and render tool icon + if let Some(icon) = tool_icon_cache.get_or_load(*tool, ui.ctx()) { + let icon_rect = button_rect.shrink(8.0); // Padding inside button + ui.painter().image( + icon.id(), + icon_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + + // Make button interactive (include path to ensure unique IDs across panes) + let button_id = ui.id().with(("tool_button", path, *tool as usize)); + let response = ui.interact(button_rect, button_id, egui::Sense::click()); + + // Check for click first + if response.clicked() { + *selected_tool = *tool; + } + + if response.hovered() { + ui.painter().rect_stroke( + button_rect, + 4.0, + egui::Stroke::new(2.0, egui::Color32::from_gray(180)), + ); + } + + // Show tooltip with tool name and shortcut (consumes response) + response.on_hover_text(format!("{} ({})", tool.display_name(), tool.shortcut_hint())); + + // Draw selection border + if is_selected { + ui.painter().rect_stroke( + button_rect, + 4.0, + egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 150, 255)), + ); + } + + // Move to next position in grid + col += 1; + if col >= columns { + // Move to next row + col = 0; + x = rect.left() + button_padding; + y += button_size + button_spacing; + } else { + // Move to next column + x += button_size + button_spacing; + } + } } /// Get a color for each pane type for visualization @@ -190,13 +1249,133 @@ fn pane_color(pane_type: PaneType) -> egui::Color32 { match pane_type { PaneType::Stage => egui::Color32::from_rgb(30, 40, 50), PaneType::Timeline => egui::Color32::from_rgb(40, 30, 50), - PaneType::TimelineV2 => egui::Color32::from_rgb(45, 35, 55), PaneType::Toolbar => egui::Color32::from_rgb(50, 40, 30), PaneType::Infopanel => egui::Color32::from_rgb(30, 50, 40), PaneType::Outliner => egui::Color32::from_rgb(40, 50, 30), - PaneType::Piano => egui::Color32::from_rgb(50, 30, 40), PaneType::PianoRoll => egui::Color32::from_rgb(55, 35, 45), PaneType::NodeEditor => egui::Color32::from_rgb(30, 45, 50), PaneType::PresetBrowser => egui::Color32::from_rgb(50, 45, 30), } } + +/// Split a pane node into a horizontal or vertical grid with two copies of the pane +fn split_node(root: &mut LayoutNode, path: &NodePath, is_horizontal: bool, percent: f32) { + if path.is_empty() { + // Split the root node + if let LayoutNode::Pane { name } = root { + let pane_name = name.clone(); + let new_node = if is_horizontal { + LayoutNode::HorizontalGrid { + percent, + children: [ + Box::new(LayoutNode::Pane { name: pane_name.clone() }), + Box::new(LayoutNode::Pane { name: pane_name }), + ], + } + } else { + LayoutNode::VerticalGrid { + percent, + children: [ + Box::new(LayoutNode::Pane { name: pane_name.clone() }), + Box::new(LayoutNode::Pane { name: pane_name }), + ], + } + }; + *root = new_node; + } + } else { + // Navigate to parent and split the child + navigate_to_node(root, &path[..path.len() - 1], &mut |node| { + let child_index = path[path.len() - 1]; + match node { + LayoutNode::HorizontalGrid { children, .. } + | LayoutNode::VerticalGrid { children, .. } => { + if let LayoutNode::Pane { name } = &*children[child_index] { + let pane_name = name.clone(); + let new_node = if is_horizontal { + LayoutNode::HorizontalGrid { + percent, + children: [ + Box::new(LayoutNode::Pane { name: pane_name.clone() }), + Box::new(LayoutNode::Pane { name: pane_name }), + ], + } + } else { + LayoutNode::VerticalGrid { + percent, + children: [ + Box::new(LayoutNode::Pane { name: pane_name.clone() }), + Box::new(LayoutNode::Pane { name: pane_name }), + ], + } + }; + children[child_index] = Box::new(new_node); + } + } + _ => {} + } + }); + } +} + +/// Remove a split by replacing it with one of its children +/// The path includes the split node path plus which child to keep (0 or 1 as last element) +fn remove_split(root: &mut LayoutNode, path: &NodePath) { + if path.is_empty() { + return; // Can't remove if path is empty + } + + // Last element indicates which child to keep (0 or 1) + let child_to_keep = path[path.len() - 1]; + + // Path to the split node is everything except the last element + let split_path = &path[..path.len() - 1]; + + if split_path.is_empty() { + // Removing root split - replace root with the chosen child + if let LayoutNode::HorizontalGrid { children, .. } + | LayoutNode::VerticalGrid { children, .. } = root + { + *root = (*children[child_to_keep]).clone(); + } + } else { + // Navigate to parent of the split node and replace it + let parent_path = &split_path[..split_path.len() - 1]; + let split_index = split_path[split_path.len() - 1]; + + navigate_to_node(root, parent_path, &mut |node| { + match node { + LayoutNode::HorizontalGrid { children, .. } + | LayoutNode::VerticalGrid { children, .. } => { + // Get the split node's chosen child + if let LayoutNode::HorizontalGrid { children: split_children, .. } + | LayoutNode::VerticalGrid { children: split_children, .. } = + &*children[split_index] + { + // Replace the split node with the chosen child + children[split_index] = split_children[child_to_keep].clone(); + } + } + _ => {} + } + }); + } +} + +/// Navigate to a node at the given path and apply a function to it +fn navigate_to_node(node: &mut LayoutNode, path: &[usize], f: &mut F) +where + F: FnMut(&mut LayoutNode), +{ + if path.is_empty() { + f(node); + } else { + match node { + LayoutNode::HorizontalGrid { children, .. } + | LayoutNode::VerticalGrid { children, .. } => { + navigate_to_node(&mut children[path[0]], &path[1..], f); + } + _ => {} + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/menu.rs b/lightningbeam-ui/lightningbeam-editor/src/menu.rs new file mode 100644 index 0000000..6243680 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/menu.rs @@ -0,0 +1,563 @@ +/// Native menu implementation using muda +/// +/// This module creates the native menu bar with all menu items matching +/// the JavaScript version's menu structure. + +use muda::{ + accelerator::{Accelerator, Code, Modifiers}, + Menu, MenuItem, PredefinedMenuItem, Submenu, +}; + +/// All possible menu actions that can be triggered +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MenuAction { + // File menu + NewFile, + NewWindow, + Save, + SaveAs, + OpenFile, + Revert, + Import, + Export, + Quit, + + // Edit menu + Undo, + Redo, + Cut, + Copy, + Paste, + Delete, + SelectAll, + SelectNone, + Preferences, + + // Modify menu + Group, + SendToBack, + BringToFront, + + // Layer menu + AddLayer, + AddVideoLayer, + AddAudioTrack, + AddMidiTrack, + DeleteLayer, + ToggleLayerVisibility, + + // Timeline menu + NewKeyframe, + NewBlankKeyframe, + DeleteFrame, + DuplicateKeyframe, + AddKeyframeAtPlayhead, + AddMotionTween, + AddShapeTween, + ReturnToStart, + Play, + + // View menu + ZoomIn, + ZoomOut, + ActualSize, + RecenterView, + NextLayout, + PreviousLayout, + SwitchLayout(usize), + + // Help menu + About, + + // Lightningbeam menu (macOS) + Settings, + CloseWindow, +} + +/// Menu system that holds all menu items and can dispatch actions +pub struct MenuSystem { + #[allow(dead_code)] + menu: Menu, + items: Vec<(MenuItem, MenuAction)>, +} + +impl MenuSystem { + /// Create a new menu system with all menus and items + pub fn new() -> Result> { + let menu = Menu::new(); + let mut items = Vec::new(); + + // Platform-specific: Add "Lightningbeam" menu on macOS + #[cfg(target_os = "macos")] + { + let app_menu = Submenu::new("Lightningbeam", true); + + let about_item = MenuItem::new("About Lightningbeam", true, None); + items.push((about_item.clone(), MenuAction::About)); + app_menu.append(&about_item)?; + + app_menu.append(&PredefinedMenuItem::separator())?; + + let settings_item = MenuItem::new( + "Settings", + true, + Some(Accelerator::new(Some(Modifiers::META), Code::Comma)), + ); + items.push((settings_item.clone(), MenuAction::Settings)); + app_menu.append(&settings_item)?; + + app_menu.append(&PredefinedMenuItem::separator())?; + + let close_item = MenuItem::new( + "Close Window", + true, + Some(Accelerator::new(Some(Modifiers::META), Code::KeyW)), + ); + items.push((close_item.clone(), MenuAction::CloseWindow)); + app_menu.append(&close_item)?; + + let quit_item = MenuItem::new( + "Quit Lightningbeam", + true, + Some(Accelerator::new(Some(Modifiers::META), Code::KeyQ)), + ); + items.push((quit_item.clone(), MenuAction::Quit)); + app_menu.append(&quit_item)?; + + menu.append(&app_menu)?; + } + + // File menu + let file_menu = Submenu::new("File", true); + + let new_file = MenuItem::new( + "New file...", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyN)), + ); + items.push((new_file.clone(), MenuAction::NewFile)); + file_menu.append(&new_file)?; + + let new_window = MenuItem::new( + "New Window", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyN, + )), + ); + items.push((new_window.clone(), MenuAction::NewWindow)); + file_menu.append(&new_window)?; + + file_menu.append(&PredefinedMenuItem::separator())?; + + let save = MenuItem::new( + "Save", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyS)), + ); + items.push((save.clone(), MenuAction::Save)); + file_menu.append(&save)?; + + let save_as = MenuItem::new( + "Save As...", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyS, + )), + ); + items.push((save_as.clone(), MenuAction::SaveAs)); + file_menu.append(&save_as)?; + + file_menu.append(&PredefinedMenuItem::separator())?; + + // Open Recent submenu (placeholder for now) + let open_recent = Submenu::new("Open Recent", true); + file_menu.append(&open_recent)?; + + let open_file = MenuItem::new( + "Open File...", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyO)), + ); + items.push((open_file.clone(), MenuAction::OpenFile)); + file_menu.append(&open_file)?; + + let revert = MenuItem::new("Revert", true, None); + items.push((revert.clone(), MenuAction::Revert)); + file_menu.append(&revert)?; + + file_menu.append(&PredefinedMenuItem::separator())?; + + let import = MenuItem::new( + "Import...", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyI, + )), + ); + items.push((import.clone(), MenuAction::Import)); + file_menu.append(&import)?; + + let export = MenuItem::new( + "Export...", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyE, + )), + ); + items.push((export.clone(), MenuAction::Export)); + file_menu.append(&export)?; + + // On non-macOS, add Quit to File menu + #[cfg(not(target_os = "macos"))] + { + file_menu.append(&PredefinedMenuItem::separator())?; + let quit = MenuItem::new( + "Quit", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyQ)), + ); + items.push((quit.clone(), MenuAction::Quit)); + file_menu.append(&quit)?; + } + + menu.append(&file_menu)?; + + // Edit menu + let edit_menu = Submenu::new("Edit", true); + + let undo = MenuItem::new( + "Undo", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyZ)), + ); + items.push((undo.clone(), MenuAction::Undo)); + edit_menu.append(&undo)?; + + let redo = MenuItem::new( + "Redo", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyZ, + )), + ); + items.push((redo.clone(), MenuAction::Redo)); + edit_menu.append(&redo)?; + + edit_menu.append(&PredefinedMenuItem::separator())?; + + let cut = MenuItem::new( + "Cut", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyX)), + ); + items.push((cut.clone(), MenuAction::Cut)); + edit_menu.append(&cut)?; + + let copy = MenuItem::new( + "Copy", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)), + ); + items.push((copy.clone(), MenuAction::Copy)); + edit_menu.append(©)?; + + let paste = MenuItem::new( + "Paste", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyV)), + ); + items.push((paste.clone(), MenuAction::Paste)); + edit_menu.append(&paste)?; + + let delete = MenuItem::new( + "Delete", + true, + Some(Accelerator::new(None, Code::Delete)), + ); + items.push((delete.clone(), MenuAction::Delete)); + edit_menu.append(&delete)?; + + edit_menu.append(&PredefinedMenuItem::separator())?; + + let select_all = MenuItem::new( + "Select All", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyA)), + ); + items.push((select_all.clone(), MenuAction::SelectAll)); + edit_menu.append(&select_all)?; + + let select_none = MenuItem::new( + "Select None", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyA, + )), + ); + items.push((select_none.clone(), MenuAction::SelectNone)); + edit_menu.append(&select_none)?; + + edit_menu.append(&PredefinedMenuItem::separator())?; + + let preferences = MenuItem::new("Preferences", true, None); + items.push((preferences.clone(), MenuAction::Preferences)); + edit_menu.append(&preferences)?; + + menu.append(&edit_menu)?; + + // Modify menu + let modify_menu = Submenu::new("Modify", true); + + let group = MenuItem::new( + "Group", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyG)), + ); + items.push((group.clone(), MenuAction::Group)); + modify_menu.append(&group)?; + + modify_menu.append(&PredefinedMenuItem::separator())?; + + let send_to_back = MenuItem::new("Send to back", true, None); + items.push((send_to_back.clone(), MenuAction::SendToBack)); + modify_menu.append(&send_to_back)?; + + let bring_to_front = MenuItem::new("Bring to front", true, None); + items.push((bring_to_front.clone(), MenuAction::BringToFront)); + modify_menu.append(&bring_to_front)?; + + menu.append(&modify_menu)?; + + // Layer menu + let layer_menu = Submenu::new("Layer", true); + + let add_layer = MenuItem::new( + "Add Layer", + true, + Some(Accelerator::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::KeyL, + )), + ); + items.push((add_layer.clone(), MenuAction::AddLayer)); + layer_menu.append(&add_layer)?; + + let add_video_layer = MenuItem::new("Add Video Layer", true, None); + items.push((add_video_layer.clone(), MenuAction::AddVideoLayer)); + layer_menu.append(&add_video_layer)?; + + let add_audio_track = MenuItem::new("Add Audio Track", true, None); + items.push((add_audio_track.clone(), MenuAction::AddAudioTrack)); + layer_menu.append(&add_audio_track)?; + + let add_midi_track = MenuItem::new("Add MIDI Track", true, None); + items.push((add_midi_track.clone(), MenuAction::AddMidiTrack)); + layer_menu.append(&add_midi_track)?; + + layer_menu.append(&PredefinedMenuItem::separator())?; + + let delete_layer = MenuItem::new("Delete Layer", true, None); + items.push((delete_layer.clone(), MenuAction::DeleteLayer)); + layer_menu.append(&delete_layer)?; + + let toggle_layer = MenuItem::new("Hide/Show Layer", true, None); + items.push((toggle_layer.clone(), MenuAction::ToggleLayerVisibility)); + layer_menu.append(&toggle_layer)?; + + menu.append(&layer_menu)?; + + // Timeline menu + let timeline_menu = Submenu::new("Timeline", true); + + let new_keyframe = MenuItem::new( + "New Keyframe", + true, + Some(Accelerator::new(None, Code::KeyK)), + ); + items.push((new_keyframe.clone(), MenuAction::NewKeyframe)); + timeline_menu.append(&new_keyframe)?; + + let new_blank_keyframe = MenuItem::new("New Blank Keyframe", true, None); + items.push((new_blank_keyframe.clone(), MenuAction::NewBlankKeyframe)); + timeline_menu.append(&new_blank_keyframe)?; + + let delete_frame = MenuItem::new("Delete Frame", true, None); + items.push((delete_frame.clone(), MenuAction::DeleteFrame)); + timeline_menu.append(&delete_frame)?; + + let duplicate_keyframe = MenuItem::new("Duplicate Keyframe", true, None); + items.push((duplicate_keyframe.clone(), MenuAction::DuplicateKeyframe)); + timeline_menu.append(&duplicate_keyframe)?; + + let add_keyframe_playhead = MenuItem::new("Add Keyframe at Playhead", true, None); + items.push((add_keyframe_playhead.clone(), MenuAction::AddKeyframeAtPlayhead)); + timeline_menu.append(&add_keyframe_playhead)?; + + timeline_menu.append(&PredefinedMenuItem::separator())?; + + let motion_tween = MenuItem::new("Add Motion Tween", true, None); + items.push((motion_tween.clone(), MenuAction::AddMotionTween)); + timeline_menu.append(&motion_tween)?; + + let shape_tween = MenuItem::new("Add Shape Tween", true, None); + items.push((shape_tween.clone(), MenuAction::AddShapeTween)); + timeline_menu.append(&shape_tween)?; + + timeline_menu.append(&PredefinedMenuItem::separator())?; + + let return_to_start = MenuItem::new("Return to start", true, None); + items.push((return_to_start.clone(), MenuAction::ReturnToStart)); + timeline_menu.append(&return_to_start)?; + + let play = MenuItem::new("Play", true, None); + items.push((play.clone(), MenuAction::Play)); + timeline_menu.append(&play)?; + + menu.append(&timeline_menu)?; + + // View menu + let view_menu = Submenu::new("View", true); + + let zoom_in = MenuItem::new( + "Zoom In", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Equal)), + ); + items.push((zoom_in.clone(), MenuAction::ZoomIn)); + view_menu.append(&zoom_in)?; + + let zoom_out = MenuItem::new( + "Zoom Out", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Minus)), + ); + items.push((zoom_out.clone(), MenuAction::ZoomOut)); + view_menu.append(&zoom_out)?; + + let actual_size = MenuItem::new( + "Actual Size", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Digit0)), + ); + items.push((actual_size.clone(), MenuAction::ActualSize)); + view_menu.append(&actual_size)?; + + let recenter = MenuItem::new("Recenter View", true, None); + items.push((recenter.clone(), MenuAction::RecenterView)); + view_menu.append(&recenter)?; + + view_menu.append(&PredefinedMenuItem::separator())?; + + // Layout submenu + let layout_submenu = Submenu::new("Layout", true); + + let next_layout = MenuItem::new( + "Next Layout", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::BracketRight)), + ); + items.push((next_layout.clone(), MenuAction::NextLayout)); + layout_submenu.append(&next_layout)?; + + let prev_layout = MenuItem::new( + "Previous Layout", + true, + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::BracketLeft)), + ); + items.push((prev_layout.clone(), MenuAction::PreviousLayout)); + layout_submenu.append(&prev_layout)?; + + // TODO: Add dynamic layout list with checkmarks for current layout + // This will need to be updated when layouts change + + view_menu.append(&layout_submenu)?; + menu.append(&view_menu)?; + + // Help menu + let help_menu = Submenu::new("Help", true); + + let about = MenuItem::new("About...", true, None); + items.push((about.clone(), MenuAction::About)); + help_menu.append(&about)?; + + menu.append(&help_menu)?; + + Ok(Self { menu, items }) + } + + /// Initialize the menu for the application window + #[cfg(target_os = "linux")] + pub fn init_for_gtk(&self, window: >k::ApplicationWindow, container: Option<>k::Box>) -> Result<(), Box> { + self.menu.init_for_gtk_window(window, container)?; + Ok(()) + } + + /// Initialize the menu for macOS (app-wide) + #[cfg(target_os = "macos")] + pub fn init_for_macos(&self) { + self.menu.init_for_nsapp(); + } + + /// Check if any menu item was triggered and return the action + pub fn check_events(&self) -> Option { + for (item, action) in &self.items { + if let Ok(event) = muda::MenuEvent::receiver().try_recv() { + if event.id == item.id() { + return Some(*action); + } + } + } + None + } + + /// Update menu item text dynamically (e.g., for Undo/Redo with action names) + #[allow(dead_code)] + pub fn update_undo_text(&self, action_name: Option<&str>) { + // Find the Undo menu item and update its text + for (item, action) in &self.items { + if *action == MenuAction::Undo { + let text = if let Some(name) = action_name { + format!("Undo {}", name) + } else { + "Undo".to_string() + }; + let _ = item.set_text(text); + break; + } + } + } + + /// Update menu item text dynamically for Redo + #[allow(dead_code)] + pub fn update_redo_text(&self, action_name: Option<&str>) { + for (item, action) in &self.items { + if *action == MenuAction::Redo { + let text = if let Some(name) = action_name { + format!("Redo {}", name) + } else { + "Redo".to_string() + }; + let _ = item.set_text(text); + break; + } + } + } + + /// Enable or disable a menu item + #[allow(dead_code)] + pub fn set_enabled(&self, action: MenuAction, enabled: bool) { + for (item, item_action) in &self.items { + if *item_action == action { + let _ = item.set_enabled(enabled); + break; + } + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs new file mode 100644 index 0000000..28853e3 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -0,0 +1,45 @@ +/// Info Panel pane - displays properties of selected objects +/// +/// This will eventually show editable properties. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct InfopanelPane {} + +impl InfopanelPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for InfopanelPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(30, 50, 40), + ); + + let text = "Info Panel\n(TODO: Implement property editor)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Info Panel" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs new file mode 100644 index 0000000..1baa163 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -0,0 +1,145 @@ +/// Pane implementations for the editor +/// +/// Each pane type has its own module with implementation details. +/// Panes can hold local state and access shared state through SharedPaneState. + +use eframe::egui; +use lightningbeam_core::{pane::PaneType, tool::Tool}; + +// Type alias for node paths (matches main.rs) +pub type NodePath = Vec; + +pub mod toolbar; +pub mod stage; +pub mod timeline; +pub mod infopanel; +pub mod outliner; +pub mod piano_roll; +pub mod node_editor; +pub mod preset_browser; + +/// Shared state that all panes can access +pub struct SharedPaneState<'a> { + pub tool_icon_cache: &'a mut crate::ToolIconCache, + pub icon_cache: &'a mut crate::IconCache, + pub selected_tool: &'a mut Tool, + pub fill_color: &'a mut egui::Color32, + pub stroke_color: &'a mut egui::Color32, +} + +/// Trait for pane rendering +/// +/// Panes implement this trait to provide custom rendering logic. +/// The header is optional and typically used for controls (e.g., Timeline playback). +/// The content area is the main body of the pane. +pub trait PaneRenderer { + /// Render the optional header section with controls + /// + /// Returns true if a header was rendered, false if no header + fn render_header(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool { + false // Default: no header + } + + /// Render the main content area + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + path: &NodePath, + shared: &mut SharedPaneState, + ); + + /// Get the display name of this pane + fn name(&self) -> &str; +} + +/// Enum wrapper for all pane implementations (enum dispatch pattern) +pub enum PaneInstance { + Stage(stage::StagePane), + Timeline(timeline::TimelinePane), + Toolbar(toolbar::ToolbarPane), + Infopanel(infopanel::InfopanelPane), + Outliner(outliner::OutlinerPane), + PianoRoll(piano_roll::PianoRollPane), + NodeEditor(node_editor::NodeEditorPane), + PresetBrowser(preset_browser::PresetBrowserPane), +} + +impl PaneInstance { + /// Create a new pane instance for the given type + pub fn new(pane_type: PaneType) -> Self { + match pane_type { + PaneType::Stage => PaneInstance::Stage(stage::StagePane::new()), + PaneType::Timeline => PaneInstance::Timeline(timeline::TimelinePane::new()), + PaneType::Toolbar => PaneInstance::Toolbar(toolbar::ToolbarPane::new()), + PaneType::Infopanel => PaneInstance::Infopanel(infopanel::InfopanelPane::new()), + PaneType::Outliner => PaneInstance::Outliner(outliner::OutlinerPane::new()), + PaneType::PianoRoll => PaneInstance::PianoRoll(piano_roll::PianoRollPane::new()), + PaneType::NodeEditor => PaneInstance::NodeEditor(node_editor::NodeEditorPane::new()), + PaneType::PresetBrowser => { + PaneInstance::PresetBrowser(preset_browser::PresetBrowserPane::new()) + } + } + } + + /// Get the pane type of this instance + pub fn pane_type(&self) -> PaneType { + match self { + PaneInstance::Stage(_) => PaneType::Stage, + PaneInstance::Timeline(_) => PaneType::Timeline, + PaneInstance::Toolbar(_) => PaneType::Toolbar, + PaneInstance::Infopanel(_) => PaneType::Infopanel, + PaneInstance::Outliner(_) => PaneType::Outliner, + PaneInstance::PianoRoll(_) => PaneType::PianoRoll, + PaneInstance::NodeEditor(_) => PaneType::NodeEditor, + PaneInstance::PresetBrowser(_) => PaneType::PresetBrowser, + } + } +} + +impl PaneRenderer for PaneInstance { + fn render_header(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool { + match self { + PaneInstance::Stage(p) => p.render_header(ui, shared), + PaneInstance::Timeline(p) => p.render_header(ui, shared), + PaneInstance::Toolbar(p) => p.render_header(ui, shared), + PaneInstance::Infopanel(p) => p.render_header(ui, shared), + PaneInstance::Outliner(p) => p.render_header(ui, shared), + PaneInstance::PianoRoll(p) => p.render_header(ui, shared), + PaneInstance::NodeEditor(p) => p.render_header(ui, shared), + PaneInstance::PresetBrowser(p) => p.render_header(ui, shared), + } + } + + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + path: &NodePath, + shared: &mut SharedPaneState, + ) { + match self { + PaneInstance::Stage(p) => p.render_content(ui, rect, path, shared), + PaneInstance::Timeline(p) => p.render_content(ui, rect, path, shared), + PaneInstance::Toolbar(p) => p.render_content(ui, rect, path, shared), + PaneInstance::Infopanel(p) => p.render_content(ui, rect, path, shared), + PaneInstance::Outliner(p) => p.render_content(ui, rect, path, shared), + PaneInstance::PianoRoll(p) => p.render_content(ui, rect, path, shared), + PaneInstance::NodeEditor(p) => p.render_content(ui, rect, path, shared), + PaneInstance::PresetBrowser(p) => p.render_content(ui, rect, path, shared), + } + } + + fn name(&self) -> &str { + match self { + PaneInstance::Stage(p) => p.name(), + PaneInstance::Timeline(p) => p.name(), + PaneInstance::Toolbar(p) => p.name(), + PaneInstance::Infopanel(p) => p.name(), + PaneInstance::Outliner(p) => p.name(), + PaneInstance::PianoRoll(p) => p.name(), + PaneInstance::NodeEditor(p) => p.name(), + PaneInstance::PresetBrowser(p) => p.name(), + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_editor.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_editor.rs new file mode 100644 index 0000000..f0defcf --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_editor.rs @@ -0,0 +1,45 @@ +/// Node Editor pane - node-based visual programming +/// +/// This will eventually render a node graph with Vello. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct NodeEditorPane {} + +impl NodeEditorPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for NodeEditorPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(30, 45, 50), + ); + + let text = "Node Editor\n(TODO: Implement node graph)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Node Editor" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/outliner.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/outliner.rs new file mode 100644 index 0000000..c901fbc --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/outliner.rs @@ -0,0 +1,47 @@ +/// Outliner pane - layer hierarchy view +/// +/// This will eventually show a tree view of layers. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct OutlinerPane { + // TODO: Add tree expansion state +} + +impl OutlinerPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for OutlinerPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(40, 50, 30), + ); + + let text = "Outliner\n(TODO: Implement layer tree)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Outliner" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs new file mode 100644 index 0000000..2cdd1b5 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs @@ -0,0 +1,45 @@ +/// Piano Roll pane - MIDI editor +/// +/// This will eventually render a piano roll with Vello. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct PianoRollPane {} + +impl PianoRollPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for PianoRollPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(55, 35, 45), + ); + + let text = "Piano Roll\n(TODO: Implement MIDI editor)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Piano Roll" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/preset_browser.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/preset_browser.rs new file mode 100644 index 0000000..d68fe55 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/preset_browser.rs @@ -0,0 +1,45 @@ +/// Preset Browser pane - asset and preset library +/// +/// This will eventually show a file browser for presets. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct PresetBrowserPane {} + +impl PresetBrowserPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for PresetBrowserPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(50, 45, 30), + ); + + let text = "Preset Browser\n(TODO: Implement file browser)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Preset Browser" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs new file mode 100644 index 0000000..5bd88cb --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -0,0 +1,47 @@ +/// Stage pane - main animation canvas +/// +/// This will eventually render the composited layers using Vello. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct StagePane { + // TODO: Add state for camera, selection, etc. +} + +impl StagePane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for StagePane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(30, 40, 50), + ); + + let text = "Stage Pane\n(TODO: Implement Vello rendering)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Stage" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs new file mode 100644 index 0000000..7587867 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -0,0 +1,57 @@ +/// Timeline pane - frame-based animation timeline +/// +/// This will eventually render keyframes, layers, and playback controls. +/// For now, it's a placeholder. + +use eframe::egui; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +pub struct TimelinePane { + // TODO: Add state for zoom, scroll, playback, etc. +} + +impl TimelinePane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for TimelinePane { + fn render_header(&mut self, ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool { + // TODO: Add playback controls (play/pause, frame counter, zoom) + ui.horizontal(|ui| { + ui.label("⏯"); + ui.label("Frame: 0"); + ui.label("FPS: 24"); + }); + true // Header was rendered + } + + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + _path: &NodePath, + _shared: &mut SharedPaneState, + ) { + // Placeholder rendering + ui.painter().rect_filled( + rect, + 0.0, + egui::Color32::from_rgb(40, 30, 50), + ); + + let text = "Timeline Pane\n(TODO: Implement frame scrubbing)"; + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(16.0), + egui::Color32::from_gray(150), + ); + } + + fn name(&self) -> &str { + "Timeline" + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs new file mode 100644 index 0000000..c426795 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs @@ -0,0 +1,235 @@ +/// Toolbar pane - displays drawing tool buttons +/// +/// The toolbar shows all available drawing tools in a responsive grid layout. +/// Users can click to select tools, which updates the global selected_tool state. + +use eframe::egui; +use lightningbeam_core::tool::Tool; +use super::{NodePath, PaneRenderer, SharedPaneState}; + +/// Toolbar pane state +pub struct ToolbarPane { + // No local state needed for toolbar +} + +impl ToolbarPane { + pub fn new() -> Self { + Self {} + } +} + +impl PaneRenderer for ToolbarPane { + fn render_content( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + path: &NodePath, + shared: &mut SharedPaneState, + ) { + let button_size = 60.0; + let button_padding = 8.0; + let button_spacing = 4.0; + + // Calculate how many columns we can fit + let available_width = rect.width() - (button_padding * 2.0); + let columns = + ((available_width + button_spacing) / (button_size + button_spacing)).floor() as usize; + let columns = columns.max(1); // At least 1 column + + // Calculate total number of tools and rows + let tools = Tool::all(); + let total_tools = tools.len(); + let total_rows = (total_tools + columns - 1) / columns; + + let mut y = rect.top() + button_padding; + + // Process tools row by row for centered layout + for row in 0..total_rows { + let start_idx = row * columns; + let end_idx = (start_idx + columns).min(total_tools); + let buttons_in_row = end_idx - start_idx; + + // Calculate the total width of buttons in this row + let row_width = (buttons_in_row as f32 * button_size) + + ((buttons_in_row.saturating_sub(1)) as f32 * button_spacing); + + // Center the row + let mut x = rect.left() + (rect.width() - row_width) / 2.0; + + for tool_idx in start_idx..end_idx { + let tool = &tools[tool_idx]; + let button_rect = + egui::Rect::from_min_size(egui::pos2(x, y), egui::vec2(button_size, button_size)); + + // Check if this is the selected tool + let is_selected = *shared.selected_tool == *tool; + + // Button background + let bg_color = if is_selected { + egui::Color32::from_rgb(70, 100, 150) // Highlighted blue + } else { + egui::Color32::from_rgb(50, 50, 50) + }; + ui.painter().rect_filled(button_rect, 4.0, bg_color); + + // Load and render tool icon + if let Some(icon) = shared.tool_icon_cache.get_or_load(*tool, ui.ctx()) { + let icon_rect = button_rect.shrink(8.0); // Padding inside button + ui.painter().image( + icon.id(), + icon_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + + // Make button interactive (include path to ensure unique IDs across panes) + let button_id = ui.id().with(("tool_button", path, *tool as usize)); + let response = ui.interact(button_rect, button_id, egui::Sense::click()); + + // Check for click first + if response.clicked() { + *shared.selected_tool = *tool; + } + + if response.hovered() { + ui.painter().rect_stroke( + button_rect, + 4.0, + egui::Stroke::new(2.0, egui::Color32::from_gray(180)), + ); + } + + // Show tooltip with tool name and shortcut (consumes response) + response.on_hover_text(format!("{} ({})", tool.display_name(), tool.shortcut_hint())); + + // Draw selection border + if is_selected { + ui.painter().rect_stroke( + button_rect, + 4.0, + egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 150, 255)), + ); + } + + // Move to next column in this row + x += button_size + button_spacing; + } + + // Move to next row + y += button_size + button_spacing; + } + + // Add color pickers below the tool buttons + y += button_spacing * 2.0; // Extra spacing + + // Fill Color + let fill_label_width = 40.0; + let color_button_size = 50.0; + let color_row_width = fill_label_width + color_button_size + button_spacing; + let color_x = rect.left() + (rect.width() - color_row_width) / 2.0; + + // Fill color label + ui.painter().text( + egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), + egui::Align2::CENTER_CENTER, + "Fill", + egui::FontId::proportional(14.0), + egui::Color32::from_gray(200), + ); + + // Fill color button + let fill_button_rect = egui::Rect::from_min_size( + egui::pos2(color_x + fill_label_width + button_spacing, y), + egui::vec2(color_button_size, color_button_size), + ); + let fill_button_id = ui.id().with(("fill_color_button", path)); + let fill_response = ui.interact(fill_button_rect, fill_button_id, egui::Sense::click()); + + // Draw fill color button with checkerboard for alpha + draw_color_button(ui, fill_button_rect, *shared.fill_color); + + if fill_response.clicked() { + // Open color picker popup + ui.memory_mut(|mem| mem.toggle_popup(fill_button_id)); + } + + // Show fill color picker popup + egui::popup::popup_below_widget(ui, fill_button_id, &fill_response, egui::popup::PopupCloseBehavior::CloseOnClickOutside, |ui: &mut egui::Ui| { + egui::color_picker::color_picker_color32(ui, shared.fill_color, egui::color_picker::Alpha::OnlyBlend); + }); + + y += color_button_size + button_spacing; + + // Stroke color label + ui.painter().text( + egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), + egui::Align2::CENTER_CENTER, + "Stroke", + egui::FontId::proportional(14.0), + egui::Color32::from_gray(200), + ); + + // Stroke color button + let stroke_button_rect = egui::Rect::from_min_size( + egui::pos2(color_x + fill_label_width + button_spacing, y), + egui::vec2(color_button_size, color_button_size), + ); + let stroke_button_id = ui.id().with(("stroke_color_button", path)); + let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click()); + + // Draw stroke color button with checkerboard for alpha + draw_color_button(ui, stroke_button_rect, *shared.stroke_color); + + if stroke_response.clicked() { + // Open color picker popup + ui.memory_mut(|mem| mem.toggle_popup(stroke_button_id)); + } + + // Show stroke color picker popup + egui::popup::popup_below_widget(ui, stroke_button_id, &stroke_response, egui::popup::PopupCloseBehavior::CloseOnClickOutside, |ui: &mut egui::Ui| { + egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend); + }); + } + + fn name(&self) -> &str { + "Toolbar" + } +} + +/// Draw a color button with checkerboard background for alpha channel +fn draw_color_button(ui: &mut egui::Ui, rect: egui::Rect, color: egui::Color32) { + // Draw checkerboard background + let checker_size = 5.0; + let cols = (rect.width() / checker_size).ceil() as usize; + let rows = (rect.height() / checker_size).ceil() as usize; + + for row in 0..rows { + for col in 0..cols { + let is_light = (row + col) % 2 == 0; + let checker_color = if is_light { + egui::Color32::from_gray(180) + } else { + egui::Color32::from_gray(120) + }; + let checker_rect = egui::Rect::from_min_size( + egui::pos2( + rect.min.x + col as f32 * checker_size, + rect.min.y + row as f32 * checker_size, + ), + egui::vec2(checker_size, checker_size), + ).intersect(rect); + ui.painter().rect_filled(checker_rect, 0.0, checker_color); + } + } + + // Draw color on top + ui.painter().rect_filled(rect, 2.0, color); + + // Draw border + ui.painter().rect_stroke( + rect, + 2.0, + egui::Stroke::new(1.0, egui::Color32::from_gray(80)), + ); +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 204e35a..3edcb4b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "addr2line" version = "0.24.2" @@ -17,6 +33,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy 0.8.27", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -81,6 +110,33 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android-activity" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +dependencies = [ + "android-properties", + "bitflags 2.8.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk 0.8.0", + "ndk-context", + "ndk-sys 0.5.0+25.2.9519653", + "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-tzdata" version = "0.1.1" @@ -152,12 +208,33 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "ashpd" version = "0.10.2" @@ -175,7 +252,7 @@ dependencies = [ "url", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.5", "zbus", ] @@ -236,6 +313,12 @@ dependencies = [ "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" @@ -305,6 +388,27 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -335,13 +439,32 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" +dependencies = [ + "block-sys", + "objc2 0.4.1", +] + [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -376,6 +499,20 @@ name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] [[package]] name = "byteorder" @@ -417,6 +554,32 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.8.0", + "log", + "polling", + "rustix 0.38.43", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix 0.38.43", + "wayland-backend", + "wayland-client", +] + [[package]] name = "camino" version = "1.1.9" @@ -527,6 +690,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -567,7 +736,7 @@ dependencies = [ "block", "cocoa-foundation", "core-foundation 0.10.0", - "core-graphics", + "core-graphics 0.24.0", "foreign-types", "libc", "objc", @@ -582,17 +751,64 @@ dependencies = [ "bitflags 2.8.0", "block", "core-foundation 0.10.0", - "core-graphics-types", + "core-graphics-types 0.2.0", "libc", "objc", ] +[[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 = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "combine" version = "4.6.7" @@ -667,6 +883,19 @@ 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 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -675,11 +904,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.8.0", "core-foundation 0.10.0", - "core-graphics-types", + "core-graphics-types 0.2.0", "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 = "core-graphics-types" version = "0.2.0" @@ -831,6 +1071,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -878,6 +1124,23 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.8.0", + "libloading 0.8.6", + "winapi", +] + [[package]] name = "darling" version = "0.20.10" @@ -1337,6 +1600,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "extended" version = "0.1.0" @@ -1663,6 +1941,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1685,6 +1973,28 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1723,6 +2033,17 @@ dependencies = [ "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" version = "0.18.5" @@ -1776,6 +2097,27 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1787,6 +2129,58 @@ dependencies = [ "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.8.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.8.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.8.0", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "gtk" version = "0.18.2" @@ -1839,12 +2233,33 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy 0.8.27", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1856,6 +2271,21 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.8.0", + "com", + "libc", + "libloading 0.8.6", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "heck" version = "0.4.1" @@ -1880,6 +2310,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "html5ever" version = "0.26.0" @@ -2022,6 +2458,17 @@ dependencies = [ "png", ] +[[package]] +name = "icrate" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" +dependencies = [ + "block2 0.3.0", + "dispatch", + "objc2 0.4.1", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2167,6 +2614,24 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2337,6 +2802,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -2380,6 +2854,23 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.6", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kuchikiki" version = "0.8.2" @@ -2399,6 +2890,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2425,9 +2922,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -2457,18 +2954,23 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", + "redox_syscall 0.5.8", ] [[package]] name = "lightningbeam" version = "0.1.0" dependencies = [ + "bytemuck", "cpal", "daw-backend", "env_logger", "ffmpeg-next", + "image", "log", "lru", + "pollster", + "raw-window-handle", "rtrb", "serde", "serde_json", @@ -2478,6 +2980,8 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-shell", "tungstenite", + "wgpu", + "winit", ] [[package]] @@ -2486,6 +2990,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.7.4" @@ -2567,6 +3077,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2576,6 +3095,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +dependencies = [ + "bitflags 2.8.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "midir" version = "0.9.1" @@ -2656,7 +3190,7 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "once_cell", @@ -2666,6 +3200,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +dependencies = [ + "bit-set", + "bitflags 2.8.0", + "codespan-reporting", + "hexf-parse", + "indexmap 2.7.0", + "log", + "num-traits", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + [[package]] name = "ndk" version = "0.8.0" @@ -2677,6 +3231,7 @@ dependencies = [ "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum", + "raw-window-handle", "thiserror 1.0.69", ] @@ -2744,7 +3299,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "memoffset", ] @@ -2829,6 +3384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -2840,6 +3396,16 @@ dependencies = [ "cc", ] +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode 3.0.0", +] + [[package]] name = "objc2" version = "0.5.2" @@ -2847,7 +3413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", - "objc2-encode", + "objc2-encode 4.0.3", ] [[package]] @@ -2857,9 +3423,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", "objc2-foundation", @@ -2873,8 +3439,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", "objc2-foundation", ] @@ -2885,8 +3451,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -2897,8 +3463,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -2908,8 +3474,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", "objc2-metal", ] @@ -2920,12 +3486,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-contacts", "objc2-foundation", ] +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + [[package]] name = "objc2-encode" version = "4.0.3" @@ -2939,10 +3511,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] @@ -2951,8 +3523,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", ] @@ -2964,8 +3536,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -2976,8 +3548,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", "objc2-metal", ] @@ -2988,7 +3560,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", + "objc2 0.5.2", "objc2-foundation", ] @@ -2999,8 +3571,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", @@ -3019,8 +3591,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -3031,8 +3603,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", "objc2-foundation", ] @@ -3044,12 +3616,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "object" version = "0.36.7" @@ -3112,6 +3693,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +dependencies = [ + "libredox", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3132,6 +3722,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pango" version = "0.18.3" @@ -3181,7 +3780,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -3402,6 +4001,26 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "portable-atomic" version = "1.11.1" @@ -3429,7 +4048,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3438,6 +4057,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3505,6 +4130,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -3532,6 +4172,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -3613,6 +4259,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + [[package]] name = "ratatui" version = "0.26.3" @@ -3659,6 +4311,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -3708,6 +4369,12 @@ 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 = "reqwest" version = "0.12.12" @@ -3753,7 +4420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", - "block2", + "block2 0.5.1", "core-foundation 0.10.0", "core-foundation-sys", "glib-sys", @@ -3761,7 +4428,7 @@ dependencies = [ "gtk-sys", "js-sys", "log", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "raw-window-handle", @@ -3813,10 +4480,23 @@ dependencies = [ "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -3877,6 +4557,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b31447ca297092c5a9916fc3b955203157b37c19ca8edde4f52e9843e602c7" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "selectors" version = "0.22.0" @@ -4159,12 +4852,55 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.8.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.43", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.8" @@ -4182,16 +4918,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", + "cfg_aliases 0.2.1", + "core-graphics 0.24.0", "foreign-types", "js-sys", "log", - "objc2", + "objc2 0.5.2", "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.8", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -4223,6 +4959,15 @@ dependencies = [ "system-deps", ] +[[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.8.0", +] + [[package]] name = "stability" version = "0.2.1" @@ -4245,6 +4990,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "string_cache" version = "0.8.7" @@ -4569,7 +5320,7 @@ dependencies = [ "bitflags 2.8.0", "cocoa", "core-foundation 0.10.0", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -4638,7 +5389,7 @@ dependencies = [ "log", "mime", "muda", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "percent-encoding", @@ -4837,7 +5588,7 @@ dependencies = [ "http 1.2.0", "jni", "log", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "percent-encoding", @@ -4910,7 +5661,7 @@ dependencies = [ "fastrand", "getrandom 0.2.15", "once_cell", - "rustix", + "rustix 0.38.43", "windows-sys 0.59.0", ] @@ -4925,6 +5676,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -4971,6 +5731,17 @@ dependencies = [ "syn 2.0.109", ] +[[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 = "time" version = "0.3.37" @@ -5002,6 +5773,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5176,12 +5972,12 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" dependencies = [ - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "once_cell", @@ -5197,6 +5993,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "tungstenite" version = "0.20.1" @@ -5309,6 +6111,12 @@ 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" @@ -5436,6 +6244,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5528,7 +6345,7 @@ checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.43", "scoped-tls", "smallvec", "wayland-sys", @@ -5541,11 +6358,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.8.0", - "rustix", + "rustix 0.38.43", "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.8.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +dependencies = [ + "rustix 0.38.43", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols" version = "0.32.5" @@ -5558,6 +6409,32 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + [[package]] name = "wayland-scanner" version = "0.31.5" @@ -5577,6 +6454,7 @@ checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", + "once_cell", "pkg-config", ] @@ -5590,6 +6468,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -5670,6 +6558,125 @@ dependencies = [ "windows-core 0.58.0", ] +[[package]] +name = "weezl" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" + +[[package]] +name = "wgpu" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.8.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "indexmap 2.7.0", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.8.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types 0.1.3", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.6", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +dependencies = [ + "bitflags 2.8.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -5707,7 +6714,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ea403deff7b51fff19e261330f71608ff2cdef5721d72b64180bb95be7c4150" dependencies = [ - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "raw-window-handle", @@ -5730,6 +6737,16 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.54.0" @@ -5894,6 +6911,15 @@ dependencies = [ "windows-targets 0.53.5", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6146,6 +7172,54 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winit" +version = "0.29.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.8.0", + "bytemuck", + "calloop", + "cfg_aliases 0.1.1", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "icrate", + "js-sys", + "libc", + "log", + "memmap2", + "ndk 0.8.0", + "ndk-sys 0.5.0+25.2.9519653", + "objc2 0.4.1", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle", + "redox_syscall 0.3.5", + "rustix 0.38.43", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.48.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -6174,6 +7248,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "write16" version = "1.0.0" @@ -6193,7 +7273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2e33c08b174442ff80d5c791020696f9f8b4e4a87b8cfc7494aad6167ec44e1" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.5.1", "cookie", "crossbeam-channel", "dpi", @@ -6207,7 +7287,7 @@ dependencies = [ "kuchikiki", "libc", "ndk 0.9.0", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "objc2-ui-kit", @@ -6250,6 +7330,33 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.6", + "once_cell", + "rustix 1.1.2", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xdg-home" version = "1.3.0" @@ -6260,6 +7367,31 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.8.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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "yoke" version = "0.7.5" @@ -6348,7 +7480,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive 0.8.27", ] [[package]] @@ -6362,6 +7503,17 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "zerofrom" version = "0.1.5" @@ -6405,6 +7557,15 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "5.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b1e818a..543d1d9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,14 @@ lru = "0.12" # WebSocket for frame streaming (disable default features to remove tracing, but keep handshake) tungstenite = { version = "0.20", default-features = false, features = ["handshake"] } +# Native rendering with wgpu +wgpu = "0.19" +winit = "0.29" +pollster = "0.3" +bytemuck = { version = "1.14", features = ["derive"] } +raw-window-handle = "0.6" +image = "0.24" + [profile.dev] opt-level = 1 # Enable basic optimizations in debug mode for audio decoding performance diff --git a/src-tauri/src/frame_ws_async.rs b/src-tauri/src/frame_ws_async.rs new file mode 100644 index 0000000..577528a --- /dev/null +++ b/src-tauri/src/frame_ws_async.rs @@ -0,0 +1,230 @@ +use axum::{ + extract::{ + ws::{Message, WebSocket, WebSocketUpgrade}, + State, + }, + response::IntoResponse, + routing::get, + Router, +}; +use flume::{Sender, unbounded}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use std::time::Duration; + +/// Playback state for a video pool +#[derive(Clone, Debug)] +pub struct PlaybackState { + pub is_playing: bool, + pub target_fps: f64, + pub current_time: f64, +} + +/// Shared server state +pub struct ServerState { + /// Connected clients + clients: RwLock>>>, + /// Playback state per pool index + playback_state: RwLock>, +} + +impl ServerState { + pub fn new() -> Self { + Self { + clients: RwLock::new(Vec::new()), + playback_state: RwLock::new(HashMap::new()), + } + } + + /// Register a new client + pub async fn add_client(&self, sender: Sender>) { + let mut clients = self.clients.write().await; + clients.push(sender); + eprintln!("[Async Frame Streamer] Client registered, total: {}", clients.len()); + } + + /// Broadcast a frame to all connected clients + pub async fn broadcast_frame(&self, frame_data: Vec) { + let clients = self.clients.read().await; + + // Send to all clients + for client in clients.iter() { + // Non-blocking send, drop frame if client is slow + let _ = client.try_send(frame_data.clone()); + } + } + + /// Remove disconnected clients + pub async fn cleanup_clients(&self) { + let mut clients = self.clients.write().await; + clients.retain(|client| !client.is_disconnected()); + eprintln!("[Async Frame Streamer] Cleaned up clients, remaining: {}", clients.len()); + } + + /// Update playback state for a pool + pub async fn set_playback_state(&self, pool_index: usize, state: PlaybackState) { + let mut states = self.playback_state.write().await; + states.insert(pool_index, state); + } + + /// Get playback state for a pool + pub async fn get_playback_state(&self, pool_index: usize) -> Option { + let states = self.playback_state.read().await; + states.get(&pool_index).cloned() + } +} + +pub struct AsyncFrameStreamer { + port: u16, + state: Arc, + shutdown_tx: Option>, +} + +impl AsyncFrameStreamer { + pub async fn new() -> Result { + let state = Arc::new(ServerState::new()); + + // Create router with WebSocket upgrade handler + let app_state = state.clone(); + let app = Router::new() + .route("/ws", get(ws_handler)) + .with_state(app_state); + + // Bind to localhost on a random port + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| format!("Failed to bind: {}", e))?; + + let port = listener + .local_addr() + .map_err(|e| format!("Failed to get address: {}", e))? + .port(); + + eprintln!("[Async Frame Streamer] WebSocket server starting on port {}", port); + + // Spawn server task + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + + tokio::spawn(async move { + axum::serve(listener, app) + .with_graceful_shutdown(async { + shutdown_rx.await.ok(); + }) + .await + .expect("Server error"); + }); + + eprintln!("[Async Frame Streamer] Server started"); + + Ok(Self { + port, + state, + shutdown_tx: Some(shutdown_tx), + }) + } + + pub fn port(&self) -> u16 { + self.port + } + + /// Send a frame to all connected clients for a specific pool + /// Frame format: [pool_index: u32][timestamp_ms: u32][width: u32][height: u32][rgba_data...] + pub async fn send_frame(&self, pool_index: usize, timestamp: f64, width: u32, height: u32, rgba_data: &[u8]) { + // Build frame message + let mut frame_msg = Vec::with_capacity(16 + rgba_data.len()); + frame_msg.extend_from_slice(&(pool_index as u32).to_le_bytes()); + frame_msg.extend_from_slice(&((timestamp * 1000.0) as u32).to_le_bytes()); + frame_msg.extend_from_slice(&width.to_le_bytes()); + frame_msg.extend_from_slice(&height.to_le_bytes()); + frame_msg.extend_from_slice(rgba_data); + + // Broadcast to all connected clients + self.state.broadcast_frame(frame_msg).await; + } + + /// Start streaming frames for a pool at a target FPS + pub async fn start_stream(&self, pool_index: usize, fps: f64) { + let state = PlaybackState { + is_playing: true, + target_fps: fps, + current_time: 0.0, + }; + self.state.set_playback_state(pool_index, state).await; + eprintln!("[Async Frame Streamer] Started streaming pool {} at {} FPS", pool_index, fps); + } + + /// Stop streaming frames for a pool + pub async fn stop_stream(&self, pool_index: usize) { + if let Some(mut state) = self.state.get_playback_state(pool_index).await { + state.is_playing = false; + self.state.set_playback_state(pool_index, state).await; + eprintln!("[Async Frame Streamer] Stopped streaming pool {}", pool_index); + } + } + + /// Seek to a specific time in a pool + pub async fn seek(&self, pool_index: usize, timestamp: f64) { + if let Some(mut state) = self.state.get_playback_state(pool_index).await { + state.current_time = timestamp; + self.state.set_playback_state(pool_index, state).await; + } + } +} + +impl Drop for AsyncFrameStreamer { + fn drop(&mut self) { + if let Some(tx) = self.shutdown_tx.take() { + let _ = tx.send(()); + } + } +} + +/// WebSocket handler +async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +/// Handle individual WebSocket connection +async fn handle_socket(mut socket: WebSocket, state: Arc) { + eprintln!("[Async Frame Streamer] New WebSocket connection"); + + // Create a channel for this client + let (tx, rx) = unbounded::>(); + + // Register this client + state.add_client(tx).await; + + // Spawn task to send frames to this client + let mut rx = rx; + let mut send_task = tokio::spawn(async move { + while let Ok(frame) = rx.recv_async().await { + if socket.send(Message::Binary(frame)).await.is_err() { + eprintln!("[Async Frame Streamer] Failed to send frame to client"); + break; + } + } + eprintln!("[Async Frame Streamer] Send task ended"); + }); + + // Keep connection alive with ping/pong + let mut interval = tokio::time::interval(Duration::from_secs(30)); + + loop { + tokio::select! { + _ = interval.tick() => { + // Connection alive, no need to ping in this simple implementation + } + _ = &mut send_task => { + eprintln!("[Async Frame Streamer] Send task completed, closing connection"); + break; + } + } + } + + // Cleanup + state.cleanup_clients().await; +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 52f0cd9..2c53548 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,8 @@ use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder}; mod audio; mod video; mod frame_streamer; +mod renderer; +mod render_window; #[derive(Default)] @@ -13,6 +15,12 @@ struct AppState { counter: u32, } +struct RenderWindowState { + handle: Option, + canvas_offset: (i32, i32), // Canvas position relative to window + canvas_size: (u32, u32), +} + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { @@ -48,6 +56,165 @@ fn get_frame_streamer_port( streamer.port() } +// Render window commands +#[tauri::command] +fn render_window_create( + x: i32, + y: i32, + width: u32, + height: u32, + canvas_offset_x: i32, + canvas_offset_y: i32, + app: tauri::AppHandle, + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let mut render_state = state.lock().unwrap(); + + if render_state.handle.is_some() { + return Err("Render window already exists".to_string()); + } + + let handle = render_window::spawn_render_window(x, y, width, height)?; + render_state.handle = Some(handle); + render_state.canvas_offset = (canvas_offset_x, canvas_offset_y); + render_state.canvas_size = (width, height); + + // Start a background thread to poll main window position + let state_clone = state.inner().clone(); + let app_clone = app.clone(); + std::thread::spawn(move || { + let mut last_pos: Option<(i32, i32)> = None; + + loop { + std::thread::sleep(std::time::Duration::from_millis(50)); + + if let Some(main_window) = app_clone.get_webview_window("main") { + if let Ok(pos) = main_window.outer_position() { + let current_pos = (pos.x, pos.y); + + // Only update if position actually changed + if last_pos != Some(current_pos) { + eprintln!("[WindowSync] Main window position: {:?}", current_pos); + + let render_state = state_clone.lock().unwrap(); + if let Some(handle) = &render_state.handle { + let new_x = pos.x + render_state.canvas_offset.0; + let new_y = pos.y + render_state.canvas_offset.1; + handle.set_position(new_x, new_y); + last_pos = Some(current_pos); + } else { + break; // No handle, exit thread + } + } + } else { + break; // Window closed, exit thread + } + } else { + break; // Main window gone, exit thread + } + } + }); + + Ok(()) +} + +#[tauri::command] +fn render_window_update_gradient( + top_r: f32, + top_g: f32, + top_b: f32, + top_a: f32, + bottom_r: f32, + bottom_g: f32, + bottom_b: f32, + bottom_a: f32, + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let render_state = state.lock().unwrap(); + + if let Some(handle) = &render_state.handle { + handle.update_gradient( + [top_r, top_g, top_b, top_a], + [bottom_r, bottom_g, bottom_b, bottom_a], + ); + Ok(()) + } else { + Err("Render window not created".to_string()) + } +} + +#[tauri::command] +fn render_window_set_position( + x: i32, + y: i32, + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let render_state = state.lock().unwrap(); + + if let Some(handle) = &render_state.handle { + handle.set_position(x, y); + Ok(()) + } else { + Err("Render window not created".to_string()) + } +} + +#[tauri::command] +fn render_window_sync_position( + app: tauri::AppHandle, + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let render_state = state.lock().unwrap(); + + if let Some(main_window) = app.get_webview_window("main") { + if let Ok(pos) = main_window.outer_position() { + if let Some(handle) = &render_state.handle { + let new_x = pos.x + render_state.canvas_offset.0; + let new_y = pos.y + render_state.canvas_offset.1; + eprintln!("[Manual Sync] Updating to ({}, {})", new_x, new_y); + handle.set_position(new_x, new_y); + Ok(()) + } else { + Err("Render window not created".to_string()) + } + } else { + Err("Could not get window position".to_string()) + } + } else { + Err("Main window not found".to_string()) + } +} + +#[tauri::command] +fn render_window_set_size( + width: u32, + height: u32, + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let render_state = state.lock().unwrap(); + + if let Some(handle) = &render_state.handle { + handle.set_size(width, height); + Ok(()) + } else { + Err("Render window not created".to_string()) + } +} + +#[tauri::command] +fn render_window_close( + state: tauri::State<'_, Arc>>, +) -> Result<(), String> { + let mut render_state = state.lock().unwrap(); + + if let Some(handle) = render_state.handle.take() { + handle.close(); + Ok(()) + } else { + Err("Render window not created".to_string()) + } +} + use tauri::PhysicalSize; #[tauri::command] @@ -148,6 +315,11 @@ pub fn run() { .manage(Arc::new(Mutex::new(audio::AudioState::default()))) .manage(Arc::new(Mutex::new(video::VideoState::default()))) .manage(Arc::new(Mutex::new(frame_streamer))) + .manage(Arc::new(Mutex::new(RenderWindowState { + handle: None, + canvas_offset: (0, 0), + canvas_size: (0, 0), + }))) .setup(|app| { #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS { @@ -215,6 +387,12 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ greet, trace, debug, info, warn, error, create_window, get_frame_streamer_port, + render_window_create, + render_window_update_gradient, + render_window_set_position, + render_window_set_size, + render_window_sync_position, + render_window_close, audio::audio_init, audio::audio_reset, audio::audio_play, diff --git a/src-tauri/src/render_window.rs b/src-tauri/src/render_window.rs new file mode 100644 index 0000000..0d5ddb5 --- /dev/null +++ b/src-tauri/src/render_window.rs @@ -0,0 +1,161 @@ +use std::sync::Arc; +use std::thread; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}, + window::WindowBuilder, +}; + +#[cfg(target_os = "linux")] +use winit::platform::x11::EventLoopBuilderExtX11; + +use crate::renderer::Renderer; + +/// Events that can be sent to the render window thread +#[derive(Debug, Clone)] +pub enum RenderEvent { + UpdateGradient { top: [f32; 4], bottom: [f32; 4] }, + SetPosition { x: i32, y: i32 }, + SetSize { width: u32, height: u32 }, + RequestRedraw, + Close, +} + +/// Handle to control the render window from other threads +pub struct RenderWindowHandle { + proxy: EventLoopProxy, +} + +impl RenderWindowHandle { + /// Update the gradient colors + pub fn update_gradient(&self, top: [f32; 4], bottom: [f32; 4]) { + let _ = self.proxy.send_event(RenderEvent::UpdateGradient { top, bottom }); + } + + /// Set window position + pub fn set_position(&self, x: i32, y: i32) { + let _ = self.proxy.send_event(RenderEvent::SetPosition { x, y }); + } + + /// Set window size + pub fn set_size(&self, width: u32, height: u32) { + let _ = self.proxy.send_event(RenderEvent::SetSize { width, height }); + } + + /// Request a redraw + pub fn request_redraw(&self) { + let _ = self.proxy.send_event(RenderEvent::RequestRedraw); + } + + /// Close the render window + pub fn close(&self) { + let _ = self.proxy.send_event(RenderEvent::Close); + } +} + +/// Spawn the render window in a separate thread +pub fn spawn_render_window( + x: i32, + y: i32, + width: u32, + height: u32, +) -> Result { + let (tx, rx) = std::sync::mpsc::channel(); + + thread::spawn(move || { + let mut event_loop_builder = EventLoopBuilder::with_user_event(); + + // On Linux, allow event loop on any thread (not just main thread) + #[cfg(target_os = "linux")] + { + event_loop_builder.with_any_thread(true); + } + + let event_loop: EventLoop = event_loop_builder.build().unwrap(); + let proxy = event_loop.create_proxy(); + + // Send the proxy back to the main thread + tx.send(proxy.clone()).unwrap(); + + let window = WindowBuilder::new() + .with_title("Lightningbeam Renderer") + .with_inner_size(winit::dpi::PhysicalSize::new(width, height)) + .with_position(winit::dpi::PhysicalPosition::new(x, y)) + .with_decorations(false) // No title bar + .with_transparent(false) // Opaque background + .with_resizable(false) + .build(&event_loop) + .unwrap(); + + let window = Arc::new(window); + + // Initialize renderer (async operation) + let mut renderer = pollster::block_on(Renderer::new(window.clone())); + + event_loop.run(move |event, elwt| { + elwt.set_control_flow(ControlFlow::Wait); + + match event { + Event::UserEvent(render_event) => match render_event { + RenderEvent::UpdateGradient { top, bottom } => { + eprintln!("[RenderWindow] Updating gradient: {:?} -> {:?}", top, bottom); + renderer.update_gradient(top, bottom); + window.request_redraw(); + } + RenderEvent::SetPosition { x, y } => { + eprintln!("[RenderWindow] Setting position: ({}, {})", x, y); + let _ = window.set_outer_position(winit::dpi::PhysicalPosition::new(x, y)); + } + RenderEvent::SetSize { width, height } => { + eprintln!("[RenderWindow] Setting size: {}x{}", width, height); + let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(width, height)); + } + RenderEvent::RequestRedraw => { + window.request_redraw(); + } + RenderEvent::Close => { + eprintln!("[RenderWindow] Closing render window"); + elwt.exit(); + } + }, + + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + elwt.exit(); + } + + Event::WindowEvent { + event: WindowEvent::Resized(physical_size), + .. + } => { + renderer.resize(physical_size); + window.request_redraw(); + } + + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + match renderer.render() { + Ok(_) => {} + Err(wgpu::SurfaceError::Lost) => renderer.resize(window.inner_size()), + Err(wgpu::SurfaceError::OutOfMemory) => { + eprintln!("Out of memory!"); + elwt.exit(); + } + Err(e) => eprintln!("Render error: {:?}", e), + } + } + + _ => {} + } + }).expect("Event loop error"); + }); + + // Wait for the proxy to be sent back + let proxy = rx.recv().map_err(|e| format!("Failed to receive proxy: {}", e))?; + + Ok(RenderWindowHandle { proxy }) +} diff --git a/src-tauri/src/renderer.rs b/src-tauri/src/renderer.rs new file mode 100644 index 0000000..2ae70fe --- /dev/null +++ b/src-tauri/src/renderer.rs @@ -0,0 +1,293 @@ +use std::sync::Arc; +use wgpu::util::DeviceExt; + +/// Vertex data for rendering +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct Vertex { + position: [f32; 2], + color: [f32; 4], +} + +impl Vertex { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + // Position + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + // Color + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} + +/// Main renderer state that manages the wgpu rendering pipeline +pub struct Renderer { + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + size: winit::dpi::PhysicalSize, + render_pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + num_vertices: u32, +} + +impl Renderer { + /// Create a new renderer for the given window + pub async fn new(window: Arc) -> Self { + let size = window.inner_size(); + + // Create wgpu instance + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + // Create surface from window + let surface = instance.create_surface(window.clone()).unwrap(); + + // Request adapter + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + // Request device and queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("Lightningbeam Render Device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + }, + None, + ) + .await + .unwrap(); + + // Configure surface + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .find(|f| f.is_srgb()) + .copied() + .unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, // VSync + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + // Create shader module + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Gradient Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shaders/gradient.wgsl").into()), + }); + + // Create render pipeline + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + + // Create initial gradient vertices (two triangles forming a quad) + let vertices = Self::create_gradient_vertices(); + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + Self { + surface, + device, + queue, + config, + size, + render_pipeline, + vertex_buffer, + num_vertices: vertices.len() as u32, + } + } + + /// Create vertices for a gradient quad covering the entire viewport + fn create_gradient_vertices() -> Vec { + vec![ + // First triangle + Vertex { + position: [-1.0, 1.0], + color: [0.2, 0.3, 0.8, 1.0], // Blue at top + }, + Vertex { + position: [-1.0, -1.0], + color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom + }, + Vertex { + position: [1.0, -1.0], + color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom + }, + // Second triangle + Vertex { + position: [-1.0, 1.0], + color: [0.2, 0.3, 0.8, 1.0], // Blue at top + }, + Vertex { + position: [1.0, -1.0], + color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom + }, + Vertex { + position: [1.0, 1.0], + color: [0.2, 0.3, 0.8, 1.0], // Blue at top + }, + ] + } + + /// Resize the renderer (call when window is resized) + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = new_size.width; + self.config.height = new_size.height; + self.surface.configure(&self.device, &self.config); + } + } + + /// Render a frame + pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.1, + b: 0.1, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.draw(0..self.num_vertices, 0..1); + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } + + /// Update gradient colors (for future customization) + pub fn update_gradient(&mut self, color_top: [f32; 4], color_bottom: [f32; 4]) { + let vertices = vec![ + Vertex { + position: [-1.0, 1.0], + color: color_top, + }, + Vertex { + position: [-1.0, -1.0], + color: color_bottom, + }, + Vertex { + position: [1.0, -1.0], + color: color_bottom, + }, + Vertex { + position: [-1.0, 1.0], + color: color_top, + }, + Vertex { + position: [1.0, -1.0], + color: color_bottom, + }, + Vertex { + position: [1.0, 1.0], + color: color_top, + }, + ]; + + self.queue + .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices)); + } +} diff --git a/src-tauri/src/shaders/gradient.wgsl b/src-tauri/src/shaders/gradient.wgsl new file mode 100644 index 0000000..216e3ef --- /dev/null +++ b/src-tauri/src/shaders/gradient.wgsl @@ -0,0 +1,26 @@ +// Vertex shader + +struct VertexInput { + @location(0) position: vec2, + @location(1) color: vec4, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) color: vec4, +} + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.clip_position = vec4(input.position, 0.0, 1.0); + output.color = input.color; + return output; +} + +// Fragment shader + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return input.color; +}