From fd582828c2a3700059b0c255cf10164853732c2d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 27 Jun 2026 18:14:05 -0400 Subject: [PATCH 01/43] Add text layers Introduce editable text layers: a resizable text box with editable text, font size, color, font family, and alignment. Core: - New TextLayer/TextContent (text_layer.rs), wired into AnyLayer/LayerType and all the exhaustive match sites; structured so content can be keyframed later via content_at(). - fonts.rs: thread-local parley FontContext with three bundled fonts (Liberation Sans/Serif/Mono, SIL OFL), system-font enumeration consolidated to base families, document-embedded fonts, glyph/caret/selection geometry, and a background preloader for the picker fonts. - Rendering via parley layout + Scene::draw_glyphs (renderer.rs); text composites through the vector path. - Actions: CreateTextClipAction (vector-layer branch, undoable), SetTextContentAction, ResizeTextBoxAction. - .beam font embedding: MediaKind::Font rows (content-hash dedupe) written on save and registered on load, with bundled-default fallback. - VectorClip content bounds include text boxes so text-only clips are selectable/draggable. Editor: - Text tool: click empty/raster/video to create a top-level text layer, or a vector layer to create+enter a clip containing the text; click an existing box to edit it. - Hybrid in-place editing: a hidden egui TextEdit drives input/IME/caret while the text and caret/selection render in Vello; empty just-created layers are removed on commit. - Selection outline + 8 resize handles (re-wrap text) with hover cursors; factored the corner/edge resize-cursor mapping shared with the Transform tool. - Info panel: edit text, size, color, alignment, box size, and a font-family picker that previews each entry in its own font (fonts preloaded in the background to avoid hitches). Deps: add parley (git, pinned to match vello's peniko); bundle Liberation fonts under lightningbeam-core/assets/fonts. Gitignore the local .cargo/config.toml used to select a machine's ffmpeg. --- lightningbeam-ui/.gitignore | 5 + lightningbeam-ui/Cargo.lock | 559 +++++++++++++----- lightningbeam-ui/Cargo.toml | 5 + .../lightningbeam-core/Cargo.toml | 3 + .../assets/fonts/LiberationMono-Regular.ttf | Bin 0 -> 319624 bytes .../assets/fonts/LiberationSans-Regular.ttf | Bin 0 -> 410820 bytes .../assets/fonts/LiberationSerif-Regular.ttf | Bin 0 -> 393692 bytes .../assets/fonts/README.txt | 14 + .../src/actions/add_clip_instance.rs | 6 +- .../src/actions/add_layer.rs | 1 + .../src/actions/create_text_clip.rs | 133 +++++ .../src/actions/loop_clip_instances.rs | 2 + .../lightningbeam-core/src/actions/mod.rs | 6 + .../src/actions/move_clip_instances.rs | 4 + .../src/actions/remove_clip_instances.rs | 2 + .../src/actions/resize_text_box.rs | 59 ++ .../src/actions/set_text_content.rs | 61 ++ .../src/actions/split_clip_instance.rs | 7 +- .../src/actions/transform_clip_instances.rs | 2 + .../src/actions/trim_clip_instances.rs | 5 + .../lightningbeam-core/src/beam_archive.rs | 5 + .../lightningbeam-core/src/clip.rs | 12 + .../lightningbeam-core/src/clipboard.rs | 8 + .../lightningbeam-core/src/document.rs | 15 +- .../lightningbeam-core/src/file_io.rs | 52 ++ .../lightningbeam-core/src/fonts.rs | 296 ++++++++++ .../lightningbeam-core/src/layer.rs | 24 + .../lightningbeam-core/src/lib.rs | 2 + .../lightningbeam-core/src/renderer.rs | 65 +- .../lightningbeam-core/src/text_layer.rs | 145 +++++ .../lightningbeam-core/src/tool.rs | 9 +- .../lightningbeam-editor/src/main.rs | 10 +- .../src/panes/asset_library.rs | 3 + .../src/panes/infopanel.rs | 207 +++++++ .../lightningbeam-editor/src/panes/stage.rs | 522 +++++++++++++++- .../src/panes/timeline.rs | 4 + .../lightningbeam-editor/src/panes/toolbar.rs | 1 + 37 files changed, 2070 insertions(+), 184 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationMono-Regular.ttf create mode 100644 lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationSans-Regular.ttf create mode 100644 lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationSerif-Regular.ttf create mode 100644 lightningbeam-ui/lightningbeam-core/assets/fonts/README.txt create mode 100644 lightningbeam-ui/lightningbeam-core/src/actions/create_text_clip.rs create mode 100644 lightningbeam-ui/lightningbeam-core/src/actions/resize_text_box.rs create mode 100644 lightningbeam-ui/lightningbeam-core/src/actions/set_text_content.rs create mode 100644 lightningbeam-ui/lightningbeam-core/src/fonts.rs create mode 100644 lightningbeam-ui/lightningbeam-core/src/text_layer.rs diff --git a/lightningbeam-ui/.gitignore b/lightningbeam-ui/.gitignore index c91cf97..d08bb02 100644 --- a/lightningbeam-ui/.gitignore +++ b/lightningbeam-ui/.gitignore @@ -15,3 +15,8 @@ # OS .DS_Store Thumbs.db + +# Local, machine-specific cargo config (e.g. PKG_CONFIG_PATH for a local ffmpeg). +# CI/packaging build ffmpeg from source (see .github/workflows/build.yml), so this +# must never leak into a commit. +.cargo/config.toml diff --git a/lightningbeam-ui/Cargo.lock b/lightningbeam-ui/Cargo.lock index c0e474c..1f6c4ca 100644 --- a/lightningbeam-ui/Cargo.lock +++ b/lightningbeam-ui/Cargo.lock @@ -197,7 +197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "libc", ] @@ -219,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.13.0", "cc", "cesu8", "jni", @@ -319,7 +319,7 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -700,10 +700,10 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "regex", @@ -741,9 +741,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -796,7 +796,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -910,7 +910,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -935,7 +935,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "log", "polling", "rustix 0.38.44", @@ -949,7 +949,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "polling", "rustix 1.1.2", "slab", @@ -1303,7 +1303,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "core-foundation 0.10.1", "libc", ] @@ -1359,7 +1359,7 @@ dependencies = [ "ndk-context", "num-derive", "num-traits", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-audio-toolbox", "objc2-avf-audio", "objc2-core-audio", @@ -1369,7 +1369,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -1445,7 +1445,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "crossterm_winapi", "libc", "mio", @@ -1754,10 +1754,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1866,7 +1866,7 @@ version = "0.33.3" dependencies = [ "accesskit", "ahash 0.8.12", - "bitflags 2.10.0", + "bitflags 2.13.0", "emath", "epaint", "log", @@ -2234,7 +2234,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "ffmpeg-sys-next", "libc", ] @@ -2348,13 +2348,22 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bf886368962a7d8456f136073ed33ed1b0770c690d2063731bb6a776e99f33" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ - "roxmltree", + "roxmltree 0.20.0", ] [[package]] @@ -2371,6 +2380,27 @@ dependencies = [ "ttf-parser 0.21.1", ] +[[package]] +name = "fontique" +version = "0.11.0" +source = "git+https://github.com/linebender/parley?branch=main#bf3debd02240f6706d9b0153fb944b788c11dac2" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts 0.40.2", + "roxmltree 0.21.1", + "smallvec", + "windows 0.62.2", + "windows-core 0.62.2", + "yeslogic-fontconfig-sys", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2662,7 +2692,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -2727,7 +2757,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg_aliases", "cgl", "dispatch2", @@ -2735,7 +2765,7 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -2804,7 +2834,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "gpu-alloc-types", ] @@ -2814,7 +2844,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -2835,7 +2865,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2846,7 +2876,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -2937,6 +2967,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "harfrust" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0589ddd0d2935dd2845827ac606b4081c266225d613b268ed2910f832889cab" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "read-fonts 0.40.2", + "smallvec", +] + [[package]] name = "hash32" version = "0.3.1" @@ -2984,6 +3026,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -3061,7 +3112,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -3075,35 +3126,58 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "icu_locale" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "icu_locale_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3115,15 +3189,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3135,18 +3209,20 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -3154,6 +3230,27 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" +dependencies = [ + "icu_collections", + "icu_locale", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" + [[package]] name = "idna" version = "1.1.0" @@ -3362,7 +3459,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] @@ -3457,7 +3554,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", "redox_syscall 0.5.18", ] @@ -3509,9 +3606,10 @@ dependencies = [ "image", "kurbo 0.12.0", "lru", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", + "parley", "pathdiff", "rstar", "rusqlite", @@ -3529,7 +3627,7 @@ dependencies = [ [[package]] name = "lightningbeam-editor" -version = "1.0.5-alpha" +version = "1.0.7-alpha" dependencies = [ "beamdsp", "bytemuck", @@ -3580,7 +3678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b407ca668368d1d5a86cea58ac82d9f9f9ca4bac1e9dce6f16f875f0f081a911" dependencies = [ "ahash 0.8.12", - "bitflags 2.10.0", + "bitflags 2.13.0", "const-str", "cssparser", "cssparser-color", @@ -3689,7 +3787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" dependencies = [ "cc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "time", ] @@ -3746,9 +3844,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -3778,7 +3876,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -3896,7 +3994,7 @@ checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -3927,7 +4025,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "jni-sys", "log", "ndk-sys", @@ -3974,7 +4072,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -4142,9 +4240,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -4155,7 +4253,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4171,10 +4269,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-cloud-kit 0.3.2", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -4192,9 +4290,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -4207,7 +4305,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4217,7 +4315,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4230,8 +4328,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4253,7 +4351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-audio-types", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4265,8 +4363,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", ] [[package]] @@ -4275,7 +4373,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4287,8 +4385,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4298,11 +4396,11 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "dispatch2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -4311,9 +4409,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -4336,7 +4434,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4358,8 +4456,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -4370,8 +4468,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -4389,7 +4487,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "dispatch", "libc", @@ -4402,10 +4500,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4415,8 +4513,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4438,7 +4536,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4450,7 +4548,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4463,8 +4561,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4484,7 +4582,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit 0.2.2", @@ -4516,7 +4614,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4525,9 +4623,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -4625,7 +4723,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cssparser", "log", "phf", @@ -4678,6 +4776,45 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parlance" +version = "0.1.0" +source = "git+https://github.com/linebender/parley?branch=main#bf3debd02240f6706d9b0153fb944b788c11dac2" + +[[package]] +name = "parley" +version = "0.11.0" +source = "git+https://github.com/linebender/parley?branch=main#bf3debd02240f6706d9b0153fb944b788c11dac2" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_core", + "parley_data", + "skrifa 0.43.2", +] + +[[package]] +name = "parley_core" +version = "0.11.0" +source = "git+https://github.com/linebender/parley?branch=main#bf3debd02240f6706d9b0153fb944b788c11dac2" +dependencies = [ + "icu_properties", +] + +[[package]] +name = "parley_data" +version = "0.11.0" +source = "git+https://github.com/linebender/parley?branch=main#bf3debd02240f6706d9b0153fb944b788c11dac2" +dependencies = [ + "icu_properties", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -4909,7 +5046,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -4963,6 +5100,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -5148,9 +5287,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -5238,7 +5377,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cassowary", "compact_str", "crossterm", @@ -5335,7 +5474,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", - "font-types", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487889119a5f19ff7c0a20637196bdc76b9f54ebec17e3588b5d75e4999f8773" +dependencies = [ + "bytemuck", + "font-types 0.12.0", + "once_cell", ] [[package]] @@ -5353,7 +5503,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -5452,7 +5602,7 @@ dependencies = [ "dispatch2", "js-sys", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -5510,7 +5660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" dependencies = [ "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.13.0", "serde", "serde_derive", "unicode-ident", @@ -5522,6 +5672,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "rstar" version = "0.12.2" @@ -5545,7 +5704,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5580,7 +5739,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5593,7 +5752,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -5612,7 +5771,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "smallvec", "ttf-parser 0.21.1", @@ -5862,7 +6021,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbe997d0f2480442d727488fbe2150779114cbe480ecdbadef58b33e0318ffb" +dependencies = [ + "bytemuck", + "read-fonts 0.40.2", ] [[package]] @@ -5896,7 +6065,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -5921,7 +6090,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "calloop 0.14.3", "calloop-wayland-source 0.4.1", "cursor-icon", @@ -5968,7 +6137,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -6473,11 +6642,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -6755,7 +6925,7 @@ dependencies = [ "kurbo 0.11.3", "log", "pico-args", - "roxmltree", + "roxmltree 0.20.0", "rustybuzz", "simplecss", "siphasher", @@ -6781,7 +6951,7 @@ dependencies = [ "kurbo 0.11.3", "log", "pico-args", - "roxmltree", + "roxmltree 0.20.0", "simplecss", "siphasher", "strict-num", @@ -6841,7 +7011,7 @@ dependencies = [ "log", "peniko", "png 0.17.16", - "skrifa", + "skrifa 0.37.0", "static_assertions", "thiserror 2.0.17", "vello_encoding", @@ -6857,7 +7027,7 @@ dependencies = [ "bytemuck", "guillotiere", "peniko", - "skrifa", + "skrifa 0.37.0", "smallvec", ] @@ -6994,7 +7164,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -7006,7 +7176,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cursor-icon", "wayland-backend", ] @@ -7028,7 +7198,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7040,7 +7210,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7053,7 +7223,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7066,7 +7236,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7079,7 +7249,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7139,7 +7309,7 @@ dependencies = [ "jni", "log", "ndk-context", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "url", "web-sys", @@ -7158,7 +7328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "document-features", @@ -7189,7 +7359,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "document-features", @@ -7249,7 +7419,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.10.0", + "bitflags 2.13.0", "block", "bytemuck", "cfg-if", @@ -7294,7 +7464,7 @@ version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -7364,11 +7534,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -7380,6 +7562,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -7406,6 +7597,19 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -7414,7 +7618,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -7483,6 +7698,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -7501,6 +7726,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -7520,6 +7754,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7646,6 +7889,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -7844,7 +8096,7 @@ dependencies = [ "ahash 0.8.12", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -8008,7 +8260,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dlib", "log", "once_cell", @@ -8043,10 +8295,21 @@ dependencies = [ ] [[package]] -name = "yoke" -version = "0.8.1" +name = "yeslogic-fontconfig-sys" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8055,9 +8318,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -8206,21 +8469,23 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -8228,9 +8493,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/lightningbeam-ui/Cargo.toml b/lightningbeam-ui/Cargo.toml index 1c2c1b1..b20938d 100644 --- a/lightningbeam-ui/Cargo.toml +++ b/lightningbeam-ui/Cargo.toml @@ -24,6 +24,11 @@ vello = { git = "https://github.com/linebender/vello", branch = "main" } wgpu = { version = "27", features = ["vulkan", "metal", "gles"] } kurbo = { version = "0.12", features = ["serde"] } peniko = "0.5" +# Text layout/shaping for text layers. Pinned to git main to match the peniko 0.5 +# / skrifa 0.37 that vello's git-main resolves to — a released parley still pins +# peniko 0.4, which would split `peniko::Font` into two incompatible types at the +# vello `draw_glyphs` boundary. +parley = { git = "https://github.com/linebender/parley", branch = "main" } # Windowing winit = "0.30" diff --git a/lightningbeam-ui/lightningbeam-core/Cargo.toml b/lightningbeam-ui/lightningbeam-core/Cargo.toml index c8a8fef..2a457c7 100644 --- a/lightningbeam-ui/lightningbeam-core/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-core/Cargo.toml @@ -18,6 +18,9 @@ bytemuck = { version = "1.14", features = ["derive"] } kurbo = { workspace = true } vello = { workspace = true } +# Text layout/shaping (text layers) +parley = { workspace = true } + # Image decoding for image fills image = { workspace = true } diff --git a/lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationMono-Regular.ttf b/lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1b3fc9bb6d30f0d5e311cc1553c2805944585fe4 GIT binary patch literal 319624 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 ~D)YWt9>N$1woVt2WT|K9+o>N!PsjKJI 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�Tj@5so$jDJ=`I}8{W<-D?xA1OuaFh-Yr2o_ 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 Y%Z`1^)%W0k595D(6%+sB_gu>V4Ym~YZ~y=R literal 0 HcmV?d00001 diff --git a/lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationSerif-Regular.ttf b/lightningbeam-ui/lightningbeam-core/assets/fonts/LiberationSerif-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3e98e409c37b7de3b7ca9a2bf95802e03fff1e5a GIT binary patch literal 393692 zcmb@v4_s7L`uP8xd+)%EqWo1+PzC}21SSe9D(a+&q^PK9V5C@RRBDt|R#sG&q?lA@ zW>i#UW>#a7QBl!mt(9)!sGx#fwV1?0+N=`~6nsUCwo4>9X>bQ_`)m*p3z%wC$qBmo6Xv!jz%n z7#JlIRkHNr^A`1)yW1l&VKwPHmMvOY*6mrxyX4kF`J==+5s2)`YW|B3 zKf3zTYk%8%{EjYTfc*?)>td2>S|zGl93qY_jxJ}bG=*`yg-&Y{wz_Bc`_FAiI25DhZS7^x$~OxrkZ^vIgl3I*G{w3Pw0`K_$}m3 zLQm`V3SKV;;Se21elB)4YoBdge!6hiImRKh&u6Nu|NM3E=i80~wF z_(0-=X`5u?LqY4u!&LMK<6}PQQC!D>P_9830ll#~%KW=LueQh*9a{M^+HQZ^o^;*z zS3)ruT8HpK?L$zM6c{5Uf$Pic_NVQ&4Bx*q(`fG!nQJVOEJ#Mb#;TEtj1wpEgRlYl zMB{3^UuQG8C;HKK)NLL0N6@~yKl1H<(0$h{ro{;v@IeDn+cOE{Kv-6wWvxASP<2ZS-yx0P@t zp^tE?-46cH@fbc_B+2?7(c{#ge;~F7YM;;@d~#@dWQu*=WXs8OqRgTGyeC5IUGAZK zso#&HW!nE4r-qCd-S4`+#^T%HQ^E*+U+R0qA5J~xT930N?Sl@@i#kKUPx?NK!S??t z^xyCLp6^9I-M{vIPv4Bf_PHmA*dN9{^c(k5lC{}x?^_5H$tU=K$%($FkArTapEt`S z(z49gm=`{mCD_il3^~VI;nQ>7tNL1(7c`jpBG}hKJ0x(e`%$-ly!ok&464*V`AU)5 zZE4#WD>H(ZFqSH5zcd*~T0HHe=dx9_*%{V0=AtIC{O#lPhe<&rXp?!g%@1-Rc9Mf8 zbk_MqJ@ZJ`v^L%QMbhh4VISXaJ`r5&fT z+Kmm4#g9v6u(MUh>TBjb?YDi;pdZ0l`^c_C=a~3E^%1^I3D^jP{`n@yZ%facf%`GRZknQ zW4+q$(|dzXy;{2+^!ls!6EUFcF_`-FG6u>J!g-0NS4V!AS!S=vx{d5SK^M_i ze}Z>tlbaZiPcSDBlc=ES%r|SO^HtE7a^9Cr%O{JhZ)A~iy9|IdHQx8LdXex&iP9K_ z=q+OoDziyyOgx{-naYH}W>hqu!=4eRsx0mWiT(?yXTw!cKaZQ}- z(ek(rwz$@4`vG*m5%4wK0@na`eaGNivpeIoKpZMx`lzR+kCDf|2VcxE*>o!8(|b2f zIKjO0Ca&9>OjYvlhG&1c+5WbGAgYZ)k z-$ufJKriAW2pOhqq4Q zS^xDM!fwzPmZRSeEyVi}(k2}rf^Lh+p#9kgdSCQ+cn9_|CiL9nff@Ash5ocOJ(qI5 z#4Q12hCsj}6)PaNiL_E~F&)kR{>4)P{5 z*X3b*l;7rk%(d%%9S(#2K^FUmfxhqUvX~onsAYDUI?kLQ94AVL)<}Ea6xPnZTzBsE z^d7hf`|L@~J0#A!hw^IJb9^9OEP5)aR(k7snE5E%N+$nQ$)yh*>`l&s-3|}?rmb>@ zV>9Uyl)sOAyr=ZhWpG{G*)7dzWfFO!-%sju^4fb>Z8K=L%ps3HYmQ}I@8OJK&jgOGGEVzomz5)zI7i!MI;nS# zU4Okkur3hS>tCK@jqezHgfV)p(3pBp+`;&y4Qv^G=nwUpGsC=&`CIRGhtfy-tf}(_ zNgVso1dWv-iH>4yY=w*IpI;@BwMDO8ooB0$SZj1S7h_AWgZ5chTzdcD_$%eUfjxaz z!w-MCy$KUti=@~QPWl3gwhE+A@Jrky7s(jAT^l6aIf=Dwhm4^;hdI{Ba2IEk;H#+@ z&q=7CI1c#^Gd{bcZ;!K}8@NB1Q3Ij#Rr9t?)c!^V2TPc>P@I8KkMn@9+BfaH9;fzL@z*)vTfkP} zUZRf&lVp7~wquXmt7?E98+8@R@f-A;j!`zee3Q-hXER?umL7x5hFVV*7BCCknG zWl8WE-1`>o##w+pls*gOo3;3H8#bG{XE?jjzmrdDGwtWp;Lc}8Jeu)k(M^m7-!j4l zP)vLVlrq-dg$87Ne2nJ6N6R_}a;4X?BEW`^{e_SGz6(3^_|7Fxn%1+^bs9SQb9_7T zaiGWZJMcp%`r4N3G;LR-Wg2D+chu^TTI(gAS#^J$e2^Khv5k4-IA<+VQ;z}`7z3w&GW#gjNK$rqrQ_^h&M`QpqeSc#?AQl zl?*e^kThck=ZO&Z5;G*h<{BBIDv94u{!8t?OEKpAel^xhiZK!WIO%F6WA|}%CQ1bM z`l$Di-!G@5_ZzYi=yWp{;LqdeP2?Wxsn0#W$7mPURbLxxy*^`wBiqLHuB024q}Yy5=UFcsmElHdd&B} z5ihZvpZe1d-PC3D;Vc;m{nR??iLAG7ALBGhFz=LMwE1bq{l1TBs|@1~`ct?0ZS>F6 z5~H5;9jD#hmmJFxH-=bk|geF_PrY4PD0i#`y9Z&{nXIs zL5F*~<8j7o=X|Z_dOd#sJ=F8DK6C1Hd%ybM!oWQLKSS0_`a{|d>& zvs&6%@5gv%>DSlKT6F#sp)Q|(^FN>THFsY7TK(Yxy=F1yl1~l&cJ#9yZSRl5KVFCY z>rr4I5D0a9>M?gpnCQPw(0fb0Kl+~`>pbnX&WvntsN10!KS*BLrvu9(}g_Zv?ea4mjO^ zjKP_-6W^PhEkRT#zPGu4n|-#@ICz)&N9eZyBLZ~`K8C-V)K2u7^}k{J%3D>ZAAP?4 zZvY70CCWLdXB;$3Qv%Dg0mk67b_ch5y%j z|B?Rz-%k#?@4kS2{!l-g3;!eE^%x9_giqmTd%vdV+B5uwGiKW0J^0nnK0<9*KR=8y zcQc+oV2quVC(!mMLj4>>;~y*d*-I1bqHohEv&zn&h7HX-f7C`n`|xilw8C)mbUwS$ z&zQcT&w0sj?_2E9c&zjNx#FpsPUFBJyiXB9-udyp$J*_Nzq5YlVk4J5(+?V_AN}ry zYyA#7@ND)e9cNC|bM-tw`klId7d^q)CoAybeZS4=gnI99qsu03h-#9>=3KcL;;?m- z#=28KANB24^Ex&$w?7ofLm1|V?`ilVj5paUy(uNI0Or6<7-QVTUgu4oMZGCiupG{Z zDX1-aQbdv$rcXB%GXugJe#KM(&O{afSgPSD9*=)aaGOUl} zMyn6=y>R~hv-H<%CUw&90{XM=+VeN^DB>x$y-MM1*0Ch$FWj0yTs48uQ>s8PP`g| zjQ1D^KwTyHYjq#o76@rOCpwQEBY;-5S!kzd9kU zR>QS^|L~ykPtI6;kH-I9n&J2}L8H&t=V^wuwBn{y3h58F&@ht^M%Ti;Tl9&oJ0+AKl**japs%$0%XixHX}MiU+BN|0{OMR z=7eq_k3VeZ9Ygy%k>9>1$zbyyovl5%Df7e5WkNwfsnrZy-oTn8GS~9 z&qdhk54rAm#X5`UAHQch*Rw%~nrxoGWD}0?Gm~%#yyh0*eUhPPuSMi z|G|%Wq@4wY{%{_8MZ_~;lph_>K(50nF!^MV>+9?NXx}wskn8ZaQ$hYm?Qv>7W}>It zG8F=K*6rOFxo&6OPP#t=eUpe>$8$jUo6c+ZDROIK9^+@sCusS-%;-&Fo0*xS!nOj==lV z+{4|YZSOC1m%FcXSG#Nd-bcpw$NTB=bK=j&`)b>Jqkm}s!FXS6d+$z?q>vNd=OtAn z)h2Dn`{(ig&7`;Se$0U4l;V_2@V?h0m3Xgy>)XM%OnpAz=e|GrUh+Nfd&XDqd)oJu zZ>R4uUxqK$mwecL=$k`dANt3k)rbD+MT7PKGp;d=g9GY}!C~YH$LcaX=%YS^?`sK@C)_?in zmse<+$d~L~zbN`*<`*7)_4(w_$A3QR^VHA#ihTCNXaD$&`|h*%KKtuuZ-4fxPCr<1 zaP9}ZT@s1@K>xKrdK~@VKh^(5_1Zs7E0q=Rwh{Rp7(yV1H;S|L1@l z?u;o>QJgUPs905@Zf2j74y>BNBZGhC9IN*&5`5gL*HLkfbdGXnbmnu8c5;vEk8`?{ zyVN<;nP*?kvW4QD?POnLOY)s_oxJaNp5-ib&Uc>#%cGO^I7L#oNqguoqu)y&H0XVzw=$^d(OW*4>;dWh^Al%_^Hb+% z&d;5#&M%x_IuAJyJHK-N!}+!Ii1Qogx6bdJZO-qVKREyCJnH<>`IGZ!=P~Cm&R;3- zH)p%^xU<8_oyf8;F6H7d=Q3Rue;uwMSFp?Ja=E&=y1GJK-CW&Wp{_7jxGTaH$R3|tflw#vgW-SsbmlZUL^_zsg4s~Hq>FTw5a}k}C6w=mhqFtGhtBnCCNPoQ#(V;*m3CqD+!3nJn2dMRMd!nJUwm*{3tB&y+lwCA{0_yXtd=cluHw zg)(2x=KJdB@K$Ugv-TpYceAXKTlhBn9kQ9R{Sfb*>KNG%%OmnAC*tk0LmrnW9>Jdk zw*(&!{wlaN_zQW?d5^rJ#sqgb2kCo9kDTCdI6ZzD{Ey(DWVKZBmTbMOl?_rYx5=$? zr);8z8@X@qm3!o$Mx}9|Tw-i7?lEfQD&v0RUY_}jXUf_03k&Av&zU_dZ|02Yxznbe znKLDOa@M4YXLu%zAD20H%;=0!BS#EN9eVnZltBXrBqb*FkB^Iv3J(kI-mPmFmoqrX zVVQ=K6t`CuPxhKg?(pnI36m2Rr`vIB zfPK-8i}-kQ*j{OG-IKl9mzS=cT#S!uXV)&15+*I~G9*QIcIisAD>3h>DD#(0FA> zj6svf>@>vLO&3c|Cod}T=FKXYoD~xvUpOS?OmFuDERiIWY?I!gN#0=Fr27JD<&|~r zoheOgs~W=O+~VYrl7x~)MFn1S5qZ~|lh>|Y;|)*t4o=AO4!){2nl@VOO-aa_>`m5I zr_DZ5&a{6Osl1M)umtzoABE~9d~<|)oFrM~mjoq+{U|!oYfSR0*#+_X6O&D^tX-R( z;LcuKymnE8uj1SUcUZ#OogpD>%O=y@l2_nW$e&*qTX8O<>q6Zz31Qyu zKgGl+tc?hFj~`ZsZ zJgukB!Ystzo|715117I6C|Tl-D~>7QzFXohh>7=l3hCj(go4F|df3rIgIg$$57lC< z)#pW!c3Z!6R;MfFR?QzCnla--2Mzn zF-Ug%3nsf$H)@Vf(@9KIBs&?OleDa}Eu;0IcNZiqPAE($b$dN|1-glJ^VqHIZw$N5 zJ9}kXe!YhmO z8x%Z0(<^!idoseQHg~?hp%a*UITmDh-q34zdOZ4OE*-;YT$^xa$=ZZD1(~)s^T{P+zjELg~#ow5pBR;WKaMYpJ@i^JpexI7_7HzQ^zUgH^(=h>^clsp}xx~Z6*|j+a+P~L~qxGiCR8E%O`ZogS0$2VWL;{RAjH+OThQPZct$tPT5O$WB+@8Rx3SqEx9mpDk9LD&|LT3uUf4L&hl& zA&sgg5>Db;ho`G?9600ntJHX|*+i+aY>eXgI{{|G2C$%0BNdIIX!2UY&M}1R6mq=H zUuC21OhBr`smO8&r})EcPLAWRea?A$nlXf=N>eB#)UPxn^K#~nkt%_n_8OM znw%9cRlj7s*f=yUv@x#Hhy6x#H&Po6U-ws&6u$y!_?MjTL!=<1SvF z9JhSRX>q;NqUQ#unRA0EhSEJ}o|`l%yLh1|ZXrr%&rgk;KV@)SWLm^r2NkwZ3^n7- z3Fb_mEo?AfHiMnB^ZLcjf|k6tJj0Xc3ds(g88>t2OtZn);#oW`9&1y}rdCWf&&(bi zmop_JE_6!Vl%Z4Jp7P0*@1_JToTBdMzw9ToU(Pl?*@K5>d$Rk*XUFEm%jnL4Aq1T0)p={KRik`}$8dUYp{5i?V(;9+(>>ZqWXM0tp zH))Rk>zOs*8&v6)x%1C1*s0X5g*UBVFBAJt^A4X=;4SW3IL%u^#G@k>M8f*+>?soq zSF9*cwm&L4nfPV=mE_Bk5iVGP9I2d0m*f>{#R^%$hjf*!6*{jK(qx^6K<88BUa&&+ zpB3c6rp}~uYZd#WGuswAuc8+?)^oC<6Yb5MU+Osryw5qO^OMb4=mNP+zkfUF^NRcxss7|~F5*`RC#9Vn-NAWhzAea; zV$K%SP8ey_qYSaQOmvajApV^6J>KwoOtdVQDo+G84&en9M zIL5VqATHlOsns={9dF=7x%8wwjp}~B^(SV&+)tanVyDEMYTK5z{HIxuZUL}FAEA5&NrvqYjH?wDX(# z7LhUeuv%p7M*fX}l_KMcM8-Se2(PxWbw;tsq(**XxnE>5;S`I1hX9*7M@6P(^0mSO zk?H8n$P<}~Z!;+?uSR56ipcCRh9r4-H7E1XEufsja(?odC~|g_NYPTj&NwmbQy53lUjf3+R^-FIy#Y3E`#XBIWpZ zS(M1-*u6YQWMv{yw<~vxTzybv751({_nLfudY1zL!O#?Xc4&=JNH$K+`nI>Rv;1b zVI^#Xz0fA|0Qnyv{{u((X+GsYxEXfy6@m~*g#wX>$@?&6KU@LieVDShVsk6`wvxVe zmB=F*BHMDH9PndXJscEy6g!WS|50?d=ki4oH|VeoYM=>Ppw z9FB_Ytrgj~TI4T-fIM$=-He^TV&|{NME*wk?-YsbPZxRD2~8sJVe>t7{+qCi<1=u@O4pmSGjj&(jFgl0Hf4Bs&br?Snw~2g(A7AA`C7}0@ z5J&;^{!s&3c2wkR@_bEsN5UWrmICQV_KJK{C-NQnzRLvU-y#1Fd0VZ>_tfKi@_m0y zME^T4KjcFNApgM&r2RnJKS?`^jibnpwut;lIX|NNV>L8EtH@8teoBW@pp2g=a!$N>>Nt$3KLJ@2O(j0@J04kvo4)g7>XvpE4 zWo($7VSt{?BdQCsu4SS^R*CAyH_p1%i0YmxD%8zaQ^*sxR}^ct3dcsoF;S8D5ZNp$ zitC>E&~uxpUZtX$q{E`JD&VN7$(5qAQ-M5FR*K5W0_115iJH1v)U;Tj z?A&xfHhqt%8Jk7TED@E5-+9OQ+7Rip6M=klyrS}#ike$0YThYFSR5Z20;eoKmpVN zcF$iWsuZ14bV|`FB~NLis0&t$x-btY?;>9E9Hl;PgO8nL=ApO=lQ5yuXxnZxU>JZ3;G9axQ*=_iG zdy1$#qM!tjZ^GtXvA{KZBgMW*ZLWYOQFmtm@wTl)@fSk5bm7sO{w0z8bK%9Y41>i+T+Ek74IAbRVO<#}13yfqX{` zVC!-Gdz?IvZxr>!N~jd|WCr8_x=$Vx#lA-Eq@11DdMZ}b(*;lkl=1WtQT1Vv4#?}V z^$dEuqQNVQy^CsCD(blmpxo!VexCZhuvt_ic^Vre58|E2O_t zFX~UoUJU{A?_MS9wPL`|YdZkhYez-BPI<5I7xm{FQF|zBPZ41AjRH|`riyyY18t)A zBHNb<&7%HN4#?idzqcDjHFNz}?EEcP)H|z0v2RiP+eN*5MAUmNqW)ed>HxmH9|e0w zec%=KVX3Ht*gHu4BYgUp_{a6I8}^H0-=bQu(?Y(Me4xBfw~6|!Ton5k^*MGvr;OGV zD1s_cUy$bu?0vZkxc;&RngJgU9T0Vxe)*~ij)?k4tEjJ|fV8i%eWV_C!+ufUtb|HY z-)d@mVc>>T@IW3E1G@jD9gd=Rv_#a8gMc!B!tT!vqK=h|`h~P# zvGHr8sNd>Ewc~3$z8=pJ)q#G;Hc>wE`pS7gUCqBdL1?5yF24$lg1ur`75qw(Yex|r z0sYO;5U7D2Vg&DpRxzBFVz_cde-*&!vR90**bBj4w?rVlJF?I!XaugquoLbE;^9Zd zh_E09GR26@5F?7PM;UAsqbGKH){DWOz=&QYMsM=<2?6rOQeG@J`ZkEsk22$Si{U;Z zMtqAH{r8H&Io?PbB*p;j3_K>rAY_BFaaxHO$ti%{6c3ccHZg|OigEgC9tKf%D)nK# zH`1D*jmIHw!2XCLF-C>}{*2^$R5YMJ>VO!m`Nmj$$)ucd2gMkVtqGLl5up4tN}ySc ziR7JxjY-(e+9Sqf>M{kr9PFIAS&XUZP0J7?H&Kk~Q9$~P7BObli;-6)#;lEE%q|vV z4teHaH~)wja}SF#?|>L*dBrF|r!Wi1GasAtE5tb439Vujk@uWhF&3b^5WV6|F&0^{ zR}9v4<2-yTiH1R9EGEz5Jz^|r6yyAAApHVl7p@fJB3O#;Whrn(jOEQ@TTn3dFdd{P)u)_a7FccAFRvpz{DW9;k%P zfb78#pe_%#i185RKGZBm9k%OIfO6}Oi1Ba~ECuu)_KLBU^sVSU;(?8DRE%wI$bnU` zSBysm$oJ@KKQdAH-!cKq0WK#a%mVMi?BGi#9XIORTG4+q6~!U@=Xq5!yl zG7M6o2&w^_Udr(z_pSmjw2HBFGvLEhN5o)FGV1f8U5sarim{7wpLL7TK)&Zf0N9<9921t=MhFe(QcQzMu|Y769poTE#ewt*~6XUx|G1{=#woQ!h$@fDDp!ZMm{j))gqaJ7z{c&kcdwYC#KVY3kUz3O%&0Okdl2?a zgkxg%Y8ErPLCoH*V)m&JGlsNS^!sMO9x?l2H?CSt_YN`RE5+=;Qp|*WF%yv|kv@R< zK;(l;#T*Q$rHILS&`fR_&PBPxSmudW)}LB zozNg=b`Bg6a|(HLRss3WtQB)=k(kp40siKuLXDWL8RqoeV$P@(b0&VWZkTy_a74^m zTrKPcv0C!|9ORDu`KpC^FcJZ#J>2g;t;2(4nC6$0cr z3%|}<4fuQ3erOl70RIZ`qX2mUc?$NzQ85c+Aq%ioh<}C6V$P?G`RLCthH7Yn!(yHt z2I!u>Qp_UCJf}^}1^Bua*aOJVLw+7|)=cv}^v~N3tzwo?pOQ>KzXW*+@)G2WqaX{Y z=i)lpFXj?~L68Tlpa#%iLSLPao%3@6{qvEZ-z;XS1^8aN6gEQx92WC}FhG7mF;s#V z4v2Xn_AkUfXKVAql~4JgS~)_GU`*71^8aJ8IWHhkP5|OUYZU?uo|#+ zX{(qkq5xYfR)H6gm1DmgAImATd}bFd4&^@ zUxDrw$gkJ~?YxS@kCi2WY$Z0Y#O9UQyb`@DH$pv7{#8+s3Fux`1<0>Lel_x|a{ynj z#@DNnt%`3_wqkl8{HzQvY z4b*K7vNg2Hn!{pNhCnLh19mF0Q;D5Q^l!n=Eg4V>$Zu&DbFBr))-Huw*bUfOhn;oU zS%;l<*jb1EI_#`#6SK+<$g3)$5s!Wz zw!vP&PBpfx^8k5u0~`_aHe|O^-ffh3+YV@kqhj734Vi$Q+tI%rJGUPYlRc(+M+TI_ zX4nJA#N3z&*x88g#(HQK^Uf&9f)YS}XC3T@RxvkOkP7&|X*E#xChXsZ{4V5oA-}5< zuyxmdI4b65>}^hmd?<%npl+Kf|L#PfZg;POZLnYX$D<$xa$zY{!4B99N5s4*8nU1a zYJj@jb5zV4+N35IRze+M>t6i2mon}}e(zF1|K3Kx&V8|v2}Q6PTEx7cI^16+W^I(1 z50Li(@;-ph1L!<}{DB?N43zgEemqFpgC#)ThtR3RhdSz0R{^!q1Z`qI90ndJgN@K2 z=GJuZiup)6)WLo@D(1E*$O7cskZ(i&s0Haz0ILAo+qvFO-5(?Eu|&v)rBDNn&?4pz z(sm32(sq=H`FJHD|9|C)5HX*iUD=sQJ1>H#r#lYckiYdJtUueFN#I{99&hBh((Tqfq8FgPaW8~I|siQb#_V!lN=ZygqM zZ?TyBV#WMRA|QWzwV2IIfi(8+=HD8{e243Ij)=LRJnuT8Qq1>OiOHVbWWR2{Un}N^ zmY4^##QX^PC%Hh_vR};4@cFZ7F+WGvS|H{Z8Df6fD(0bPF~16db}_$B6!Qpqzd`St zMj-85%Kr|3+OY9GI{$1D^GC}5akrR1X9DtL)aw{_k5vQdzhL_peEVfHw2Ao}_I@i9 zvz_bXq;*t@>1!8DO8EL*hFC@jG>T=G!e-bemPKeC6U(t*tRT{Z%ESte7Rw1P(z-YS z{Vqqu>RK*VNEGD24zaqS-^~N$=~fTe>7D_rV6RxA{H82)DNtruA|MajEmrs-sDL)H zBJu%Sk?2Mui`*|(R2FO#tA_x#d$fqvlQMfYi`6R=YQ>7iMs$r>z0v7i3GHI_DTX6r z#c&;q|FPKWYe71cz(%0_1kz6&>~iP2vANszNc>%YeX23cNFOv zq>s*kYB(&`7!Tn8Sjrij3PoaN5@t4wH4fbgQDS+*;HX$A2^R;(=2v$2s~C)O15 zO(8yow47L|7VFF$v8Iw|>RxCSYuYxkax2A}zDKMXgfljaH8WqVJU6t7HLDzsi8UKP zc!p!mX%;JgsaSJ!;GkISsjaiHeby>CAXY&-V55L|;UGX>NICP-pO20C?P8ss3;0sx zgd(VdMzPL`2K3LNZs!~mYe5#20A(y#4I7~j>R~tRhgLW$*1{~n*1~ew2=%ZRNMG14 zR&ffDUR(inum_HawTQM@MBYWDEy9mQUZDJQk+YAs&Mkm4*bI%ZAINte>E}^D_SDvS z=$wa6Nez&9aUxVgn^>i!*|dvw0lu-%wJywn93bt&QdkM-U$`02yQl>A!a<;{i;yi1 zfpoyvrTKsk`(A75YS;*MP!GFdKeWP8v6eX@7E&P#D03NQE?WiFPz&T+wg)J089pz! zAQ}ci2IK(wmTwd5;wY#Ts|#A5NfgNI9O?g)nzq(1RRbhaS zt7_qxSl8qNWnXhttZR{9i~hCkVqJ&)y3K%375UI0*7YG!1nAsA-ESa%1M(ZAfcTAd za73)t)5g6mO_(Q8;EZ}e}h-7>M%gRdaqcw#R59Fk@t4wwWwuJDc;N7LJO=p31s= zhge(C*+P8FMmQ|iJ(;i>+Qh0MUQ-DN#kzM8tOClo&kdA)A7$Sk1*Om+RxR?{a-a?m zkp5sU)Qj~{3N(sUhrPNYz)oEaG{Qk>7wh3z$b=$T1vStJ2ccc8t+9{^MX(BLpb-v2 zyI7CJLMGrZ`y}g8>bpG$HbRS7J3L}NUJJyZ*e2GKWny`=z$+H}5o_lvv7Sl?uAe5~ z(?`UrC;b`P_L)ZD`k8jIb}fY_v7Q|S)nYZI!U3_KD*|jkkL-nFr~umW1?;`B2S|J2 zm{^Tr;D!vbUL^h^Ixo>aFY&xhKJG(>lPFe5*FUg4DGH$%MJI$A*{Buk&@u)PZxoHHRjLX8ej zOAAjMmYNh5l+b^`i1d+Z!+V-(W`f!`IX!)Hc3N8YmmiE@P&6zjXINTxw)I4Nj_@($ zIE>-AK(_G4Osh?cxOP6}jr5<&u zyItxQm%7rWM!M7hm+I+KU%AxlE>-SQOI&KUODT^_rMi^crK0T2W13v5-lghXs=}qp zT&l#S@?9#CA8NSNQI~3Usr`29Mmx#lQt5VTluPXnWHE|eD$k{6y3|lR+lj{UH=n_+Ttyv~+8HCZMM-?!kL;kxZ;Jl%Fmw-*@l+W4s*6Zzv9*Ab^a?TG7at0ZA_N?XP7 zOV+zQb0gzogRJ0)ux=qP%NgFQPmE!iJ)`>dby!x99z88HB0SjX3^#jPs*CCp@8kn;TBYN5XYM;Y({3NJDk9ad7Qj;(u4dNryOifyk1aq(=;lqyh8KpBi-kg5Hj2%s5 zepMm4rPI`i+|t~-U86hBRU<}syk&$e>6ov!YL=+9^VMA)7iiA!*vD7>87T$667vC?j#*)w>EOI$11A`Ngjq<8UXl{#9vM=KevT1Knd(W-c~@{A7L8w~d0Lx+d$ z(~S{5A+688w4n@pU2!e(_aGtd(km^zSK9F5!%`imtASkhQ@z4Zx40ihjY#X!vsZY+ zKrKhyBPuAkM}kTpFfe?)Qc+s3yHcNCbZFGX{&9~NUQwJos_(w-v+v$=M&Eu{j$ioj z+P^{eP0BXIA+tL-T1XYqNGZZ zXQaWu8Ic^<-LjM!XStI4M7jF7h7SwswX3je(6eWE4JWh=tN)-~g|YpgJv;Vv!dQL| zL<8~51c{z7f#AfLK53}XA`EU77SHSu7C*daaQc9R{y{yWdZrB@*(tT>jNgeHmYek# zA1~?H^+2tfa{t3MbIv|%X5PH{`PR^mXCHXr!L9e#-4hT+IhJ(X)=~A+E_HFkvzyo7 zR9m-ZLq}xCx|g1P>ctZRYC6MrBq)kHC-SxI3^mNNH*(;hzE)gBM0bakYMmaME~m$Z z1P4o;*}F?_0NEu)ozaDX*|+&xp{x!iXMi zV^qr+0ka%NW%>MpgeljfA!_s>#locTxtFYfsV) zvrTxAdIKf6?E;%^i%nxx=osbXmJQ54foa=6>jb9sfLKpjdIpc~Bs@a6kw-@lJ!w3L zr~Rw%8aN`pN3anb$NfIAyRxVJ@oJQR;#b2*sqT#Mo@P|9@y3XB zI@ z-&SMEL*QJq}SJ>oGmpjxF+e)^r{G~(H zIn?bAb%TSI+@XpcD$k)j4wZV6p^Q#T%?`EOq3Z1%8y#x3L$x_ni$ftdnjHL|TGcvK zwL?`nw57a&8Mi})In>Jz^`t|EI_wVDJs$YzHT>Ut+FpGE8afPUTzaX0G$4}+tVNy6 z_^?!_>x6m!)knAIeEvngQhsj~F0q~vado#`RzxKK^TQaKU@!fF^)4 zR9%F3ivVu;U8S4QNB@hT!b%os3mL_ZbLBgzPxc*&;hN!3^F`M9+7^;ZDO5EJ3P!i2mi;_ zb#kDW{rfjtMGpi22Su5%H(9UaN-ym8P0!NK5*9hi!4@v5S2q=$6qyj|7?@PzQDc*9 zhN^L%j(UDj*zxMnn&ghxJSxW;yx~u3O2>0Atv`3s`o@kIRL-j#&eLtaMBcLEtee;) zCwaozj|CY~3y5JE(heges0;`Iez+oy(Em zJloUY5hF*i>ISB&>+ZkZ+b=0AYlLU!uv?xxZF)volshuPGjep1T~4mrWE2}iSvj_N zmYPf@N}elJjj1Qy(w%qo{=n_byBF; zBK{1gZqwD3fh~GqdyWl$GiZ$F-}E!3zbC>}4%U2Dd56lZwX1WYd*yq+l7CF#p4+e?&!*9qgS-E`-(zmuo1PQ(CF=nRuZjRqE&6Q zDvnm3X#ZBrzb2j7K(OGjV=%(QBGQIOm|;f3iTG0;Ir|SBIJo~{!&NumaA|qP>QzSF z@f=m6^3`;;sN|M$I&BxL{hk8L{gfu zLPZ=Osv*_82RBb<1+)sJ>o#KVP8SUJM>U&ooBJ@UmSn6Ok zVw7HW_5LbUC4}pvSP#8y2L;EQ8`|G`{^@OJUVih4vgCw`yRJL<(OLD)g(b#QcRYIU zpZ;>w4V8VP>y(lF>?37=er;#&+4enfF8%4n_tT`(Giy*pQc|y85piZv_wEuT$ti;( zBO)VK6b_A4k&y|e^b6}pZTj^~Na(YoFd^9Nx}vZwxFXmH4OYyty*(-Xt1YS}Mb)OL z;uPgcIZ>;$q34`qZ%A|-`|CCI9EP|As%M`x^fp^>LpWv*H1+|AX7E}x z3AnX}&(B05EUb_(3FYRR9Fi0?Ns*|}BY193+Mq6q7D*5cet+KTq~BlwA+~3CWQ#T^ zhS>(c93>%2{ZZC*~MSAOGp2 zuRea|0gV3zYsWqQfPByDxmUM}PYoViD%oFo{TCmwVb2Xe@cixTF3J@jepqSF<>NdD zeR>@@$Mlk0rXb3n;CIoU=DxCdM- zBB6--i>RxJT8l_7O81N58Q0-BdJT6hH5-hO_-wF_i4KS}i7`G8Q{8YYF?u47bm*qt zDZT#Q&;IzuogY1L|9$lZvG(%&zn}H}?&l9H^KV^xL;Kd<-=VH|KcZHe`fHldUGV8v zXHfZA8JPXE!IO`@*nR&Po;gn7R=>jAS>SVYRbwEA75X8$yR?5%|M_g}R zGIzxtzxt!n+_CKHWpDKzetX*;TQ;s-yoLSx0lchQ-AQtKJhmO83L}S5tyuITD4vkOy{1uR$L0 z2|eh)8%pEmkwrJvehT<}w+pW=RU`$^}Ah{}OF zw8zbUYVgysj5nix677D%VITv3>he>kpW6M@>Zc}N8Iw=(quCl$B5I~es_CR`JsXzT z(gsB^^z=3DxFo?0i(?ua$0QmrsY+aul3p$mUxI}(0ZSw6`(nt6kMh3f2@B=PPK(6{ ziOA;jg~B%bW66N+skzR8NqS_i$=<;i+`7&%;p$kJdic`Y&#(LVEy`0pfD$$DS|))B zxg#XZ48D;PFkRn}ICp3bHtpH*d^Fy0NtN6rO-Z%++)Jtu>WMn@CyyPE1(?auhap2h z2<|sp4(h-H1cNaPDlL++l1*2qTdq5*8-hqJ_$#ixDUo>l?F{kBRZSifc@{`NJommd zO58q%PTZi}s@!@5#%2LMrHf#vnAu)sIc^Yf)DUd1lK2ePFv22Yl#PaGk`W`Z#ap)6 zrx=BbvTJYRhTnG}1F7<7s?U&uU<|0%Mb2G{FS}ICS+Ib1FIa%~FGTxe-)KMDTOpE7 ztP_`uN5vsgFY@-k{kFCyau!@uZGuAQStyM{`+s=&2HHYfZcxg`@VOoONE#0s2H&FR zFGG_iPQEtg12aHoTkO8E(3Vs**N_<1NxCTZHv>oI4$Ru zmhnn$8q8a~#^-olOZazB%lOt)aXJ>KBXR1DQ)iqu#;KiimF74##HlJy@i@8T2)Lwi zDqK&TL@*w}c5qmEG#F4cPIiqe^~DJ=?Qv@2VG4h#DX@L^ z^u?RoXBE^xzLlQ(-tO^NXhcu*XnM$T&*mMsj_y{pYP9?%;v-im^>#*QtgSPWSd4q)2M$M9h*jd)2L?}bxorIt;9Z!kc~=4 zkvxssr%|g0(K(GG(+Dr8hg=(DKJ+)O09|p_LR~9!E13KX`eKDCB7;pH1IK6$zv{ZK zH|PrT<6^blDa4|J!!6>rx~fJq$4Rv`#wT$*9Aso?b(Ijy*NG0dQ-|9cD$X~E9yhzx zAG}xj!@fT#KX~~py$r)$6W#O^<cy3YCxE&K;y0PWQH0L)+y825Jl~`;l^`a`tuQ*wa_Ez^^H(eEq-!ld2e9yJ50>33{3h zr*5^#UpjJZHl5RIx44=+p z-PG--oo?Fbrj>4Lb5paM8r)RnrnsBjZqm8w^#39J_;O44D#+@ld1+!Ln;0DEQc?6ob$iqqmhychQ@#(1-g6-|XEqIIxAbeM0Y5 zGj;asvsp~7rMr|nbPEUJNeeMis2w_^`;yD9PN7KnoxH=8k2C0Uay*_$UcRoRxX|93 zEObWn7QLl43A3eH((7|wx!#s!uC6tiE4e%^Ntd&$ggQ&8qlDT^sI`RT66!CZJ-h_B zO}r#pLMKb8j{{Ve5N?GMnsAgqwwKTVuL4E=C3LKWx_Dhp{7uH_xcOw5yQM}-9oLlM zfzw|~AK-#?nrU&u1K+ADDkE6O#=7_ggtW2x>KX^k;ZddsafwzJE0kW@{mju1_dfjX z9~~X|&ZAH7KbL#pfcR|~Rqng%#W$3{4+%=M^!3_yMc<)#@4ajAmxlX3#^^pCsD7w> z^}amUGk3rCGZ?k7b~X?FMfWk*xS%jejyWVIdgg@;IW0*c8+%#M-gJJyAh>bwl0(Gg zCzbJP(r0>eQ4bDb7h+?4F0RK!Rh!D#I&3%^B!hBT*$I|9=iLEAeYo+5t;*0}{_&R; zzo;#oZ1}C4t|ytUq3aXMhtHN(DF3OvuKZT{WzB^@SAJT@=(Tp}oPHJN-vs&ca%eAZ z!Rof=`hA#IA12r*+0j0`u{8-s8HwzO0UwR z?9-`3%g3`8roubW)}U~s+-PyT$qyTeE-z$pwI*9E%PnHLg(SgZw}>Xa#i9!a-L1(W zwIqX1ow+rs6I+rxsXs(rA!-j%Q;680v!9ISr!-|)>xPkMbY&eeB*Fe4!=G490#`9d zEXu||R(s%p^m%=>{rHEpI3mjt+HK~+m%;8$QeA{rL^rG{GI)4vX!|L0~Yr&5}RYA)q^s8B1&u7{{U;HOi5I_@WwujCSZ^EZTjJld$ef~T0(Ng=@6abw<-|GOgxrBYK+WU)OBtc zhp_R?#v#F$IrRdTB*lMT~JNK>T8NQc-+vJq7+;h?xWNB(Ig<(>vr`jNJ!ypB4@GozJ?Ik2V=n{29S_W@D2Yu1xD zbj+v^yBbUH(S6SPdhREi-&_?wz0DVR1>H{3&O!9sFMKWkz?+?I^FisAvTXW5(B~2D zPCHnQH%ro4{Z1)cXR|`6HCmnBLFx?B#vru?sX0isLDB_jAV{Zz)ElHng0zx9Y6wzQ zkd6iENRYaNv{Qpu%cDVZ^B2eCfgB{9!z!YV{vh=QsV7JsL2Bm@DuWc|<;Q~56{M!% z2>iI|eC2S&n5}?8L?uEHBMf~SJ*qON%CkL26O%Pbqds*!F|7K_9#O`M6Yo=A8vO4U z`v%`rDRRRm{lw~kA`j*VtHpiyDYY6C2VveV-V&%zjjL`U#??mS;&4$l4EJX zUZBYg$;e1KlF?`~Nm%=wY45p>4qEA;1rD0co%2+mJcENyJLnVcYWIfbuD6J`e{qH$C{_wR7*hO3gX1pS$m& z9R7J-{Am*hVEm8$Xk*SNBh3l1LZpAv5F@mP6iTzzR9!{X<-;+at)<|KV`{MR4=sRk zM{&(j$~w_DX|nU|$C!zW^jU$k3*F>X){T^<41d75=L+j(XAU-nNU&NYDJ#qBlmdS1 zOG97FV@=j6Rkc2s3$;O0jdkAD6!kdv)v29@TK^ zoK8BYF?bv$l%~~P=$lKpTCQ{%4Nj9O$B^T3>jj$wbeSbW7SFLj4tE(XPQe4I)n!?OJ;CvK=}|aKgtVRIM}$89PU;AG9YXLdXB%gj=vM(B`I?YCz z+I?E7*Ct9?_0F#_Ce2E(zMIceT$n9a2(GBb&!(!d$hSR@aAdhj%FBK9P_&>x1x-4WU(-Ass_Nm^ zIqgw87Nz!RYRfTW`52xL&ZM;M#pcE3OZIneHDaj{H?5@Xob3D0>9XD$aWgJ zz^?6pfu(_8xzbzL7aI7+7jq3Q;vSJ#prOR0aposmod?&z^We zMeW?XT7Pn)D8_jj{D*a~VZPjSk^DQSXcn_1kJln(0speHuxZQ}NXf}Xb-@Y234&BV zm!r0G*$ILZ3L!zEe8~WptGc)#)}A4wspUPIj0On|ugPAjt+Zggq>k z=RIKytS0?2w(U^85j5KwmKrRP1^LXgf~oTt67+eWhiXqG2CofXtb4Q}82-gAqfcO+ ze8~MS)ha)I*JwVw1WE&+1IWJmM==Mv!VbCBWyv*I4E}&qFIa6>2)s7QXT#qHYmCj34d*(LQ4V}VBv*@_B6j-1 zPS4qCotS0%&LD38E9X2#(^yfpP^#R{fXq$xA~oQZ!-aza3Y zDg%NL6U=KyTcVjuxoLPh5^P2w`03|WE8WT?)k?!EL{d;4Eh?vPQRTbzqg4^<(Q|F7 z*_k2jJvUE!o@=E9UN7kPVvd4Bx4cyE$qTu1nBO-He+IWzC)p7<;8At^T*=uIqDY9o z2+?yPS{I@jA*u_JEkr{hIvJuKZX1MQPzX^iJ~~6x5u#{_`a=YJMxSQVXyp*Y12U2| z4J%X3w2`SzakD*Fv{M~UfAB)0iTjkPJL%`j+K0qZ`*FfY%|7~%omc?h-Zl9Cex-}6 z|i)6ESE14j7Au@ZPu)8$m~WTN76a6(*{HV zthb5d3;sqqdH^4~IE!!hlKQBUDcsu3vZ>ZM?8u&Fn)QaM409?qo)~=Kgfi+Ov8I>S z(~bM7cZX~yC1g9ZTaq@X z(*zT1Yf_&j^=51oVT-2i8yiKZGj@r(`DRkHo3MzA1AGVP7~jO{`Nl?1CEw)%-1vUJ z!K2>W`Ku`Da}(a_DoU-`x8+Q#6y?0LGa?)PZo?6b?f zW)hTbY@Bx~pNp;V+cpY6mNyth9f@Sh5~aT?wspW45O=L6Ckcq@EOmxyj&zWcskH13ccog@U=ok*s z;myV<8(0HUgZa1>33 zFYMQH4?A{FgdeczLNp@x+J&F5>hs&o-F~Zhz`!Xz9sh=pe;fA$ZqO58;@p)VHKjcN z`!pxu^ZbSFdIC(!U$`T~mpIBRIL7^m)rR1y`f{b7@Dzmt{D{xTl#GF3D3cNRrwvpJ zT{I^$MwE^O70jpi+w_fR_Hj98D(2nE=lx}QJ$zCSGGofp>;9h911m7^>WpA^dU)L2 z3?3@CXuw3TnyAY}ohEAGF?4qR;HZg~^O8CqRTt4F{fmq_$Lu!o^6{v(_1{c;y2rd9 z!xMiKHrZ9{d0PE-!Ev_`yE0QT#ORZVdl$GLi+(UXyg(f$0OT}o!_z_>T-W*K_ zZcz3rdl%q9kSWZ&h4bB^0p)9*eP|wP_sAv@s6@iyBs_h}9Ly>juid#}aFf4$-U`Z{ zw|26m+i~5uDptK+poDe$c{lb%;2aCaias6$?U{v;&>_!s8jT^r7xIN80aH^l;5A?; z&@IhOx*=<$pMo5rjtB`68s8ZqdxZKU)EA+i2;mkFS|cR$67@P7;rPpV0e<+@XhWbX zL#e0(wyBb`kw9xTvR&G-Y{_=GjbpjPIZR4L#iG)&xprlsdDVArAKbQ;DhzGA`riJ- ziP=Y|)4)r;9?PK5{%hTZx#ddF*v{#n{bf-3XAv`QA-)5BoG$$omaSI#B18~)^h>Vj9Zo5zGcs)fm}MqBMLfrM8R{p)WYHlQXOGkNlP7>$i(-} z;DQPtU#^40!h_D#NYh6jxJIe@?1L>`HDy<>R8D{Y>HC+CkB_3i{dq8=oY_~QEIj@q zuuc@FqOG4|K3sf0X1H}`!SA>0?BR&Z-jsBC)M+pR>5O)4){CNVW>WMD5xTUAk9}o? zFzUGP;}arl7p=UUGwGC;s`UUmsk#$5f}ipED1@a49?R%V(L_EUKuJjW=#Zlb8-nlM z^pLXdH^-MQH#|-aE0wR6NXN#Tu1T&?&NW^`fA|NHFShA)ZuyxX`pfC?SgVTy*y9;N~AY}pZpxr;B>87u*lrgk&!g03n68UlI{Jj+Ono4ShF zL`EGRLTqWQs)%$G*WEC7XJzFRvp;<6omXj*^3cNNbpN;L1Lt=hZgQH(MK1gc>A(6? zSul$peX9F~hZ*kGVUG6e_Xh(l%8EY?%&dibfaLJKDLALNr8nO*6p-;8JMCiDP&t zA(I!Yk5bbyf=jF3MVbYHZAhmDN{)kw`>s)qT5y_YMy(Jb{#&)7{$cOlCmy(>esp<$ zWnI;oGjA!nZPJ|4#r4PfU2m`TwC{ej`P^5r^70r~z&T2z^norY6bn^?OpnV$7gSXE zutz^BG)^e7T7{wleK3Ro<;nF@byLz;R+g;~78U5EY$-eHh}JeGqjtxbrlcdUx1PG{ zX=gok)YHm(YOAN_dTOYrs(Omole?aD_0(Ta$Lp!Do_g!4n*(ng39Ld2^>n(PPI2HP zBZ1q#QRnzf(C&KL_>J$@Wz?kttkhs+-o9u(T&n94A4voC@Dk{+r(?X8J-kbuyi4u% z)LKuK^#ouxt=f;~Xsu3UYANL(8S5{&BBdTKThV?uZ0EsYJ5iS-yhSQ616Z276&dZ_{pJJjdjwLvE<#gW7QwO*Z1a> z*74IHfBf~sg)1Y!E7*4NsK!f`ou$>ccRcx`^1`yY^A;|e-zwg~S_Fv8Fj}4lsU?+ebPdVgr2C z=|ixik6L}yI=;zQ+mCF$V&i2ki!e{d1 z=%)kWQQ^pNM?*441)Gg6XtvFICc3w#BY_Z%;<_o=7wiuj zCfb8NLD3$pL_urtSa2X{5OCQZ>_mAV3XEA&kR5rNymkuzyy7bLvwA%Ll=I6*Pw``3 z{DR$@<#9o7w+T5$7@1uj1ERmNHscUbwklG@5=&dxGWVKIQYl&wao!dSHw4WtyZg zMx#Jec8rlp*ekc|aelo4=_FuZlk{k)=eU`Un5oxH-DcWprj2G=X{I(a#m(e4lgM>InzqP|2W2OOq+i9lu46qTj#gONG7Y=QpxJaDo!b8g$ z1$9wlW4bfM_=CX@IdC)`l!qJ^Ltb7?C@H~uZIKdVMqk*J9BnI!XuTIeH1dUBlJVD+B84~nWf=(sqc!J(YP4NP;G*u3379Qai@4ox)Riypl0nm@C@<*IwS0+(D;=W*Pt)Ni}$ zsyTPvtIS@rhFnr>*@UX`W%bIRQ;W)+1K8Zqjh(FBUYQIH`qRvp^Nk81!ykZ0dGHdr zLvXor&2aC}HHN}@SatJkF5Gx?o08dHkCUN|ouN&4m`;W1NSI~I(S>O|Zgz%gWtiH+ z)EuUUFja*q9;P6F+!H3TlgDzkYXQ{9!qlh5bwSlo%QFci<(cno;L3Bq z_I?+Cvz5c(Z(2UourU)|Rp~KpCdt^G`5&_SNU6yEU;U={&dqnNTC;v*2YskGl)rs` z?jQgC>z^F%|LDg@fFh7ZV#?)G55~bK#Dq=q%rK%roW9&_Uv_?h&*_E~;tpD)O-ZXa z6f_2BCUr);1YK0J$pzF=KtchHuPmVc0_x-Utpy}!T-Oy0t3ZspQbrCgYo(NHRjbk5 zK@wOVT#zt0`9Q*jeWTbLQppI82aIm8lI~hUSz_tEm-YVi{kLvfVCYulHDcQx>sC!q zEymi!iY(|%$*Z(8I59& z7>PPUO-XXt1KHV#Uu2rKXw)Mokv@o6?T#pm-X7n?!?$tM%H1jY_yb%=wP0As&J|8r>PPugV7|HkKot4PRP!$q#@ zstURgSsKQwq2<6IrUd!IlK9^9&ous^vV8QkTYmCz^4s4U^MlSC9;qm;UNQ5kE2i(b z26&rwPiM&a>0J%`ZmSB#8m@1=eS75XWfe^gB`CSshy4tn4tmP?p7bRj*=>OztlU_QcnB<^w)R3l))!<2b$ zQi92arnk1Nz2n<;@4sJLIpNBJjqVjUh&#%QPn>8TTsOJiJ~=nS+Y^NS=y%cqwC4f2 zMQ~bmI+N4n%hkKQE)1*JrW5VrtfbZMwZMjH@qh#Mb5>^OTx&dTWX;ki@Furwd=1U- z%HVWSHI<0Qmb9glG4f%91*h^ft5Q`dW+7wtOJESsXys?tGaBD@`_k`HVvX{7)}@E* z25y04l;uD~{M285ZnwYu^5XI27D3j$kFd*PTi}3@L(AnMqiD(zb8Pk;6L!T!n2B^Y zV-{Jn1oE!5Q=6Td?bKkWDm%sP*r?l1JMFa5PAmUs`2PPB z`0=OzpM62sziLn=_r9?64xrs=bcY>+D%#)5ye*i(GD4!kjkb(vpCgx(fAa$K2tc~Y zls=rEKb~3+lf`_C`G;6ORk@>;4n06lGCV-vy2dTt28s_3t`WE4(*t@J;fM1I=E^Pn zw;aya+uVA$$E*5eXdW4`92spXkBqxCj|?vyCmOidgxyOzFP-LI6WHzE>7|WcTFIYc zw>!>VDNxwPeJunp#CYzC(VTXzh-%&x9bRh9*eFN&G46HMrR|}E526qQ!+zA!WMjwO zCe*=84>2E8HcsF0oX9kJ>kG`9-$alSL3Px0!Rdecc&hU9a(eaAlOHwz@x*Rgu+S|o z9o(53y-nh`2OkpG^ATh`lSyWuVhn@AarxT@v(swP2V8DHf~Nc)$)$G~t>)~!P)NS6 zZHg}B4vFzlRj47<7TOr<4jl;{51k5`Sa}?Utf&`-r$VPg+2eI6V_-W`YKU`a_COzE zI$YIkl0H>t-|>O6UU_yIRVxF_Y5yHB9$Qbd zmMULUbvcwlx^TXvw?%7?1gOX{IG3)jj_w4kC-0i%C+Q%{i2hs zI;T05>vF(<9afecn_$FFhmZ#$cR`qDL!=GUms|`!9;P?KbSO+)xd_}Erlv5}hiMEK zf!$#;gpt%POuq`#Pr`ICOx z1fgARkP5$&i~Hzn0~hyC^Yjd&D%N*u;{J><*}`d^>pZ3Frj);t#(p&2N+qvh5=+z6 z`3&nke#PKe7?QTUFncJIPZFA31-X&u_keha9EmEir7($d#Gg&SydoB!2~WxG zqS->o=IYdW+-5Nwi;gkdMiv2>1Q+$~9#VbO1rHtbP>+XBdZ^Pw?H+34zUwHz2tfGg z=a2CW*8}{vkArq-&@=d>^hU0>*ro|4%^K6fJM{;BdK-rvK@=TZo^lzd^TTx`$x zgpAp_A)SZy5TT1mj4=4mdEbH^39k923an^bDm>fnt6xPHySrGtB&;OkLcgXY@O zrDdp z#!J8c;d!`psT?=u2Bk>Es>h;}|ZovK=(^)@M z7SP-9w_rKY-SX>5fQ~fic86@~wDcqAn5D_mWf`#O9Fir+q~=4nBYp~@RgM4O%!mFC z^ANd-v6s{~1<&!sQUhQ=3`sN-A8N@KSw3`WN-81x*_jfQxf&6~oGT&BY1n(&+D_hd;OJeC+S$k-^1=97?5$MRW( za#uce@YrNoD@S3ER*u3-t-Lnf*RBDT*sgaq7VT)(183yIbLY@~aoje=avZ1GemXJGJg1YQ5h`C39@!lJ=wydcs ztG~u~|I&i*w$1wZ#x)-wwtoHB-wm1%>^I~pXP$^>pZi1XqP#NWVe!Dh7jq>~2sCvo z#`-nv0Tl{Q$ji-QG?X6<=Hwd$PuO6xAXcrYST1fY?kJX`sf(`So??SgtlsKs(^u@G zzgQBA``Cv`iXzkc&B>72EIJ&>6vi`&nUF~g9^QqbShzYV<}z~zPcWecm@<~arBo)D zp0_{2({-c-Ql7jNxz$sdDBab{S6d&SwQOSYyHkOGt)!Fo>KY-_j}j)x zkvyRQnRrXfJke*8kP>Ws-8V@dWQHnw2Cl&1HK{`YZn{b3HL6#TtE#5B#>Z?MY|q!@ zHu{XrFRIvBTvJrz=G=#SN9EAkrX^DsF5R$U>C!37X3bhQW$6uDHZ58_Wl2-(jJv1b z^4znxUa@7S*uD9NB~$t1WjC(du#`P+S~6wvqD@!a-E`pATc4f&9VQEjm`???6Bo9~ zr9P)0Nho6^AsA7zWoKm-75e;P33fvSSs{T8qAg4o#K^^Yj^8lL@heBH1%_O=Lq?Dy zk|hM)h-G9^iUy}qOtHX!;HygW9C4Wls9L3B9&VAUj^VdB zR-G6r{Utmxqeoh$e8xhhmCwj$Q@;L0`DYvb^sXcSj_+1XSBh9YT@`J{=M#?4NVws>ED9zI{ct5Z};!?+g1yYP@(q{|B8wc;leC-_)q=our z!USQ8@QB>fSW_?5Ul0~-wz^+bVq7^bCBdaRJVX&EN6I>dG zV9r$)hS)%3RZbs+I9rw894`7Kk+H!fN_{|B8nZ?A~uURyh>x$?@*%i6}>ecQL@ zRIV*;ntRoa@%m-oS`_-uj#$p0Hs%#yHFM167gbf)UU7YW>o!l#wrg(R?tgEYC(zVT z**I>}m{|+!7e5fFzIrj8DyyF`O4r(W;pDLwncLWUTBo~2T#Rhu$bCq^m4^_kYj7I; zxhA--?MNc_KG#-an_^pN(`VUk^V4EK!L@CgpKAS7=BGSAW&7!$e)=yz{l-tf@YBnF zdfHEq_~{;`Ib7?fCCFdnrz`w4(N7hA3i-+Er-eg)`m>)t@Y7q`8;|+vUO(O8r{#W{ zoAC`n{=%T2{_3Zb8vHYUO2aSH;Qf9w@wOnV%5k1^@sOYP`RRV1hGm7H=KBe0SQ`9P z<)Q>>;U^XJLBBkQHuIb<3pC)d9M}neK7prw>DOvUvKBRY%SN8QQB3D| zL2j5={q!8q?uaf&{ABYFtJ)xibnwCXigqT+aE#0#u5Zv+28C&xo@oVuxdZdHQpH=9 z)pA7T_H?g4<62xiuC#Q*gwoP+%A?~-M_n|jxOALue`zf~S!wM>qwvW@T6V4oY=X#J z&0MF)!Fzb(sG7=xngSs=H_MKI<_Y7gqtP*MCX347OqLeeqLFAt6#msk_9B>{qD9dn z3y#qHdD7z2<^4RV)2Rk$zREM#DUci6vfwG*(#>WxrY)Gxcw&r>rL;#Dx42lt)3n8r zwM~o{AZ;6^4%LZJDohk%c~G2{KFTOS6?}-^u5&hBt`Z;NzteFD}Q|Q zNu~b@@s8Jj_ukQ)AJ&a_&04*qgRJ*X^^VrvEhRwP*Mz9K(=7HNSqih$ zH-7t)XD!ji)t6m<^Va6;CRKXMX8riqiio}Rf(e)EXC<#xx@?7$qNP_|IqPakY+4?w zz0ISXkg{{j3dfeh-$E2t2~(wKrDnu1Sm`}^$dqlCtfHio3|50J$D$|wU*~4g7Yl53 zm5na3(O4T5*&v3H(MF%z=w~+i4;wvUqX%uY*+y$@1jyMoYP3;}jfy$gqOWZ9v5k(~ z=ye+jJvQpH(fu~sV55~bYO~QS8`ayW%0_V;Ic+4`=(LSa@fUt#qh1^Bwb4!+ZM4y9 zUVW2|WE+jKQ9d+EA2u(-TXvz{m+Ni-smAugaJ#C|>wN|dQQJIZ`$T^6UlSH

4NdhRX~O6$10jW^P`VuDhKId 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

_qJse~z= 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 { return Err("Cannot add clip instances directly to group layers".to_string()); } - AnyLayer::Raster(_) => { + AnyLayer::Raster(_) | AnyLayer::Text(_) => { return Err("Cannot add clip instances directly to group layers".to_string()); } } @@ -145,8 +145,8 @@ impl Action for AddClipInstanceAction { AnyLayer::Group(_) => { // Group layers don't have clip instances, nothing to rollback } - AnyLayer::Raster(_) => { - // Raster layers don't have clip instances, nothing to rollback + AnyLayer::Raster(_) | AnyLayer::Text(_) => { + // Raster/text layers don't have clip instances, nothing to rollback } } self.executed = false; diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/add_layer.rs b/lightningbeam-ui/lightningbeam-core/src/actions/add_layer.rs index 7eac030..c2f7b79 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/add_layer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/add_layer.rs @@ -138,6 +138,7 @@ impl Action for AddLayerAction { AnyLayer::Effect(_) => "Add effect layer", AnyLayer::Group(_) => "Add group layer", AnyLayer::Raster(_) => "Add raster layer", + AnyLayer::Text(_) => "Add text layer", } .to_string() } diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/create_text_clip.rs b/lightningbeam-ui/lightningbeam-core/src/actions/create_text_clip.rs new file mode 100644 index 0000000..3df1f4c --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/actions/create_text_clip.rs @@ -0,0 +1,133 @@ +//! Create-text-clip action +//! +//! Used by the text tool when the active layer is a **vector** layer: it creates a +//! VectorClip containing a single text layer, registers it, and places a clip +//! instance in the parent vector layer. The editor then enters that clip so the +//! text is directly editable. All of this is one undoable step so undo never +//! leaves an orphan clip. + +use crate::action::Action; +use crate::clip::{ClipInstance, VectorClip}; +use crate::document::Document; +use crate::layer::AnyLayer; +use crate::text_layer::TextLayer; +use uuid::Uuid; + +/// Minimum clip duration (seconds) when the document has no duration set yet. +const FALLBACK_DURATION: f64 = 10.0; + +pub struct CreateTextClipAction { + /// The vector layer to place the clip instance in. + parent_layer_id: Uuid, + /// The text layer to embed (its id is stable across redo). + text_layer: TextLayer, + /// Instance position in the parent layer's space. + position: (f64, f64), + + // Assigned on first execute; reused on redo so ids are stable. + clip_id: Option, + instance_id: Option, + executed: bool, +} + +impl CreateTextClipAction { + pub fn new(parent_layer_id: Uuid, text_layer: TextLayer, position: (f64, f64)) -> Self { + Self { + parent_layer_id, + text_layer, + position, + clip_id: None, + instance_id: None, + executed: false, + } + } + + /// Preset the clip + instance ids (so the caller knows them up-front for + /// entering the clip immediately after execute). Ids stay stable across redo. + pub fn with_ids(mut self, clip_id: Uuid, instance_id: Uuid) -> Self { + self.clip_id = Some(clip_id); + self.instance_id = Some(instance_id); + self + } + + /// The vector clip id (after execute). + pub fn clip_id(&self) -> Option { + self.clip_id + } + + /// The clip instance id placed in the parent layer (after execute). + pub fn instance_id(&self) -> Option { + self.instance_id + } + + /// The embedded text layer's id. + pub fn text_layer_id(&self) -> Uuid { + self.text_layer.layer.id + } +} + +impl Action for CreateTextClipAction { + fn execute(&mut self, document: &mut Document) -> Result<(), String> { + let clip_id = self.clip_id.unwrap_or_else(Uuid::new_v4); + self.clip_id = Some(clip_id); + let instance_id = self.instance_id.unwrap_or_else(Uuid::new_v4); + self.instance_id = Some(instance_id); + + let duration = if document.duration > 0.0 { document.duration } else { FALLBACK_DURATION }; + + // Build the clip with the text layer as its single root layer. + let mut clip = VectorClip::with_id( + clip_id, + "Text", + self.text_layer.box_width.max(1.0), + self.text_layer.box_height.max(1.0), + duration, + ); + // A movie clip (not keyframe-gated) so the text persists across the timeline. + clip.is_group = false; + clip.layers.add_root(AnyLayer::Text(self.text_layer.clone())); + // Registers the clip and its layers in layer_to_clip_map for O(1) lookup. + document.add_vector_clip(clip); + + // Place an instance of the clip in the parent vector layer. + let layer = document + .get_layer_mut(&self.parent_layer_id) + .ok_or_else(|| format!("Parent layer {} not found", self.parent_layer_id))?; + let AnyLayer::Vector(vector_layer) = layer else { + return Err("Text clip can only be created inside a vector layer".to_string()); + }; + let mut instance = ClipInstance::with_id(instance_id, clip_id); + instance.transform.x = self.position.0; + instance.transform.y = self.position.1; + vector_layer.clip_instances.push(instance); + + self.executed = true; + Ok(()) + } + + fn rollback(&mut self, document: &mut Document) -> Result<(), String> { + if !self.executed { + return Ok(()); + } + // Remove the clip instance from the parent layer. + if let (Some(instance_id), Some(AnyLayer::Vector(vector_layer))) = + (self.instance_id, document.get_layer_mut(&self.parent_layer_id)) + { + vector_layer.clip_instances.retain(|ci| ci.id != instance_id); + } + // Remove the clip and its layer_to_clip_map registrations. + if let Some(clip_id) = self.clip_id { + if let Some(clip) = document.vector_clips.remove(&clip_id) { + for node in &clip.layers.roots { + document.layer_to_clip_map.remove(&node.data.id()); + } + } + } + self.executed = false; + Ok(()) + } + + fn description(&self) -> String { + "Add text".to_string() + } +} diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/loop_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/loop_clip_instances.rs index 9d8d79e..01fd71e 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/loop_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/loop_clip_instances.rs @@ -37,6 +37,7 @@ impl Action for LoopClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; for (instance_id, _old_dur, new_dur, _old_lb, new_lb) in loops { @@ -61,6 +62,7 @@ impl Action for LoopClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; for (instance_id, old_dur, _new_dur, old_lb, _new_lb) in loops { diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs b/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs index 29db4f5..81a36f6 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs @@ -44,6 +44,9 @@ pub mod resize_raster_layer; pub mod move_layer; pub mod set_fill_paint; pub mod set_image_fill; +pub mod create_text_clip; +pub mod set_text_content; +pub mod resize_text_box; pub use add_clip_instance::AddClipInstanceAction; pub use add_effect::AddEffectAction; @@ -84,3 +87,6 @@ pub use set_fill_paint::SetFillPaintAction; pub use set_image_fill::SetImageFillAction; pub use change_bpm::ChangeBpmAction; pub use change_fps::ChangeFpsAction; +pub use create_text_clip::CreateTextClipAction; +pub use set_text_content::SetTextContentAction; +pub use resize_text_box::ResizeTextBoxAction; diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs index fba7ee7..dd70075 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs @@ -58,6 +58,7 @@ impl Action for MoveClipInstancesAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; if let Some(instance) = clip_instances.iter().find(|ci| ci.id == *member_instance_id) { @@ -97,6 +98,7 @@ impl Action for MoveClipInstancesAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; let group: Vec<(Uuid, f64, f64)> = moves.iter().filter_map(|(id, old_start, _)| { @@ -132,6 +134,7 @@ impl Action for MoveClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; // Update timeline_start for each clip instance @@ -159,6 +162,7 @@ impl Action for MoveClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; // Restore original timeline_start for each clip instance diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/remove_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/remove_clip_instances.rs index afadffb..a043c7b 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/remove_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/remove_clip_instances.rs @@ -46,6 +46,7 @@ impl Action for RemoveClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; // Find and remove the instance, saving it for rollback @@ -72,6 +73,7 @@ impl Action for RemoveClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; clip_instances.push(instance); diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/resize_text_box.rs b/lightningbeam-ui/lightningbeam-core/src/actions/resize_text_box.rs new file mode 100644 index 0000000..abe3d51 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/actions/resize_text_box.rs @@ -0,0 +1,59 @@ +//! Resize-text-box action +//! +//! Updates a text layer's box origin and dimensions (which changes the wrap +//! width, so parley re-wraps the text). One undoable step. + +use crate::action::Action; +use crate::document::Document; +use crate::layer::AnyLayer; +use kurbo::Point; +use uuid::Uuid; + +pub struct ResizeTextBoxAction { + layer_id: Uuid, + new_origin: Point, + new_width: f64, + new_height: f64, + old: Option<(Point, f64, f64)>, +} + +impl ResizeTextBoxAction { + pub fn new(layer_id: Uuid, new_origin: Point, new_width: f64, new_height: f64) -> Self { + Self { layer_id, new_origin, new_width, new_height, old: None } + } +} + +impl Action for ResizeTextBoxAction { + fn execute(&mut self, document: &mut Document) -> Result<(), String> { + let layer = document + .get_layer_mut(&self.layer_id) + .ok_or_else(|| format!("Layer {} not found", self.layer_id))?; + let AnyLayer::Text(text_layer) = layer else { + return Err("ResizeTextBoxAction target is not a text layer".to_string()); + }; + if self.old.is_none() { + self.old = Some((text_layer.box_origin, text_layer.box_width, text_layer.box_height)); + } + text_layer.box_origin = self.new_origin; + text_layer.box_width = self.new_width.max(1.0); + text_layer.box_height = self.new_height.max(1.0); + Ok(()) + } + + fn rollback(&mut self, document: &mut Document) -> Result<(), String> { + let Some((origin, w, h)) = self.old else { return Ok(()) }; + let layer = document + .get_layer_mut(&self.layer_id) + .ok_or_else(|| format!("Layer {} not found", self.layer_id))?; + if let AnyLayer::Text(text_layer) = layer { + text_layer.box_origin = origin; + text_layer.box_width = w; + text_layer.box_height = h; + } + Ok(()) + } + + fn description(&self) -> String { + "Resize text box".to_string() + } +} diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/set_text_content.rs b/lightningbeam-ui/lightningbeam-core/src/actions/set_text_content.rs new file mode 100644 index 0000000..cb9c991 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/actions/set_text_content.rs @@ -0,0 +1,61 @@ +//! Set-text-content action +//! +//! Replaces a text layer's [`TextContent`] (text string, font size, color, family, +//! alignment) as one undoable step. Used both by in-place editing (text changes) +//! and the info panel (style changes). + +use crate::action::Action; +use crate::document::Document; +use crate::layer::AnyLayer; +use crate::text_layer::TextContent; +use uuid::Uuid; + +pub struct SetTextContentAction { + layer_id: Uuid, + new: TextContent, + old: Option, +} + +impl SetTextContentAction { + pub fn new(layer_id: Uuid, new: TextContent) -> Self { + Self { layer_id, new, old: None } + } + + /// Construct with an explicit `old` value. Used by in-place editing, which mutates + /// the document live for preview and then records one undoable step capturing the + /// content as it was *before* editing began. + pub fn with_old(layer_id: Uuid, old: TextContent, new: TextContent) -> Self { + Self { layer_id, new, old: Some(old) } + } +} + +impl Action for SetTextContentAction { + fn execute(&mut self, document: &mut Document) -> Result<(), String> { + let layer = document + .get_layer_mut(&self.layer_id) + .ok_or_else(|| format!("Layer {} not found", self.layer_id))?; + let AnyLayer::Text(text_layer) = layer else { + return Err("SetTextContentAction target is not a text layer".to_string()); + }; + if self.old.is_none() { + self.old = Some(text_layer.content.clone()); + } + text_layer.content = self.new.clone(); + Ok(()) + } + + fn rollback(&mut self, document: &mut Document) -> Result<(), String> { + let Some(old) = self.old.clone() else { return Ok(()) }; + let layer = document + .get_layer_mut(&self.layer_id) + .ok_or_else(|| format!("Layer {} not found", self.layer_id))?; + if let AnyLayer::Text(text_layer) = layer { + text_layer.content = old; + } + Ok(()) + } + + fn description(&self) -> String { + "Edit text".to_string() + } +} diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/split_clip_instance.rs b/lightningbeam-ui/lightningbeam-core/src/actions/split_clip_instance.rs index f5de9a4..5e2101d 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/split_clip_instance.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/split_clip_instance.rs @@ -114,6 +114,7 @@ impl Action for SplitClipInstanceAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => return Err("Cannot split clip instances on group layers".to_string()), AnyLayer::Raster(_) => return Err("Cannot split clip instances on group layers".to_string()), + AnyLayer::Text(_) => return Err("Cannot split clip instances on group layers".to_string()), }; let instance = clip_instances @@ -233,7 +234,7 @@ impl Action for SplitClipInstanceAction { AnyLayer::Group(_) => { return Err("Cannot split clip instances on group layers".to_string()); } - AnyLayer::Raster(_) => { + AnyLayer::Raster(_) | AnyLayer::Text(_) => { return Err("Cannot split clip instances on group layers".to_string()); } } @@ -294,8 +295,8 @@ impl Action for SplitClipInstanceAction { AnyLayer::Group(_) => { // Group layers don't have clip instances, nothing to rollback } - AnyLayer::Raster(_) => { - // Raster layers don't have clip instances, nothing to rollback + AnyLayer::Raster(_) | AnyLayer::Text(_) => { + // Raster/text layers don't have clip instances, nothing to rollback } } diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/transform_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/transform_clip_instances.rs index 433e6dc..e8d21b9 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/transform_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/transform_clip_instances.rs @@ -101,6 +101,7 @@ impl Action for TransformClipInstancesAction { AnyLayer::Effect(_) => {} AnyLayer::Group(_) => {} AnyLayer::Raster(_) => {} + AnyLayer::Text(_) => {} } Ok(()) } @@ -140,6 +141,7 @@ impl Action for TransformClipInstancesAction { AnyLayer::Effect(_) => {} AnyLayer::Group(_) => {} AnyLayer::Raster(_) => {} + AnyLayer::Text(_) => {} } Ok(()) } diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs index 9595ba3..60c1bd7 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs @@ -101,6 +101,7 @@ impl Action for TrimClipInstancesAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; if let Some(instance) = clip_instances.iter().find(|ci| ci.id == *member_instance_id) { @@ -138,6 +139,7 @@ impl Action for TrimClipInstancesAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; if let Some(instance) = clip_instances.iter().find(|ci| ci.id == *member_instance_id) { @@ -182,6 +184,7 @@ impl Action for TrimClipInstancesAction { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; let instance = clip_instances.iter() @@ -275,6 +278,7 @@ impl Action for TrimClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; // Apply trims @@ -315,6 +319,7 @@ impl Action for TrimClipInstancesAction { AnyLayer::Effect(el) => &mut el.clip_instances, AnyLayer::Group(_) => continue, AnyLayer::Raster(_) => continue, + AnyLayer::Text(_) => continue, }; // Restore original trim values diff --git a/lightningbeam-ui/lightningbeam-core/src/beam_archive.rs b/lightningbeam-ui/lightningbeam-core/src/beam_archive.rs index 770b544..6300df3 100644 --- a/lightningbeam-ui/lightningbeam-core/src/beam_archive.rs +++ b/lightningbeam-ui/lightningbeam-core/src/beam_archive.rs @@ -62,6 +62,10 @@ pub enum MediaKind { /// the keyframe id). Decoded eagerly on load and shown while the full-res pixels /// page in, so cold scrubs don't flash blank. See `raster_proxy_media_id`. RasterProxy = 6, + /// An embedded font file (TTF/OTF) used by a text layer (keyed by a content hash + /// of the bytes so identical fonts dedupe). Registered into the font collection + /// on load so documents render faithfully on machines lacking the font. + Font = 7, } impl MediaKind { @@ -74,6 +78,7 @@ impl MediaKind { 4 => Some(Self::Waveform), 5 => Some(Self::Thumbnail), 6 => Some(Self::RasterProxy), + 7 => Some(Self::Font), _ => None, } } diff --git a/lightningbeam-ui/lightningbeam-core/src/clip.rs b/lightningbeam-ui/lightningbeam-core/src/clip.rs index f483380..17a306a 100644 --- a/lightningbeam-ui/lightningbeam-core/src/clip.rs +++ b/lightningbeam-ui/lightningbeam-core/src/clip.rs @@ -122,6 +122,7 @@ impl VectorClip { AnyLayer::Effect(el) => &el.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; for ci in clip_instances { // Compute end position of this clip instance in beats @@ -223,6 +224,17 @@ impl VectorClip { Some(existing) => existing.union(transformed_bounds), }); } + } else if let AnyLayer::Text(text_layer) = &layer_node.data { + // Text layers contribute their box bounds (so a text-only clip is + // selectable/draggable, not a degenerate point). + let r = Rect::from_origin_size( + text_layer.box_origin, + (text_layer.box_width, text_layer.box_height), + ); + combined_bounds = Some(match combined_bounds { + None => r, + Some(existing) => existing.union(r), + }); } } diff --git a/lightningbeam-ui/lightningbeam-core/src/clipboard.rs b/lightningbeam-ui/lightningbeam-core/src/clipboard.rs index 4355754..a9ea32b 100644 --- a/lightningbeam-ui/lightningbeam-core/src/clipboard.rs +++ b/lightningbeam-ui/lightningbeam-core/src/clipboard.rs @@ -62,6 +62,7 @@ impl ClipboardLayerType { AnyLayer::Effect(_) => ClipboardLayerType::Effect, AnyLayer::Group(_) => ClipboardLayerType::Vector, AnyLayer::Raster(_) => ClipboardLayerType::Vector, + AnyLayer::Text(_) => ClipboardLayerType::Vector, } } @@ -449,6 +450,13 @@ fn regen_any_layer(layer: &AnyLayer, id_map: &mut HashMap) -> AnyLay nl.layer.id = new_layer_id; AnyLayer::Raster(nl) } + AnyLayer::Text(tl) => { + let new_layer_id = Uuid::new_v4(); + id_map.insert(tl.layer.id, new_layer_id); + let mut nl = tl.clone(); + nl.layer.id = new_layer_id; + AnyLayer::Text(nl) + } AnyLayer::Group(gl) => { let new_layer_id = Uuid::new_v4(); id_map.insert(gl.layer.id, new_layer_id); diff --git a/lightningbeam-ui/lightningbeam-core/src/document.rs b/lightningbeam-ui/lightningbeam-core/src/document.rs index 62eba1c..a27b405 100644 --- a/lightningbeam-ui/lightningbeam-core/src/document.rs +++ b/lightningbeam-ui/lightningbeam-core/src/document.rs @@ -489,8 +489,8 @@ impl Document { } } } - crate::layer::AnyLayer::Raster(_) => { - // Raster layers don't have clip instances + crate::layer::AnyLayer::Raster(_) | crate::layer::AnyLayer::Text(_) => { + // Raster and text layers don't have clip instances } crate::layer::AnyLayer::Group(group) => { // Recurse into group children to find their clip instance endpoints @@ -530,8 +530,8 @@ impl Document { } } } - crate::layer::AnyLayer::Raster(_) => { - // Raster layers don't have clip instances + crate::layer::AnyLayer::Raster(_) | crate::layer::AnyLayer::Text(_) => { + // Raster and text layers don't have clip instances } crate::layer::AnyLayer::Group(g) => { process_group_children(&g.children, doc, max_end, calc_end); @@ -937,6 +937,7 @@ impl Document { AnyLayer::Effect(effect) => &effect.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; let instance = instances.iter().find(|inst| &inst.id == instance_id)?; @@ -977,6 +978,7 @@ impl Document { AnyLayer::Effect(effect) => &effect.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; for instance in instances { @@ -1039,6 +1041,7 @@ impl Document { AnyLayer::Vector(_) => return Some(desired_start), // Shouldn't reach here AnyLayer::Group(_) => return Some(desired_start), // Groups don't have own clips AnyLayer::Raster(_) => return Some(desired_start), // Raster layers don't have own clips + AnyLayer::Text(_) => return Some(desired_start), // Text layers don't have own clips }; let mut occupied_ranges: Vec<(f64, f64, Uuid)> = Vec::new(); @@ -1134,6 +1137,7 @@ impl Document { AnyLayer::Vector(v) => &v.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; // Collect non-group clip ranges @@ -1205,6 +1209,7 @@ impl Document { AnyLayer::Vector(vector) => &vector.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; for other in instances { @@ -1253,6 +1258,7 @@ impl Document { AnyLayer::Vector(vector) => &vector.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; let mut nearest_start = f64::MAX; @@ -1300,6 +1306,7 @@ impl Document { AnyLayer::Vector(vector) => &vector.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; let mut nearest_end = 0.0; diff --git a/lightningbeam-ui/lightningbeam-core/src/file_io.rs b/lightningbeam-ui/lightningbeam-core/src/file_io.rs index 4df18b7..98129ee 100644 --- a/lightningbeam-ui/lightningbeam-core/src/file_io.rs +++ b/lightningbeam-ui/lightningbeam-core/src/file_io.rs @@ -242,6 +242,21 @@ fn thumbnail_media_id(clip_id: Uuid) -> Uuid { Uuid::from_u128(clip_id.as_u128() ^ SENTINEL) } +/// Content-hash id for an embedded font row, so identical font files dedupe to one +/// row regardless of which / how many text layers use them. A 128-bit id is built +/// from two salted 64-bit hashes of the bytes. +fn font_media_id(bytes: &[u8]) -> Uuid { + use std::hash::{Hash, Hasher}; + let mut h1 = std::collections::hash_map::DefaultHasher::new(); + bytes.hash(&mut h1); + let mut h2 = std::collections::hash_map::DefaultHasher::new(); + 0xF0E1_D2C3_B4A5_9687u64.hash(&mut h2); + bytes.hash(&mut h2); + let hi = h1.finish() as u128; + let lo = h2.finish() as u128; + Uuid::from_u128((hi << 64) | lo) +} + /// Derived id for a raster keyframe's low-res proxy row (distinct from the keyframe's /// own full-res `Raster` row, which is keyed by the raw keyframe id). fn raster_proxy_media_id(kf_id: Uuid) -> Uuid { @@ -535,6 +550,32 @@ pub fn save_beam( } let _ = image_count; + // --- embedded fonts -> media rows (Font), keyed by content hash so identical + // fonts dedupe. Embed every non-bundled family used by a text layer so the + // document renders faithfully on machines lacking the font. The bundled + // three ship with the app and are never embedded. --- + { + use crate::layer::AnyLayer; + let mut families: HashSet = HashSet::new(); + for layer in document.all_layers() { + if let AnyLayer::Text(tl) = layer { + let fam = &tl.content.font_family; + if !fam.is_empty() && !crate::fonts::is_bundled(fam) { + families.insert(fam.clone()); + } + } + } + for fam in families { + if let Some(bytes) = crate::fonts::family_font_bytes(&fam) { + let id = font_media_id(&bytes); + if !txn.media_exists(id)? { + txn.put_media_packed(id, MediaKind::Font, &fam, &bytes, MediaMeta::default())?; + } + live_media.insert(id); + } + } + } + // --- orphan cleanup: drop media for removed clips/keyframes --- let removed = txn.retain_media(&live_media)?; @@ -598,6 +639,17 @@ fn load_beam_sqlite(path: &Path) -> Result { eprintln!("📊 [LOAD_BEAM] Starting load_beam() (SQLite container)..."); let archive = BeamArchive::open(path)?; + + // Register document-embedded fonts into the runtime font collection so text + // layers referencing them render faithfully even if the host lacks the font. + if let Ok(font_ids) = archive.media_ids_of_kind(MediaKind::Font) { + for id in font_ids { + if let Ok(bytes) = archive.read_media_full(id) { + crate::fonts::register_embedded(bytes); + } + } + } + let json = archive.get_project_json()?; let beam_project: BeamProject = serde_json::from_str(&json) .map_err(|e| format!("Failed to deserialize project.json: {}", e))?; diff --git a/lightningbeam-ui/lightningbeam-core/src/fonts.rs b/lightningbeam-ui/lightningbeam-core/src/fonts.rs new file mode 100644 index 0000000..40f2e80 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/fonts.rs @@ -0,0 +1,296 @@ +//! Font registry + text layout for text layers. +//! +//! Wraps a thread-local parley [`FontContext`]/[`LayoutContext`]. Three fonts are +//! bundled (serif, sans-serif, monospaced) so text renders deterministically even +//! offline; system fonts are also enumerated (parley's fontique source) and +//! document-embedded fonts can be registered at load time (see `file_io`). +//! +//! The same `linebender_resource_handle` crate (0.1.1) backs both vello's +//! `peniko::FontData` and parley's `FontData`, so a glyph run's `run.font()` can be +//! handed straight to `vello::Scene::draw_glyphs` with no conversion. + +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +use parley::{ + Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, Layout, LayoutContext, + PositionedLayoutItem, StyleProperty, +}; +use vello::peniko::Blob; + +use crate::text_layer::{TextAlign, TextContent}; + +// ── Bundled fonts (SIL OFL; vendored under assets/fonts) ───────────────────── +static BUNDLED_SANS: &[u8] = include_bytes!("../assets/fonts/LiberationSans-Regular.ttf"); +static BUNDLED_SERIF: &[u8] = include_bytes!("../assets/fonts/LiberationSerif-Regular.ttf"); +static BUNDLED_MONO: &[u8] = include_bytes!("../assets/fonts/LiberationMono-Regular.ttf"); + +/// Parley requires a brush type, but glyph color is applied by vello at draw time, +/// so we use a zero-sized placeholder. +#[derive(Clone, Copy, PartialEq, Default, Debug)] +pub struct NoBrush; + +struct FontStore { + fcx: FontContext, + lcx: LayoutContext, + /// Registered family names of the three bundled fonts (sans, serif, mono). + bundled: Vec, + /// The default family (bundled sans-serif), used when `font_family` is empty. + default_family: String, +} + +impl FontStore { + fn new() -> Self { + let mut fcx = FontContext::new(); + let mut bundled = Vec::new(); + for bytes in [BUNDLED_SANS, BUNDLED_SERIF, BUNDLED_MONO] { + let blob = Blob::new(Arc::new(bytes) as Arc + Send + Sync>); + for (family_id, _) in fcx.collection.register_fonts(blob, None) { + if let Some(name) = fcx.collection.family_name(family_id) { + let name = name.to_string(); + if !bundled.contains(&name) { + bundled.push(name); + } + } + } + } + let default_family = bundled.first().cloned().unwrap_or_else(|| "sans-serif".to_string()); + Self { fcx, lcx: LayoutContext::new(), bundled, default_family } + } +} + +thread_local! { + static STORE: RefCell = RefCell::new(FontStore::new()); +} + +/// The default family name (bundled sans-serif), used when a text layer's +/// `font_family` is empty. +pub fn default_family() -> String { + STORE.with(|s| s.borrow().default_family.clone()) +} + +/// Whether `family` is one of the three bundled families (which therefore must +/// never be embedded into a `.beam`). +pub fn is_bundled(family: &str) -> bool { + STORE.with(|s| s.borrow().bundled.iter().any(|f| f == family)) +} + +/// True if a shorter word-prefix of `name` is itself a family in `set` — i.e. `name` +/// is a variant of a base family (e.g. "Noto Sans Arabic" when "Noto Sans" exists). +/// Used to collapse the many script/region-specific fonts (mostly Noto) into their +/// base family in the picker; parley's fallback still resolves the right script font +/// automatically when rendering non-Latin text. +fn is_variant_of_listed_base(name: &str, set: &std::collections::HashSet<&str>) -> bool { + let words: Vec<&str> = name.split(' ').collect(); + for k in 1..words.len() { + if set.contains(words[..k].join(" ").as_str()) { + return true; + } + } + false +} + +/// Available family names for the info-panel picker: the three bundled families first, +/// then a consolidated, alphabetical list of system families — script/region variants +/// whose base family is present are dropped (monospace variants are kept). +pub fn families() -> Vec { + STORE.with(|s| { + let s = &mut *s.borrow_mut(); + + let mut system: Vec = + s.fcx.collection.family_names().map(|n| n.to_string()).collect(); + system.sort(); + system.dedup(); + let set: std::collections::HashSet<&str> = system.iter().map(|x| x.as_str()).collect(); + + let mut out = s.bundled.clone(); + for name in &system { + if out.iter().any(|b| b == name) { + continue; + } + // Keep base families and monospace variants; drop script/region variants + // whose base family is already in the list. + if name.contains("Mono") || !is_variant_of_listed_base(name, &set) { + out.push(name.clone()); + } + } + out + }) +} + +/// Register a document-embedded font (raw TTF/OTF bytes) into the runtime font +/// collection so text layers referencing its family resolve to it. Returns the +/// registered family names. +pub fn register_embedded(bytes: Vec) -> Vec { + STORE.with(|s| { + let s = &mut *s.borrow_mut(); + let blob = Blob::new(Arc::new(bytes) as Arc + Send + Sync>); + let mut names = Vec::new(); + for (family_id, _) in s.fcx.collection.register_fonts(blob, None) { + if let Some(name) = s.fcx.collection.family_name(family_id) { + names.push(name.to_string()); + } + } + names + }) +} + +/// Whether `family` currently resolves to a registered family (bundled, system, +/// or embedded). Used to flag "missing font" on load. +pub fn family_available(family: &str) -> bool { + if family.is_empty() { + return true; + } + STORE.with(|s| { + let s = &mut *s.borrow_mut(); + s.fcx.collection.family_id(family).is_some() + }) +} + +/// The raw bytes of the font file that `family` resolves to (for embedding into a +/// `.beam`). Returns `None` if the family resolves to no glyphs. Lays out a single +/// glyph and reads the resolved run's font blob (the same `linebender_resource_handle` +/// blob vello uses). +pub fn family_font_bytes(family: &str) -> Option> { + let content = TextContent { + text: "A".to_string(), + font_family: family.to_string(), + ..TextContent::default() + }; + with_layout(&content, 10_000.0, |layout| { + for line in layout.lines() { + for item in line.items() { + if let PositionedLayoutItem::GlyphRun(run) = item { + return Some(run.run().font().data.data().to_vec()); + } + } + } + None + }) +} + +// ── Background font preloading ─────────────────────────────────────────────── +// +// Loading a family's bytes (`family_font_bytes`) is the expensive part (font file IO + +// shaping). Doing it for the whole picker on the UI thread causes hitches, so a +// background thread (started at app launch) loads every picker family's bytes ahead of +// time into a shared map. The UI then only has to *register* them with its renderer +// (cheap), and synchronously loads the few stragglers only if needed before they're ready. + +struct PreloadState { + loaded: HashMap>, + started: bool, + done: bool, +} + +fn preload() -> &'static Mutex { + static P: OnceLock> = OnceLock::new(); + P.get_or_init(|| Mutex::new(PreloadState { + loaded: HashMap::new(), + started: false, + done: false, + })) +} + +/// Start (once) a background thread that loads every picker font's bytes. Idempotent; +/// safe to call every frame or from app startup. +pub fn start_preload() { + { + let mut p = preload().lock().unwrap(); + if p.started { + return; + } + p.started = true; + } + let _ = std::thread::Builder::new() + .name("font-preload".into()) + .spawn(|| { + // This thread gets its own thread-local FontContext (enumerates system fonts + // off the UI thread). Bytes are plain `Vec` (Send) handed back via the map. + for fam in families() { + if let Some(bytes) = family_font_bytes(&fam) { + preload().lock().unwrap().loaded.insert(fam, bytes); + } + } + preload().lock().unwrap().done = true; + }); +} + +/// Remove and return a preloaded font's bytes if the background thread has them ready. +pub fn take_preloaded(family: &str) -> Option> { + preload().lock().unwrap().loaded.remove(family) +} + +/// Whether the background preloader has finished loading every family. +pub fn preload_done() -> bool { + preload().lock().unwrap().done +} + +/// Caret rectangle (x0, y0, x1, y1, in layout space relative to the box origin) for +/// `byte_index` into `content.text`, wrapped to `max_width`. +pub fn caret_geometry(content: &TextContent, max_width: f32, byte_index: usize) -> Option<(f64, f64, f64, f64)> { + let caret_w = (content.font_size as f32 * 0.06).max(1.0); + with_layout(content, max_width, |layout| { + let cur = parley::Cursor::from_byte_index(layout, byte_index, parley::Affinity::Downstream); + let bb = cur.geometry(layout, caret_w); + Some((bb.x0, bb.y0, bb.x1, bb.y1)) + }) +} + +/// Selection highlight rectangles (each x0, y0, x1, y1 in layout space) for the byte +/// range `[start, end)` into `content.text`, wrapped to `max_width`. +pub fn selection_geometry(content: &TextContent, max_width: f32, start: usize, end: usize) -> Vec<(f64, f64, f64, f64)> { + if start == end { + return Vec::new(); + } + with_layout(content, max_width, |layout| { + let anchor = parley::Cursor::from_byte_index(layout, start, parley::Affinity::Downstream); + let focus = parley::Cursor::from_byte_index(layout, end, parley::Affinity::Downstream); + let sel = parley::Selection::new(anchor, focus); + sel.geometry(layout) + .into_iter() + .map(|(bb, _)| (bb.x0, bb.y0, bb.x1, bb.y1)) + .collect() + }) +} + +fn alignment_of(a: TextAlign) -> Alignment { + match a { + TextAlign::Left => Alignment::Left, + TextAlign::Center => Alignment::Center, + TextAlign::Right => Alignment::Right, + TextAlign::Justify => Alignment::Justify, + } +} + +/// Build a parley layout for `content` wrapped to `max_width` (document units), +/// then invoke `f` with it. The layout is rebuilt on each call (v1: no cache). +pub fn with_layout( + content: &TextContent, + max_width: f32, + f: impl FnOnce(&Layout) -> R, +) -> R { + STORE.with(|s| { + let s = &mut *s.borrow_mut(); + let default_family = s.default_family.clone(); + let FontStore { fcx, lcx, .. } = s; + + let mut builder = lcx.ranged_builder(fcx, &content.text, 1.0, true); + builder.push_default(StyleProperty::FontSize(content.font_size as f32)); + + let family_name = if content.font_family.is_empty() { + default_family + } else { + content.font_family.clone() + }; + let family = FontFamily::Single(FontFamilyName::Named(Cow::Owned(family_name))); + builder.push_default(StyleProperty::FontFamily(family)); + + let mut layout = builder.build(&content.text); + layout.break_all_lines(Some(max_width)); + layout.align(alignment_of(content.align), AlignmentOptions::default()); + f(&layout) + }) +} diff --git a/lightningbeam-ui/lightningbeam-core/src/layer.rs b/lightningbeam-ui/lightningbeam-core/src/layer.rs index ef845c8..005e999 100644 --- a/lightningbeam-ui/lightningbeam-core/src/layer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/layer.rs @@ -9,6 +9,7 @@ use crate::vector_graph::VectorGraph; use crate::effect_layer::EffectLayer; use crate::object::ShapeInstance; use crate::raster_layer::RasterLayer; +use crate::text_layer::TextLayer; use crate::shape::Shape; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -31,6 +32,8 @@ pub enum LayerType { Group, /// Raster pixel-buffer painting layer Raster, + /// Text layer (a single editable text box) + Text, } /// Common trait for all layer types @@ -866,6 +869,7 @@ impl GroupLayer { AnyLayer::Effect(l) => &l.clip_instances, AnyLayer::Group(_) => &[], // no nested groups AnyLayer::Raster(_) => &[], // raster layers have no clip instances + AnyLayer::Text(_) => &[], // raster layers have no clip instances }; for ci in instances { result.push((child_id, ci)); @@ -884,6 +888,7 @@ pub enum AnyLayer { Effect(EffectLayer), Group(GroupLayer), Raster(RasterLayer), + Text(TextLayer), } impl LayerTrait for AnyLayer { @@ -895,6 +900,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.id(), AnyLayer::Group(l) => l.id(), AnyLayer::Raster(l) => l.id(), + AnyLayer::Text(l) => l.id(), } } @@ -906,6 +912,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.name(), AnyLayer::Group(l) => l.name(), AnyLayer::Raster(l) => l.name(), + AnyLayer::Text(l) => l.name(), } } @@ -917,6 +924,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_name(name), AnyLayer::Group(l) => l.set_name(name), AnyLayer::Raster(l) => l.set_name(name), + AnyLayer::Text(l) => l.set_name(name), } } @@ -928,6 +936,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.has_custom_name(), AnyLayer::Group(l) => l.has_custom_name(), AnyLayer::Raster(l) => l.has_custom_name(), + AnyLayer::Text(l) => l.has_custom_name(), } } @@ -939,6 +948,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_has_custom_name(custom), AnyLayer::Group(l) => l.set_has_custom_name(custom), AnyLayer::Raster(l) => l.set_has_custom_name(custom), + AnyLayer::Text(l) => l.set_has_custom_name(custom), } } @@ -950,6 +960,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.visible(), AnyLayer::Group(l) => l.visible(), AnyLayer::Raster(l) => l.visible(), + AnyLayer::Text(l) => l.visible(), } } @@ -961,6 +972,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_visible(visible), AnyLayer::Group(l) => l.set_visible(visible), AnyLayer::Raster(l) => l.set_visible(visible), + AnyLayer::Text(l) => l.set_visible(visible), } } @@ -972,6 +984,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.opacity(), AnyLayer::Group(l) => l.opacity(), AnyLayer::Raster(l) => l.opacity(), + AnyLayer::Text(l) => l.opacity(), } } @@ -983,6 +996,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_opacity(opacity), AnyLayer::Group(l) => l.set_opacity(opacity), AnyLayer::Raster(l) => l.set_opacity(opacity), + AnyLayer::Text(l) => l.set_opacity(opacity), } } @@ -994,6 +1008,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.volume(), AnyLayer::Group(l) => l.volume(), AnyLayer::Raster(l) => l.volume(), + AnyLayer::Text(l) => l.volume(), } } @@ -1005,6 +1020,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_volume(volume), AnyLayer::Group(l) => l.set_volume(volume), AnyLayer::Raster(l) => l.set_volume(volume), + AnyLayer::Text(l) => l.set_volume(volume), } } @@ -1016,6 +1032,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.muted(), AnyLayer::Group(l) => l.muted(), AnyLayer::Raster(l) => l.muted(), + AnyLayer::Text(l) => l.muted(), } } @@ -1027,6 +1044,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_muted(muted), AnyLayer::Group(l) => l.set_muted(muted), AnyLayer::Raster(l) => l.set_muted(muted), + AnyLayer::Text(l) => l.set_muted(muted), } } @@ -1038,6 +1056,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.soloed(), AnyLayer::Group(l) => l.soloed(), AnyLayer::Raster(l) => l.soloed(), + AnyLayer::Text(l) => l.soloed(), } } @@ -1049,6 +1068,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_soloed(soloed), AnyLayer::Group(l) => l.set_soloed(soloed), AnyLayer::Raster(l) => l.set_soloed(soloed), + AnyLayer::Text(l) => l.set_soloed(soloed), } } @@ -1060,6 +1080,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.locked(), AnyLayer::Group(l) => l.locked(), AnyLayer::Raster(l) => l.locked(), + AnyLayer::Text(l) => l.locked(), } } @@ -1071,6 +1092,7 @@ impl LayerTrait for AnyLayer { AnyLayer::Effect(l) => l.set_locked(locked), AnyLayer::Group(l) => l.set_locked(locked), AnyLayer::Raster(l) => l.set_locked(locked), + AnyLayer::Text(l) => l.set_locked(locked), } } } @@ -1085,6 +1107,7 @@ impl AnyLayer { AnyLayer::Effect(l) => &l.layer, AnyLayer::Group(l) => &l.layer, AnyLayer::Raster(l) => &l.layer, + AnyLayer::Text(l) => &l.layer, } } @@ -1097,6 +1120,7 @@ impl AnyLayer { AnyLayer::Effect(l) => &mut l.layer, AnyLayer::Group(l) => &mut l.layer, AnyLayer::Raster(l) => &mut l.layer, + AnyLayer::Text(l) => &mut l.layer, } } diff --git a/lightningbeam-ui/lightningbeam-core/src/lib.rs b/lightningbeam-ui/lightningbeam-core/src/lib.rs index db5a9d1..eadda98 100644 --- a/lightningbeam-ui/lightningbeam-core/src/lib.rs +++ b/lightningbeam-ui/lightningbeam-core/src/lib.rs @@ -54,6 +54,8 @@ pub mod svg_export; pub mod snap; pub mod webcam; pub mod raster_layer; +pub mod text_layer; +pub mod fonts; pub mod raster_store; pub mod brush_settings; pub mod brush_engine; diff --git a/lightningbeam-ui/lightningbeam-core/src/renderer.rs b/lightningbeam-ui/lightningbeam-core/src/renderer.rs index 63ffcb3..01b098d 100644 --- a/lightningbeam-ui/lightningbeam-core/src/renderer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/renderer.rs @@ -698,6 +698,11 @@ pub fn render_layer_isolated( }; } } + AnyLayer::Text(text_layer) => { + // Text composites as vector geometry (glyphs in the Vello scene). + rendered.has_content = + render_text_layer_to_scene(text_layer, time, &mut rendered.scene, base_transform); + } } rendered @@ -757,6 +762,57 @@ fn render_raster_layer_to_scene( scene.fill(Fill::NonZero, base_transform, &brush, None, &canvas_rect); } +/// Render a text layer's glyphs into a Vello scene. +/// +/// Text is laid out with parley (wrapped to the box width) and drawn via +/// `Scene::draw_glyphs`. The box origin offsets the whole layout; `base_transform` +/// carries the layer/clip-instance transform and camera. Returns whether anything +/// was drawn. +fn render_text_layer_to_scene( + layer: &crate::text_layer::TextLayer, + time: f64, + scene: &mut Scene, + base_transform: Affine, +) -> bool { + let content = layer.content_at(time); + if content.text.is_empty() { + return false; + } + let color = vello::peniko::Color::new(content.color); + let origin = Affine::translate((layer.box_origin.x, layer.box_origin.y)); + let mut drew = false; + crate::fonts::with_layout(content, layer.box_width as f32, |layout| { + for line in layout.lines() { + for item in line.items() { + let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + let run = glyph_run.run(); + let font = run.font(); + let font_size = run.font_size(); + let synthesis = run.synthesis(); + let glyph_xform = synthesis + .skew() + .map(|angle| Affine::skew((angle as f64).to_radians().tan(), 0.0)); + drew = true; + scene + .draw_glyphs(font) + .font_size(font_size) + .brush(color) + .transform(base_transform * origin) + .glyph_transform(glyph_xform) + .draw( + Fill::NonZero, + glyph_run.positioned_glyphs().map(|g| vello::Glyph { + id: g.id as u32, + x: g.x, + y: g.y, + }), + ); + } + } + }); + drew +} + // ============================================================================ // Legacy Single-Scene Rendering (kept for backwards compatibility) // ============================================================================ @@ -884,6 +940,11 @@ fn render_layer( if let Some(ex) = extract.as_deref_mut() { ex.drew_other = true; } render_raster_layer_to_scene(raster_layer, time, scene, base_transform); } + AnyLayer::Text(text_layer) => { + // Text is non-video content — force the Vello fallback if extracting. + if let Some(ex) = extract.as_deref_mut() { ex.drew_other = true; } + render_text_layer_to_scene(text_layer, time, scene, base_transform); + } } } @@ -1900,7 +1961,9 @@ fn render_vector_content_cpu( render_vector_content_cpu(document, time, child, pixmap, base_transform, parent_opacity, image_cache); } } - AnyLayer::Audio(_) | AnyLayer::Video(_) | AnyLayer::Effect(_) | AnyLayer::Raster(_) => {} + // Text is not rendered in the tiny-skia CPU fallback (GPU path only) for v1. + AnyLayer::Audio(_) | AnyLayer::Video(_) | AnyLayer::Effect(_) | AnyLayer::Raster(_) + | AnyLayer::Text(_) => {} } } diff --git a/lightningbeam-ui/lightningbeam-core/src/text_layer.rs b/lightningbeam-ui/lightningbeam-core/src/text_layer.rs new file mode 100644 index 0000000..96387b6 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/text_layer.rs @@ -0,0 +1,145 @@ +//! Text layer for Lightningbeam +//! +//! A text layer holds a single editable text field inside a resizable box, +//! with editable size, color, and font. Text is rendered as vector glyphs +//! (via parley + Vello) so it composites through the same path as vector art. +//! +//! The text/style fields are grouped in [`TextContent`] so they can move to a +//! per-keyframe model later without touching call sites; v1 stores a single +//! static instance and reads it through [`TextLayer::content_at`]. + +use crate::layer::{Layer, LayerTrait, LayerType}; +use kurbo::Point; +use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; +use uuid::Uuid; + +/// Horizontal alignment of text within the box. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum TextAlign { + Left, + Center, + Right, + Justify, +} + +impl Default for TextAlign { + fn default() -> Self { + TextAlign::Left + } +} + +/// The text content + styling for a text layer. +/// +/// Grouped as its own struct so a future keyframed model can store +/// `Vec` of these without changing the layer's public shape. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TextContent { + /// The text string. + pub text: String, + /// Font size in pixels (document space). + pub font_size: f64, + /// Fill color, linear RGBA in 0..=1. + pub color: [f32; 4], + /// Logical font family name. Empty string = the bundled default font. + pub font_family: String, + /// Horizontal alignment within the box. + pub align: TextAlign, +} + +impl Default for TextContent { + fn default() -> Self { + Self { + text: String::new(), + font_size: 48.0, + color: [1.0, 1.0, 1.0, 1.0], + font_family: String::new(), + align: TextAlign::Left, + } + } +} + +impl TextContent { + /// A stable hash of everything that affects shaped layout (everything except + /// `color`, which only affects the brush, not glyph positions). Used to key + /// the renderer's external parley-layout cache, including the wrap width. + pub fn layout_hash(&self, box_width: f64) -> u64 { + let mut h = std::collections::hash_map::DefaultHasher::new(); + self.text.hash(&mut h); + self.font_size.to_bits().hash(&mut h); + self.font_family.hash(&mut h); + self.align.hash(&mut h); + box_width.to_bits().hash(&mut h); + h.finish() + } +} + +impl Hash for TextAlign { + fn hash(&self, state: &mut H) { + (*self as u8).hash(state); + } +} + +/// A text layer: a single resizable text box. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TextLayer { + /// Base layer properties (id, name, opacity, visibility, animation data, …). + pub layer: Layer, + /// Top-left of the text box, in layer/clip-local space. + pub box_origin: Point, + /// Box width in document units (drives text wrapping). + pub box_width: f64, + /// Box height in document units. + pub box_height: f64, + /// The text content + styling (single static instance for v1). + pub content: TextContent, +} + +impl TextLayer { + /// Default text-box dimensions for a freshly created layer. + pub const DEFAULT_WIDTH: f64 = 300.0; + pub const DEFAULT_HEIGHT: f64 = 100.0; + + /// Create a new, empty text layer with the given name, positioned at `origin`. + pub fn new(name: impl Into, origin: Point) -> Self { + Self { + layer: Layer::new(LayerType::Text, name), + box_origin: origin, + box_width: Self::DEFAULT_WIDTH, + box_height: Self::DEFAULT_HEIGHT, + content: TextContent::default(), + } + } + + /// Read the active text content at `time`. v1 is static, so `time` is unused; + /// a future keyframed model will select the active keyframe here. + pub fn content_at(&self, _time: f64) -> &TextContent { + &self.content + } + + /// Mutable access to the active text content at `time`. + pub fn content_at_mut(&mut self, _time: f64) -> &mut TextContent { + &mut self.content + } +} + +// Delegate all LayerTrait methods to self.layer (mirrors RasterLayer). +impl LayerTrait for TextLayer { + fn id(&self) -> Uuid { self.layer.id } + fn name(&self) -> &str { &self.layer.name } + fn set_name(&mut self, name: String) { self.layer.name = name; } + fn has_custom_name(&self) -> bool { self.layer.has_custom_name } + fn set_has_custom_name(&mut self, custom: bool) { self.layer.has_custom_name = custom; } + fn visible(&self) -> bool { self.layer.visible } + fn set_visible(&mut self, visible: bool) { self.layer.visible = visible; } + fn opacity(&self) -> f64 { self.layer.opacity } + fn set_opacity(&mut self, opacity: f64) { self.layer.opacity = opacity; } + fn volume(&self) -> f64 { self.layer.volume } + fn set_volume(&mut self, volume: f64) { self.layer.volume = volume; } + fn muted(&self) -> bool { self.layer.muted } + fn set_muted(&mut self, muted: bool) { self.layer.muted = muted; } + fn soloed(&self) -> bool { self.layer.soloed } + fn set_soloed(&mut self, soloed: bool) { self.layer.soloed = soloed; } + fn locked(&self) -> bool { self.layer.locked } + fn set_locked(&mut self, locked: bool) { self.layer.locked = locked; } +} diff --git a/lightningbeam-ui/lightningbeam-core/src/tool.rs b/lightningbeam-ui/lightningbeam-core/src/tool.rs index 3ec96b7..88d9c47 100644 --- a/lightningbeam-ui/lightningbeam-core/src/tool.rs +++ b/lightningbeam-ui/lightningbeam-core/src/tool.rs @@ -381,7 +381,9 @@ impl Tool { use crate::layer::LayerType; match layer_type { None | Some(LayerType::Vector) => Tool::all(), - Some(LayerType::Audio) | Some(LayerType::Video) => &[Tool::Select, Tool::Split], + // The Text tool is available on audio/video/raster too: clicking the + // stage with it there creates a new top-level text layer. + Some(LayerType::Audio) | Some(LayerType::Video) => &[Tool::Select, Tool::Split, Tool::Text], Some(LayerType::Raster) => &[ // Brush tools Tool::Draw, Tool::Pencil, Tool::Pen, Tool::Airbrush, @@ -397,9 +399,10 @@ impl Tool { // Transform Tool::Transform, Tool::Warp, Tool::Liquify, // Utility - Tool::Eyedropper, + Tool::Eyedropper, Tool::Text, ], - _ => &[Tool::Select], + Some(LayerType::Text) => &[Tool::Select, Tool::Text, Tool::Transform], + _ => &[Tool::Select, Tool::Text], } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 435f7cf..eb45720 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -2130,7 +2130,7 @@ impl EditorApp { AnyLayer::Audio(al) => find_splittable_clips(&al.clip_instances, split_time, document), AnyLayer::Video(vl) => find_splittable_clips(&vl.clip_instances, split_time, document), AnyLayer::Effect(el) => find_splittable_clips(&el.clip_instances, split_time, document), - AnyLayer::Group(_) | AnyLayer::Raster(_) => vec![], + AnyLayer::Group(_) | AnyLayer::Raster(_) | AnyLayer::Text(_) => vec![], }; for instance_id in active_layer_clips { @@ -2148,7 +2148,7 @@ impl EditorApp { AnyLayer::Audio(al) => find_splittable_clips(&al.clip_instances, split_time, document), AnyLayer::Video(vl) => find_splittable_clips(&vl.clip_instances, split_time, document), AnyLayer::Effect(el) => find_splittable_clips(&el.clip_instances, split_time, document), - AnyLayer::Group(_) | AnyLayer::Raster(_) => vec![], + AnyLayer::Group(_) | AnyLayer::Raster(_) | AnyLayer::Text(_) => vec![], }; if member_splittable.contains(member_instance_id) { clips_to_split.push((*member_layer_id, *member_instance_id)); @@ -2499,7 +2499,7 @@ impl EditorApp { AnyLayer::Audio(al) => &al.clip_instances, AnyLayer::Video(vl) => &vl.clip_instances, AnyLayer::Effect(el) => &el.clip_instances, - AnyLayer::Group(_) | AnyLayer::Raster(_) => &[], + AnyLayer::Group(_) | AnyLayer::Raster(_) | AnyLayer::Text(_) => &[], }; let instances: Vec<_> = clip_slice .iter() @@ -2911,7 +2911,7 @@ impl EditorApp { AnyLayer::Audio(al) => &al.clip_instances, AnyLayer::Video(vl) => &vl.clip_instances, AnyLayer::Effect(el) => &el.clip_instances, - AnyLayer::Group(_) | AnyLayer::Raster(_) => &[], + AnyLayer::Group(_) | AnyLayer::Raster(_) | AnyLayer::Text(_) => &[], }; instances.iter() .filter(|ci| selection.contains_clip_instance(&ci.id)) @@ -3303,7 +3303,7 @@ impl EditorApp { AnyLayer::Video(_) => hint.has_video = true, AnyLayer::Audio(_) => hint.has_audio = true, AnyLayer::Raster(_) => hint.has_raster = true, - AnyLayer::Vector(_) | AnyLayer::Effect(_) => hint.has_vector = true, + AnyLayer::Vector(_) | AnyLayer::Effect(_) | AnyLayer::Text(_) => hint.has_vector = true, AnyLayer::Group(g) => scan(&g.children, hint), } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs index 0132c8f..cc39985 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs @@ -1292,6 +1292,9 @@ impl AssetLibraryPane { lightningbeam_core::layer::AnyLayer::Raster(_) => { // Raster layers don't have their own clip instances } + lightningbeam_core::layer::AnyLayer::Text(_) => { + // Text layers don't have their own clip instances + } } } false diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs index 0cb524a..0391dcd 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -46,10 +46,16 @@ pub struct InfopanelPane { fps_drag_start: Option, /// Resize mode for the active raster layer's "to document size" action (scale vs canvas). raster_resize_mode: lightningbeam_core::raster_layer::RasterResizeMode, + /// Font families already registered into egui for the family-picker previews + /// (rendered each entry in its own font), so we only load each once. + registered_preview_fonts: std::collections::HashSet, } impl InfopanelPane { pub fn new() -> Self { + // Kick off background loading of the font-picker fonts at startup so the dropdown + // is ready without a hitch by the time the user opens it. + lightningbeam_core::fonts::start_preload(); let presets = bundled_brushes(); let default_eraser_idx = presets.iter().position(|p| p.name == "Brush"); Self { @@ -64,8 +70,26 @@ impl InfopanelPane { selected_tool_gradient_stop: None, fps_drag_start: None, raster_resize_mode: lightningbeam_core::raster_layer::RasterResizeMode::Scale, + registered_preview_fonts: std::collections::HashSet::new(), } } + + /// Register pre-loaded `bytes` for `family` into egui under `FontFamily::Name(family)` + /// so a label can be rendered in that font. Cheap (no font IO); the add is idempotent + /// and batched into one atlas rebuild at frame end, so the preview appears next frame. + fn register_font_bytes(&mut self, ctx: &egui::Context, family: &str, bytes: Vec) { + if family.is_empty() || !self.registered_preview_fonts.insert(family.to_string()) { + return; + } + ctx.add_font(egui::epaint::text::FontInsert::new( + family, + egui::FontData::from_owned(bytes), + vec![egui::epaint::text::InsertFontFamily { + family: egui::FontFamily::Name(family.into()), + priority: egui::epaint::text::FontPriority::Highest, + }], + )); + } } /// Aggregated info about the current DCEL selection @@ -1167,6 +1191,7 @@ impl InfopanelPane { AnyLayer::Effect(_) => "Effect", AnyLayer::Group(_) => "Group", AnyLayer::Raster(_) => "Raster", + AnyLayer::Text(_) => "Text", }; ui.horizontal(|ui| { ui.label("Type:"); @@ -1247,6 +1272,186 @@ impl InfopanelPane { }); } + /// Render a text-layer section: edit the text content, font, size, color, + /// alignment, and box size of the active text layer. Driven by the *active* + /// layer so it appears whenever a text layer is selected. + fn render_text_layer_section(&mut self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState, layer_id: Uuid) { + use lightningbeam_core::text_layer::TextAlign; + use lightningbeam_core::actions::{ResizeTextBoxAction, SetTextContentAction}; + + // Snapshot current values, then drop the document borrow before mutating shared. + let snapshot = { + let document = shared.action_executor.document(); + match document.get_layer(&layer_id) { + Some(AnyLayer::Text(tl)) => { + Some((tl.content.clone(), tl.box_origin, tl.box_width, tl.box_height)) + } + _ => None, + } + }; + let Some((content0, origin, w0, h0)) = snapshot else { return }; + + // Families egui has actually bound (added fonts take effect next frame). Using an + // unbound `FontFamily::Name` panics, so only preview-render families in this set. + let bound_families: std::collections::HashSet = + ui.ctx().fonts(|f| f.families().into_iter().collect()); + // Set when the font dropdown popup is open this frame (its closure only runs then), + // so we can synchronously finish loading any not-yet-preloaded fonts the user sees. + let mut dropdown_open = false; + + egui::CollapsingHeader::new("Text") + .id_salt(("text_layer", path)) + .default_open(true) + .show(ui, |ui| { + ui.add_space(4.0); + let mut content = content0.clone(); + let mut content_changed = false; + + ui.label("Text:"); + if ui + .add(egui::TextEdit::multiline(&mut content.text).desired_rows(2).desired_width(f32::INFINITY)) + .changed() + { + content_changed = true; + } + + ui.horizontal(|ui| { + ui.label("Size:"); + if ui + .add(egui::DragValue::new(&mut content.font_size).range(1.0..=2000.0).speed(0.5)) + .changed() + { + content_changed = true; + } + }); + + ui.horizontal(|ui| { + ui.label("Color:"); + // content.color is sRGB-encoded RGBA in 0..1 (matches peniko Color::new). + let mut col = egui::Color32::from_rgba_unmultiplied( + (content.color[0] * 255.0).round() as u8, + (content.color[1] * 255.0).round() as u8, + (content.color[2] * 255.0).round() as u8, + (content.color[3] * 255.0).round() as u8, + ); + if ui.color_edit_button_srgba(&mut col).changed() { + content.color = [ + col.r() as f32 / 255.0, + col.g() as f32 / 255.0, + col.b() as f32 / 255.0, + col.a() as f32 / 255.0, + ]; + content_changed = true; + } + }); + + ui.horizontal(|ui| { + ui.label("Align:"); + for (label, a) in [("L", TextAlign::Left), ("C", TextAlign::Center), ("R", TextAlign::Right), ("J", TextAlign::Justify)] { + if ui.selectable_label(content.align == a, label).clicked() { + content.align = a; + content_changed = true; + } + } + }); + + // Font family picker (bundled families first, then system; embedded fonts + // register under their own names on load). + ui.horizontal(|ui| { + ui.label("Font:"); + let current_label = if content.font_family.is_empty() { + "(Default)".to_string() + } else { + content.font_family.clone() + }; + egui::ComboBox::from_id_salt(("text_font", path)) + .selected_text(current_label) + .show_ui(ui, |ui| { + dropdown_open = true; + if ui.selectable_label(content.font_family.is_empty(), "(Default)").clicked() { + content.font_family.clear(); + content_changed = true; + } + // The popup closure only runs while the dropdown is open, so each + // entry is rendered in its own font and queued for registration. + for fam in lightningbeam_core::fonts::families() { + let named = egui::FontFamily::Name(fam.as_str().into()); + // Render in its own font only once egui has bound it; otherwise + // plain (and queue it for registration so it previews next frame). + let label = if bound_families.contains(&named) { + egui::RichText::new(&fam).family(named) + } else { + egui::RichText::new(&fam) + }; + if ui.selectable_label(content.font_family == fam, label).clicked() { + content.font_family = fam.clone(); + content_changed = true; + } + } + }); + // Missing-font indicator. + if !content.font_family.is_empty() + && !lightningbeam_core::fonts::family_available(&content.font_family) + { + ui.colored_label(egui::Color32::from_rgb(230, 160, 60), "⚠ missing"); + } + }); + + // Box size. + let mut bw = w0; + let mut bh = h0; + let mut box_changed = false; + ui.horizontal(|ui| { + ui.label("Box:"); + if ui.add(egui::DragValue::new(&mut bw).range(1.0..=100000.0).speed(1.0)).changed() { + box_changed = true; + } + ui.label("×"); + if ui.add(egui::DragValue::new(&mut bh).range(1.0..=100000.0).speed(1.0)).changed() { + box_changed = true; + } + }); + + ui.add_space(4.0); + + if content_changed { + shared.pending_actions.push(Box::new(SetTextContentAction::new(layer_id, content))); + } + if box_changed { + shared.pending_actions.push(Box::new(ResizeTextBoxAction::new(layer_id, origin, bw, bh))); + } + }); + + // Register fonts the background preloader has finished (cheap: bytes already loaded), + // a few per frame so each atlas rebuild stays small. Only fall back to synchronous + // loading for families the user is actively viewing in the open dropdown that the + // background hasn't reached yet. Repaint while there's more to do or the preloader + // is still running, so registration keeps flowing without input. + let ctx = ui.ctx().clone(); + const PER_FRAME: usize = 4; + let mut budget = PER_FRAME; + let mut more = false; + for fam in lightningbeam_core::fonts::families() { + if self.registered_preview_fonts.contains(&fam) { + continue; + } + let bytes = lightningbeam_core::fonts::take_preloaded(&fam).or_else(|| { + if dropdown_open { lightningbeam_core::fonts::family_font_bytes(&fam) } else { None } + }); + if let Some(bytes) = bytes { + if budget == 0 { + more = true; + break; + } + self.register_font_bytes(&ctx, &fam, bytes); + budget -= 1; + } + } + if more || !lightningbeam_core::fonts::preload_done() { + ctx.request_repaint(); + } + } + /// Render clip instance info section fn render_clip_instance_section(&self, ui: &mut Ui, path: &NodePath, shared: &SharedPaneState, clip_ids: &[Uuid]) { let document = shared.action_executor.document(); @@ -1270,6 +1475,7 @@ impl InfopanelPane { AnyLayer::Effect(l) => &l.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], }; if let Some(ci) = instances.iter().find(|c| c.id == ci_id) { found = true; @@ -1601,6 +1807,7 @@ impl PaneRenderer for InfopanelPane { // active (independent of selection focus, since painting doesn't focus the layer). if let Some(active_id) = *shared.active_layer_id { self.render_raster_layer_section(ui, path, shared, active_id); + self.render_text_layer_section(ui, path, shared, active_id); } // Onion-skinning view settings — always available, regardless of selection. diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 92d85be..a34094f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -575,6 +575,9 @@ struct VelloRenderContext { is_playing: bool, /// Active layer for tool operations active_layer_id: Option, + /// Text layer being edited in place, with the caret + selection as byte offsets + /// into its text (start, end). Drives the Vello-drawn caret/selection. + text_edit: Option<(uuid::Uuid, usize, usize)>, /// Delta for drag preview (world space) drag_delta: Option, /// Current selection state @@ -2109,6 +2112,74 @@ impl egui_wgpu::CallbackTrait for VelloCallback { } } + // Active text-layer border: the same black/yellow marching-ants outline as + // raster, around the text box bounds (so its size is visible while selected). + if let Some(active_id) = self.ctx.active_layer_id { + if let Some(lightningbeam_core::layer::AnyLayer::Text(tl)) = self.ctx.document.get_layer(&active_id) { + let rect = vello::kurbo::Rect::from_origin_size( + tl.box_origin, + (tl.box_width, tl.box_height), + ); + let inv_zoom = 1.0 / (self.ctx.zoom as f64).max(1e-3); + let stroke_w = 1.5 * inv_zoom; + let dash = 6.0 * inv_zoom; + let pattern = [dash, dash]; + scene.stroke( + &vello::kurbo::Stroke::new(stroke_w).with_dashes(0.0, pattern), + overlay_transform, + vello::peniko::Color::new([0.0, 0.0, 0.0, 1.0]), + None, + &rect, + ); + scene.stroke( + &vello::kurbo::Stroke::new(stroke_w).with_dashes(dash, pattern), + overlay_transform, + vello::peniko::Color::new([1.0, 0.85, 0.0, 1.0]), + None, + &rect, + ); + + // 8 resize handles (corners + edge midpoints), constant screen size. + let (ox, oy, bw, bh) = (tl.box_origin.x, tl.box_origin.y, tl.box_width, tl.box_height); + let hs = 4.0 * inv_zoom; + let handle_pts = [ + (ox, oy), (ox + bw, oy), (ox + bw, oy + bh), (ox, oy + bh), + (ox + bw * 0.5, oy), (ox + bw, oy + bh * 0.5), + (ox + bw * 0.5, oy + bh), (ox, oy + bh * 0.5), + ]; + for (hx, hy) in handle_pts { + let hr = vello::kurbo::Rect::new(hx - hs, hy - hs, hx + hs, hy + hs); + scene.fill(vello::peniko::Fill::NonZero, overlay_transform, + vello::peniko::Color::new([1.0, 1.0, 1.0, 1.0]), None, &hr); + scene.stroke(&vello::kurbo::Stroke::new(stroke_w), overlay_transform, + vello::peniko::Color::new([0.0, 0.0, 0.0, 1.0]), None, &hr); + } + } + } + + // In-place text editing: Vello-drawn selection highlight + caret for the layer + // being edited (egui drives the input; we render the caret ourselves so it tracks + // any clip-instance transform). Byte offsets come from the egui field's cursor. + if let Some((edit_id, sel_start, sel_end)) = self.ctx.text_edit { + if let Some(lightningbeam_core::layer::AnyLayer::Text(tl)) = self.ctx.document.get_layer(&edit_id) { + use vello::peniko::{Color, Fill}; + let box_xf = overlay_transform * vello::kurbo::Affine::translate((tl.box_origin.x, tl.box_origin.y)); + let content = tl.content_at(self.ctx.playback_time); + let max_w = tl.box_width as f32; + // Selection highlight (behind would be ideal; semi-transparent so glyphs show through). + let (lo, hi) = (sel_start.min(sel_end), sel_start.max(sel_end)); + for (x0, y0, x1, y1) in lightningbeam_core::fonts::selection_geometry(content, max_w, lo, hi) { + let r = vello::kurbo::Rect::new(x0, y0, x1, y1); + scene.fill(Fill::NonZero, box_xf, Color::new([0.30, 0.55, 1.0, 0.35]), None, &r); + } + // Caret at the primary cursor (sel_end). + if let Some((x0, y0, x1, y1)) = lightningbeam_core::fonts::caret_geometry(content, max_w, sel_end) { + let r = vello::kurbo::Rect::new(x0, y0, x1.max(x0 + 1.0), y1); + scene.fill(Fill::NonZero, box_xf, Color::new([0.05, 0.05, 0.05, 1.0]), None, &r); + } + } + } + // Render drag preview objects with transparency if let (Some(delta), Some(active_layer_id)) = (self.ctx.drag_delta, self.ctx.active_layer_id) { if let Some(layer) = self.ctx.document.get_layer(&active_layer_id) { @@ -3267,6 +3338,28 @@ pub struct StagePane { /// Synthetic drag/click override for test mode replay (debug builds only) #[cfg(debug_assertions)] replay_override: Option, + + // --- Text layer in-place editing --- + /// The text layer currently being edited in place (None = not editing). + text_edit_layer: Option, + /// Working buffer mirrored to/from the hidden egui TextEdit while editing. + text_edit_buffer: String, + /// Set the frame editing begins so the egui field grabs focus exactly once. + text_edit_request_focus: bool, + /// Caret + selection as BYTE offsets into the text (anchor, focus), for the Vello caret. + text_edit_cursor: Option<(usize, usize)>, + /// The text content captured when editing began, for a single clean undo step. + text_edit_original: Option, + /// True when the current edit is of a freshly-created layer whose creation is still the + /// top undo entry — so committing it empty can remove it by rolling that creation back. + /// Cleared if another action (e.g. a resize) is pushed during the edit. + text_edit_is_new: bool, + /// If the current edit created a new clip (vector branch), its id — so an empty commit + /// also exits that clip after removing it. + text_edit_created_clip: Option, + /// Active drag of a text box resize handle: + /// (layer_id, handle_index, start_origin_x, start_origin_y, start_w, start_h). + text_box_drag: Option<(uuid::Uuid, usize, f64, f64, f64, f64)>, } /// Synthetic drag/click state injected during test mode replay @@ -3620,6 +3713,27 @@ fn selection_stipple_brush() -> &'static vello::peniko::ImageBrush { }) } +/// Convert a character index (egui caret) to a byte offset (parley/`str`). +fn char_to_byte(s: &str, char_idx: usize) -> usize { + s.char_indices().nth(char_idx).map(|(b, _)| b).unwrap_or(s.len()) +} + +/// Diagonal resize cursor for an axis-aligned box corner (0=TL, 1=TR, 2=BR, 3=BL). +fn corner_resize_cursor(corner: usize) -> egui::CursorIcon { + match corner % 4 { + 0 | 2 => egui::CursorIcon::ResizeNwSe, // TL / BR + _ => egui::CursorIcon::ResizeNeSw, // TR / BL + } +} + +/// Axis resize cursor for an axis-aligned box edge (0=Top, 1=Right, 2=Bottom, 3=Left). +fn edge_resize_cursor(edge: usize) -> egui::CursorIcon { + match edge % 4 { + 0 | 2 => egui::CursorIcon::ResizeVertical, // Top / Bottom + _ => egui::CursorIcon::ResizeHorizontal, // Right / Left + } +} + impl StagePane { pub fn new() -> Self { let instance_id = INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); @@ -3661,6 +3775,14 @@ impl StagePane { pending_tool_readback_b: None, #[cfg(debug_assertions)] replay_override: None, + text_edit_layer: None, + text_edit_buffer: String::new(), + text_edit_request_focus: false, + text_edit_cursor: None, + text_edit_original: None, + text_edit_is_new: false, + text_edit_created_clip: None, + text_box_drag: None, } } @@ -4836,6 +4958,364 @@ impl StagePane { } } + /// Text tool: click the stage to create a text layer. + /// - Active layer is Vector → create a clip (VectorClip + instance) containing a + /// text layer, then enter that clip so the text is directly editable. + /// - Otherwise (raster/video/none) → create a new top-level text layer. + fn handle_text_tool( + &mut self, + ui: &mut egui::Ui, + response: &egui::Response, + world_pos: egui::Vec2, + shared: &mut SharedPaneState, + ) { + use lightningbeam_core::layer::AnyLayer; + use lightningbeam_core::text_layer::{TextLayer, TextContent}; + use lightningbeam_core::actions::{AddLayerAction, CreateTextClipAction}; + use vello::kurbo::Point; + + // Allow dragging the active text layer's resize handles with the Text tool too + // (runs on drag events, so it must precede the click-only guard below). + if self.handle_text_box_resize(ui, response, world_pos, shared) { + return; + } + // Only act on a click (ignore drags / hovers). + if !self.rsp_clicked(response) { + return; + } + + let point = Point::new(world_pos.x as f64, world_pos.y as f64); + + // Click on an existing text box → edit it (don't spawn a duplicate). + if let Some(hit_id) = self.text_layer_at(point, shared) { + if self.text_edit_layer != Some(hit_id) { + if let Some(cur) = self.text_edit_layer { + self.commit_text_edit(shared, cur); + } + *shared.active_layer_id = Some(hit_id); + self.begin_text_edit(hit_id); + } + return; + } + + // Empty space → commit any in-progress edit, then create a new text layer. + if let Some(cur) = self.text_edit_layer { + self.commit_text_edit(shared, cur); + } + + let fc = *shared.fill_color; + let color = [ + fc.r() as f32 / 255.0, + fc.g() as f32 / 255.0, + fc.b() as f32 / 255.0, + fc.a() as f32 / 255.0, + ]; + + let is_vector = shared + .active_layer_id + .and_then(|id| shared.action_executor.document().get_layer(&id)) + .map_or(false, |l| matches!(l, AnyLayer::Vector(_))); + + if is_vector { + let parent_id = shared.active_layer_id.unwrap(); + // The text layer sits at the clip's origin; the instance is placed at the click. + let mut tl = TextLayer::new("Text", Point::ZERO); + tl.content = TextContent { color, ..TextContent::default() }; + let text_layer_id = tl.layer.id; + let clip_id = uuid::Uuid::new_v4(); + let instance_id = uuid::Uuid::new_v4(); + let action = CreateTextClipAction::new(parent_id, tl, (point.x, point.y)) + .with_ids(clip_id, instance_id); + if shared.action_executor.execute(Box::new(action)).is_ok() { + // Enter the new clip; main.rs activates the clip's first layer (the text layer). + *shared.pending_enter_clip = Some((clip_id, instance_id, parent_id)); + self.begin_text_edit(text_layer_id); + self.text_edit_is_new = true; + self.text_edit_created_clip = Some(clip_id); + } + } else { + // Raster / video / group / no layer → new top-level text layer, created in + // the current editing context (root, or the clip currently being edited). + let mut tl = TextLayer::new("Text", point); + tl.content = TextContent { color, ..TextContent::default() }; + let text_layer_id = tl.layer.id; + let action = AddLayerAction::new(AnyLayer::Text(tl)) + .with_target_clip(shared.editing_clip_id); + if shared.action_executor.execute(Box::new(action)).is_ok() { + *shared.active_layer_id = Some(text_layer_id); + self.begin_text_edit(text_layer_id); + self.text_edit_is_new = true; + } + } + } + + /// Handle dragging a text box resize handle. Returns true if the interaction was + /// consumed (so the normal select/create logic should be skipped this frame). + /// `world_pos` is in the active layer's local space (clip-local when editing a clip), + /// which matches the box coordinates, so hit-testing is axis-aligned here. + /// Index (0..8) of the text-box resize handle under `(mx,my)`, if any. + /// 0=TL 1=TR 2=BR 3=BL (corners), 4=Top 5=Right 6=Bottom 7=Left (edges). + fn text_handle_at(ox: f64, oy: f64, bw: f64, bh: f64, mx: f64, my: f64, tol: f64) -> Option { + let pts = [ + (ox, oy), (ox + bw, oy), (ox + bw, oy + bh), (ox, oy + bh), + (ox + bw * 0.5, oy), (ox + bw, oy + bh * 0.5), + (ox + bw * 0.5, oy + bh), (ox, oy + bh * 0.5), + ]; + pts.iter().position(|(hx, hy)| (hx - mx).abs() <= tol && (hy - my).abs() <= tol) + } + + /// Resize cursor for a text-box handle index (reuses the shared corner/edge mapping). + fn text_handle_cursor(idx: usize) -> egui::CursorIcon { + if idx < 4 { corner_resize_cursor(idx) } else { edge_resize_cursor(idx - 4) } + } + + fn handle_text_box_resize( + &mut self, + ui: &egui::Ui, + response: &egui::Response, + world_pos: egui::Vec2, + shared: &mut SharedPaneState, + ) -> bool { + use lightningbeam_core::layer::AnyLayer; + + let Some(layer_id) = *shared.active_layer_id else { return false }; + let Some((ox, oy, bw, bh)) = (match shared.action_executor.document().get_layer(&layer_id) { + Some(AnyLayer::Text(tl)) => Some((tl.box_origin.x, tl.box_origin.y, tl.box_width, tl.box_height)), + _ => None, + }) else { return false }; + + let (mx, my) = (world_pos.x as f64, world_pos.y as f64); + let tol = 6.0 / (self.zoom as f64).max(1e-3); + + // Hover feedback: show a resize cursor over a handle when not already dragging. + if self.text_box_drag.is_none() { + if let Some(idx) = Self::text_handle_at(ox, oy, bw, bh, mx, my, tol) { + ui.ctx().set_cursor_icon(Self::text_handle_cursor(idx)); + } + } + + // Begin a handle drag: hit-test the 8 handles. + if self.rsp_drag_started(response) && self.text_box_drag.is_none() { + if let Some(idx) = Self::text_handle_at(ox, oy, bw, bh, mx, my, tol) { + self.text_box_drag = Some((layer_id, idx, ox, oy, bw, bh)); + return true; + } + } + + let Some((drag_id, idx, sox, soy, sbw, sbh)) = self.text_box_drag else { return false }; + if drag_id != layer_id { return false; } + + if self.rsp_dragged(response) { + let (no, nw, nh) = Self::resize_text_box(idx, sox, soy, sbw, sbh, mx, my); + // Live preview (no undo entry). + let doc = shared.action_executor.document_mut(); + if let Some(AnyLayer::Text(tl)) = doc.get_layer_mut(&layer_id) { + tl.box_origin = no; + tl.box_width = nw; + tl.box_height = nh; + } + return true; + } + + if self.rsp_drag_stopped(response) { + let (no, nw, nh) = Self::resize_text_box(idx, sox, soy, sbw, sbh, mx, my); + // Restore the start box, then record one undoable resize. + { + let doc = shared.action_executor.document_mut(); + if let Some(AnyLayer::Text(tl)) = doc.get_layer_mut(&layer_id) { + tl.box_origin = vello::kurbo::Point::new(sox, soy); + tl.box_width = sbw; + tl.box_height = sbh; + } + } + let action = lightningbeam_core::actions::ResizeTextBoxAction::new(layer_id, no, nw, nh); + let _ = shared.action_executor.execute(Box::new(action)); + self.text_box_drag = None; + // A resize is now the top undo entry, so the creation can no longer be + // rolled back by an empty-commit; keep the layer even if left empty. + self.text_edit_is_new = false; + return true; + } + + true + } + + /// Compute a new (origin, width, height) for a text box given a dragged handle. + /// Handles: 0=TL 1=TR 2=BR 3=BL (corners), 4=T 5=R 6=B 7=L (edges). + fn resize_text_box(idx: usize, ox: f64, oy: f64, w: f64, h: f64, mx: f64, my: f64) + -> (vello::kurbo::Point, f64, f64) + { + const MIN: f64 = 8.0; + let (mut l, mut r, mut t, mut b) = (ox, ox + w, oy, oy + h); + match idx { + 0 => { l = mx; t = my; } + 1 => { r = mx; t = my; } + 2 => { r = mx; b = my; } + 3 => { l = mx; b = my; } + 4 => { t = my; } + 5 => { r = mx; } + 6 => { b = my; } + 7 => { l = mx; } + _ => {} + } + if r - l < MIN { + if matches!(idx, 0 | 3 | 7) { l = r - MIN; } else { r = l + MIN; } + } + if b - t < MIN { + if matches!(idx, 0 | 1 | 4) { t = b - MIN; } else { b = t + MIN; } + } + (vello::kurbo::Point::new(l, t), r - l, b - t) + } + + /// Begin in-place editing of an existing text layer (not a fresh creation). + fn begin_text_edit(&mut self, layer_id: uuid::Uuid) { + self.text_edit_layer = Some(layer_id); + self.text_edit_buffer.clear(); + self.text_edit_request_focus = true; + self.text_edit_cursor = None; + self.text_edit_original = None; // captured on the first update frame + self.text_edit_is_new = false; + self.text_edit_created_clip = None; + } + + /// Clear all in-place text editing state. + fn finish_text_edit_state(&mut self) { + self.text_edit_layer = None; + self.text_edit_buffer.clear(); + self.text_edit_cursor = None; + self.text_edit_original = None; + self.text_edit_request_focus = false; + self.text_edit_is_new = false; + self.text_edit_created_clip = None; + } + + /// Commit the current in-place edit as a single undoable step. + fn commit_text_edit(&mut self, shared: &mut SharedPaneState, layer_id: uuid::Uuid) { + use lightningbeam_core::actions::SetTextContentAction; + + // A freshly-created layer left empty is removed by rolling back its creation + // (the top undo entry), which cleanly reverses both the root and clip branches. + if self.text_edit_is_new && self.text_edit_buffer.trim().is_empty() { + let _ = shared.action_executor.undo(); + if *shared.active_layer_id == Some(layer_id) { + *shared.active_layer_id = None; + } + // If creating it entered a new clip, leave that clip (it no longer exists). + if self.text_edit_created_clip.is_some() { + *shared.pending_exit_clip = true; + } + self.finish_text_edit_state(); + return; + } + + if let Some(orig) = self.text_edit_original.take() { + let mut final_content = orig.clone(); + final_content.text = self.text_edit_buffer.clone(); + if final_content != orig { + // The document already holds `final_content` (live preview); with_old + // records the pre-edit value so undo restores it in one step. + let action = SetTextContentAction::with_old(layer_id, orig, final_content); + let _ = shared.action_executor.execute(Box::new(action)); + } + } + self.finish_text_edit_state(); + } + + /// Find the topmost text layer in the current editing context whose box contains + /// `point` (in the context's local space, which matches `world_pos`). + fn text_layer_at(&self, point: vello::kurbo::Point, shared: &SharedPaneState) -> Option { + use lightningbeam_core::layer::AnyLayer; + let doc = shared.action_executor.document(); + let layers = doc.context_layers(shared.editing_clip_id.as_ref()); + for layer in layers.into_iter().rev() { + if let AnyLayer::Text(tl) = layer { + let r = vello::kurbo::Rect::from_origin_size( + tl.box_origin, (tl.box_width, tl.box_height)); + if r.contains(point) { + return Some(tl.layer.id); + } + } + } + None + } + + /// Drive the hybrid in-place text editor: a hidden, focused egui `TextEdit` captures + /// keystrokes/IME and the caret position; the text + caret are rendered in Vello. + /// The field is parked far offscreen (`constrain(false)` so egui doesn't clamp it back + /// on-screen) — invisible and not intercepting pointer events over the text box. + fn update_text_editing(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) { + use lightningbeam_core::layer::AnyLayer; + let Some(layer_id) = self.text_edit_layer else { return }; + + // Current text, or bail (and clear) if the layer is gone / not text anymore. + let cur_text = { + let doc = shared.action_executor.document(); + match doc.get_layer(&layer_id) { + Some(AnyLayer::Text(tl)) => { + if self.text_edit_original.is_none() { + self.text_edit_original = Some(tl.content.clone()); + self.text_edit_buffer = tl.content.text.clone(); + } + tl.content.text.clone() + } + _ => { self.finish_text_edit_state(); return; } + } + }; + + // Escape cancels: restore the pre-edit content and exit. + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + if let Some(orig) = self.text_edit_original.take() { + let doc = shared.action_executor.document_mut(); + if let Some(AnyLayer::Text(tl)) = doc.get_layer_mut(&layer_id) { + tl.content = orig; + } + } + self.finish_text_edit_state(); + return; + } + + // Hidden, focused TextEdit parked offscreen (constrain(false) keeps egui from + // clamping it back into view). Captures keyboard/IME + caret only. + let te_id = egui::Id::new(("lb_text_edit", layer_id)); + let output = egui::Area::new(egui::Id::new(("lb_text_edit_area", layer_id))) + .constrain(false) + .fixed_pos(egui::pos2(-100_000.0, -100_000.0)) + .show(ui.ctx(), |ui| { + egui::TextEdit::multiline(&mut self.text_edit_buffer) + .id(te_id) + .desired_width(1.0) + .show(ui) + }) + .inner; + + // Acquire and hold keyboard focus; once acquired, losing it commits the edit. + if self.text_edit_request_focus { + if output.response.has_focus() { + self.text_edit_request_focus = false; + } else { + output.response.request_focus(); + } + } else if !output.response.has_focus() { + self.commit_text_edit(shared, layer_id); + return; + } + + // Caret/selection → byte offsets for the Vello caret. + if let Some(cr) = output.cursor_range { + let anchor = char_to_byte(&self.text_edit_buffer, cr.secondary.index); + let focus = char_to_byte(&self.text_edit_buffer, cr.primary.index); + self.text_edit_cursor = Some((anchor, focus)); + } + + // Live preview: push the buffer into the document text (no undo entry). + if self.text_edit_buffer != cur_text { + let doc = shared.action_executor.document_mut(); + if let Some(AnyLayer::Text(tl)) = doc.get_layer_mut(&layer_id) { + tl.content.text = self.text_edit_buffer.clone(); + } + } + } + fn handle_ellipse_tool( &mut self, ui: &mut egui::Ui, @@ -9928,29 +10408,17 @@ impl StagePane { // Check corner handles with correct diagonal cursors for (idx, corner) in world_corners.iter().enumerate() { if point.distance(*corner) < tolerance { - // Top-left (0) and bottom-right (2): NW-SE diagonal (\) - // Top-right (1) and bottom-left (3): NE-SW diagonal (/) - let cursor = match idx { - 0 | 2 => egui::CursorIcon::ResizeNwSe, // Top-left, Bottom-right - 1 | 3 => egui::CursorIcon::ResizeNeSw, // Top-right, Bottom-left - _ => egui::CursorIcon::Default, - }; - ui.ctx().set_cursor_icon(cursor); + ui.ctx().set_cursor_icon(corner_resize_cursor(idx)); hovering_handle = true; break; } } - // Check edge handles + // Check edge handles (midpoints are ordered top, right, bottom, left) if !hovering_handle { for (idx, edge_pos) in edge_midpoints.iter().enumerate() { if point.distance(*edge_pos) < tolerance { - let cursor = match idx { - 0 | 2 => egui::CursorIcon::ResizeVertical, // Top/Bottom - 1 | 3 => egui::CursorIcon::ResizeHorizontal, // Right/Left - _ => egui::CursorIcon::Default, - }; - ui.ctx().set_cursor_icon(cursor); + ui.ctx().set_cursor_icon(edge_resize_cursor(idx)); hovering_handle = true; break; } @@ -10863,10 +11331,19 @@ impl StagePane { match *shared.selected_tool { Tool::Select => { - let is_raster = shared.active_layer_id.and_then(|id| { + let active = shared.active_layer_id.and_then(|id| { shared.action_executor.document().get_layer(&id) - }).map_or(false, |l| matches!(l, lightningbeam_core::layer::AnyLayer::Raster(_))); - if is_raster { + }); + let is_raster = matches!(active, Some(lightningbeam_core::layer::AnyLayer::Raster(_))); + let is_text = matches!(active, Some(lightningbeam_core::layer::AnyLayer::Text(_))); + if is_text && self.handle_text_box_resize(ui, &response, world_pos, shared) { + // Consumed by a resize-handle drag. + } else if is_text && response.double_clicked() { + // Double-click a selected text layer to edit it in place. + if let Some(id) = *shared.active_layer_id { + self.begin_text_edit(id); + } + } else if is_raster { self.handle_raster_select_tool(ui, &response, world_pos, shared); } else { self.handle_select_tool(ui, &response, world_pos, shift_held, shared); @@ -10932,6 +11409,9 @@ impl StagePane { Tool::Gradient => { self.handle_raster_gradient_tool(ui, &response, world_pos, shared); } + Tool::Text => { + self.handle_text_tool(ui, &response, world_pos, shared); + } _ => { // Other tools not implemented yet } @@ -11765,6 +12245,9 @@ impl PaneRenderer for StagePane { // Handle input for pan/zoom and tool controls self.handle_input(ui, rect, shared); + // Drive the in-place text editor (hidden egui field → Vello caret). + self.update_text_editing(ui, shared); + // Handle asset drag-and-drop from Asset Library if let Some(dragging) = shared.dragging_asset.clone() { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { @@ -12143,6 +12626,9 @@ impl PaneRenderer for StagePane { container_path: shared.container_path.clone(), is_playing: *shared.is_playing, active_layer_id: *shared.active_layer_id, + text_edit: self.text_edit_layer.and_then(|id| { + self.text_edit_cursor.map(|(s, e)| (id, s, e)) + }), drag_delta, selection: shared.selection.clone(), fill_color: *shared.fill_color, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index 5cfdca3..f233858 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -212,6 +212,7 @@ fn effective_clip_duration( AnyLayer::Effect(_) => Some(lightningbeam_core::effect::EFFECT_DURATION), AnyLayer::Group(_) => None, AnyLayer::Raster(_) => None, + AnyLayer::Text(_) => None, } } @@ -685,6 +686,7 @@ fn layer_clips<'a>( AnyLayer::Effect(l) => &l.clip_instances, AnyLayer::Group(_) => &[], AnyLayer::Raster(_) => &[], + AnyLayer::Text(_) => &[], } } @@ -718,6 +720,7 @@ fn collect_clip_instances<'a>( } } AnyLayer::Raster(_) => {} + AnyLayer::Text(_) => {} } } @@ -768,6 +771,7 @@ fn layer_type_info(layer: &AnyLayer) -> (&'static str, egui::Color32) { AnyLayer::Effect(_) => ("Effect", egui::Color32::from_rgb(255, 100, 180)), AnyLayer::Group(_) => ("Group", egui::Color32::from_rgb(0, 180, 180)), AnyLayer::Raster(_) => ("Raster", egui::Color32::from_rgb(160, 100, 200)), + AnyLayer::Text(_) => ("Text", egui::Color32::from_rgb(220, 200, 120)), } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs index 019df99..28315bd 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs @@ -43,6 +43,7 @@ impl PaneRenderer for ToolbarPane { AnyLayer::Effect(_) => LayerType::Effect, AnyLayer::Group(_) => LayerType::Group, AnyLayer::Raster(_) => LayerType::Raster, + AnyLayer::Text(_) => LayerType::Text, }); // Auto-switch to Select if the current tool isn't available for this layer type From da0b39fef04c50769685485b004fec4eee0a00ab Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 29 Jun 2026 21:42:18 -0400 Subject: [PATCH 02/43] Refactor RenderContext construction into EditorApp::build_frame Extract the ~80-field SharedPaneState/RenderContext literal from update() into EditorApp::build_frame, returning a FrameBundle that also carries the layout-editing borrows (current_layout, drag_state, etc.). Per-frame locals move into a FrameScratch struct so the bundle can borrow both self and the scratch under one lifetime. This is a behavior-preserving refactor that gives the upcoming mobile UI path a single shared construction site for SharedPaneState instead of duplicating the literal. --- .../lightningbeam-editor/src/main.rs | 321 ++++++++++-------- 1 file changed, 178 insertions(+), 143 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index eb45720..3bb7c96 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6535,38 +6535,20 @@ impl eframe::App for EditorApp { // Main pane area (editor mode) let mut layout_action: Option = None; - let mut clipboard_consumed = false; + // Per-frame scratch consumed by the post-render drain (see FrameScratch). + // Declared outside the closure because some of it (clipboard_consumed) is read + // after the panel closes. + let mut scratch = FrameScratch::default(); + #[cfg(debug_assertions)] + { + scratch.synthetic_input = test_mode_replay.synthetic_input; + } egui::CentralPanel::default().show(ctx, |ui| { let available_rect = ui.available_rect_before_wrap(); // Reset hovered divider each frame self.hovered_divider = None; - // Track fallback pane priority for view actions (reset each frame) - let mut fallback_pane_priority: Option = None; - - // Registry for view action handlers (two-phase dispatch) - let mut pending_handlers: Vec = Vec::new(); - - // Registry for actions to execute after rendering (two-phase dispatch) - let mut pending_actions: Vec> = Vec::new(); - - // Menu actions queued by pane context menus - let mut pending_menu_actions: Vec = Vec::new(); - - // Editing context navigation requests from stage pane - let mut pending_enter_clip: Option<(Uuid, Uuid, Uuid)> = None; - let mut pending_exit_clip = false; - - // Synthetic input from test mode replay (debug builds only) - #[cfg(debug_assertions)] - let mut synthetic_input_storage: Option = test_mode_replay.synthetic_input; - - // Queue for effect thumbnail requests (collected during rendering) - let mut effect_thumbnail_requests: Vec = Vec::new(); - // Empty cache fallback if generator not initialized - let empty_thumbnail_cache: HashMap> = HashMap::new(); - // Sync clip instance transforms from animation data at current playback time. // This ensures selection boxes, hit testing, and interactive editing see the // animated transform values, not just the base values on the ClipInstance struct. @@ -6601,133 +6583,36 @@ impl eframe::App for EditorApp { } } - // Create render context - let mut ctx = RenderContext { - shared: panes::SharedPaneState { - container_path: self.current_file_path.clone(), - onion: { - // Onion skinning is disabled during playback. - let mut o = self.onion_skin; - o.enabled = o.enabled && !self.is_playing; - o - }, - onion_skin: &mut self.onion_skin, - tool_icon_cache: &mut self.tool_icon_cache, - icon_cache: &mut self.icon_cache, - selected_tool: &mut self.selected_tool, - fill_color: &mut self.fill_color, - stroke_color: &mut self.stroke_color, - active_color_mode: &mut self.active_color_mode, - pending_view_action: &mut self.pending_view_action, - fallback_pane_priority: &mut fallback_pane_priority, - pending_handlers: &mut pending_handlers, - theme: &self.theme, - action_executor: &mut self.action_executor, - selection: &mut self.selection, - focus: &mut self.focus, - editing_clip_id: self.editing_context.current_clip_id(), - editing_instance_id: self.editing_context.current_instance_id(), - editing_parent_layer_id: self.editing_context.current_parent_layer_id(), - pending_enter_clip: &mut pending_enter_clip, - pending_exit_clip: &mut pending_exit_clip, - active_layer_id: &mut self.active_layer_id, - tool_state: &mut self.tool_state, - pending_actions: &mut pending_actions, - draw_simplify_mode: &mut self.draw_simplify_mode, - rdp_tolerance: &mut self.rdp_tolerance, - schneider_max_error: &mut self.schneider_max_error, - raster_settings: &mut self.raster_settings, - audio_controller: self.audio_controller.as_ref(), - clip_snapshot: self.audio_controller.as_ref().map(|arc| { - arc.lock().unwrap().clip_snapshot() - }), - audio_input_opener: &mut self.audio_input, - audio_input_stream: &mut self.audio_input_stream, - audio_buffer_size: self.audio_buffer_size, - video_manager: &self.video_manager, - playback_time: &mut self.playback_time, - is_playing: &mut self.is_playing, - is_recording: &mut self.is_recording, - metronome_enabled: &mut self.metronome_enabled, - count_in_enabled: &mut self.count_in_enabled, - recording_clips: &mut self.recording_clips, - recording_start_time: &mut self.recording_start_time, - recording_layer_ids: &mut self.recording_layer_ids, - dragging_asset: &mut self.dragging_asset, - stroke_width: &mut self.stroke_width, - fill_enabled: &mut self.fill_enabled, - snap_enabled: &mut self.snap_enabled, - paint_bucket_gap_tolerance: &mut self.paint_bucket_gap_tolerance, - polygon_sides: &mut self.polygon_sides, - layer_to_track_map: &self.layer_to_track_map, - clip_instance_to_backend_map: &self.clip_instance_to_backend_map, - midi_event_cache: &mut self.midi_event_cache, - audio_pools_with_new_waveforms: &self.audio_pools_with_new_waveforms, - raw_audio_cache: &self.raw_audio_cache, - waveform_gpu_dirty: &mut self.waveform_gpu_dirty, - waveform_minmax_pools: &self.waveform_minmax_pools, - raster_fault_requests: &self.raster_fault_requests, - effect_to_load: &mut self.effect_to_load, - effect_thumbnail_requests: &mut effect_thumbnail_requests, - effect_thumbnail_cache: self.effect_thumbnail_generator.as_ref() - .map(|g| g.thumbnail_cache()) - .unwrap_or(&empty_thumbnail_cache), - effect_thumbnails_to_invalidate: &mut self.effect_thumbnails_to_invalidate, - webcam_frame: self.webcam_frame.clone(), - webcam_record_command: &mut self.webcam_record_command, - target_format: self.target_format, - pending_menu_actions: &mut pending_menu_actions, - clipboard_manager: &mut self.clipboard_manager, - input_level: self.input_level, - output_level: self.output_level, - track_levels: &self.track_levels, - track_to_layer_map: &self.track_to_layer_map, - waveform_stereo: self.config.waveform_stereo, - project_generation: &mut self.project_generation, - graph_topology_generation: &mut self.graph_topology_generation, - script_to_edit: &mut self.script_to_edit, - script_saved: &mut self.script_saved, - pending_region_cut_base: &mut self.pending_region_cut_base, - region_select_mode: &mut self.region_select_mode, - lasso_mode: &mut self.lasso_mode, - pending_graph_loads: &self.pending_graph_loads, - clipboard_consumed: &mut clipboard_consumed, - keymap: &self.keymap, - pending_node_group: &mut self.pending_node_group, - pending_node_ungroup: &mut self.pending_node_ungroup, - #[cfg(debug_assertions)] - test_mode: &mut self.test_mode, - #[cfg(debug_assertions)] - synthetic_input: &mut synthetic_input_storage, - brush_preview_pixels: &self.brush_preview_pixels, - }, - pane_instances: &mut self.pane_instances, - }; + // Build the per-frame context (single source of truth for SharedPaneState, + // shared with the mobile path). The bundle borrows `self` + `scratch`; it + // is dropped before the post-render drain below re-touches them. + let mut bundle = self.build_frame(&mut scratch); render_layout_node( ui, - &mut self.current_layout, + bundle.current_layout, available_rect, - &mut self.drag_state, - &mut self.hovered_divider, - &mut self.selected_pane, + bundle.drag_state, + bundle.hovered_divider, + bundle.selected_pane, &mut layout_action, - &mut self.split_preview_mode, + bundle.split_preview_mode, &Vec::new(), // Root path - &mut ctx, + &mut bundle.rc, ); + drop(bundle); // Process collected effect thumbnail requests - if !effect_thumbnail_requests.is_empty() { + if !scratch.effect_thumbnail_requests.is_empty() { if let Some(generator) = &mut self.effect_thumbnail_generator { - generator.request_thumbnails(&effect_thumbnail_requests); + generator.request_thumbnails(&scratch.effect_thumbnail_requests); } } // Execute action on the best handler (two-phase dispatch) if let Some(action) = &self.pending_view_action { - if let Some(best_handler) = pending_handlers.iter().min_by_key(|h| h.priority) { + if let Some(best_handler) = scratch.pending_handlers.iter().min_by_key(|h| h.priority) { // Look up the pane instance and execute the action if let Some(pane_instance) = self.pane_instances.get_mut(&best_handler.pane_path) { match pane_instance { @@ -6748,7 +6633,7 @@ impl eframe::App for EditorApp { self.sync_audio_layers_to_backend(); // Execute all pending actions (two-phase dispatch) - for action in pending_actions { + for action in std::mem::take(&mut scratch.pending_actions) { // Record action for test mode (debug builds only) #[cfg(debug_assertions)] let action_desc = action.description(); @@ -6780,7 +6665,7 @@ impl eframe::App for EditorApp { } // Process menu actions queued by pane context menus - for action in pending_menu_actions { + for action in std::mem::take(&mut scratch.pending_menu_actions) { self.handle_menu_action(action); } @@ -6942,7 +6827,7 @@ impl eframe::App for EditorApp { } // Process editing context navigation (enter/exit movie clips) - if let Some((clip_id, instance_id, parent_layer_id)) = pending_enter_clip { + if let Some((clip_id, instance_id, parent_layer_id)) = scratch.pending_enter_clip { let entry = EditingContextEntry { clip_id, instance_id, @@ -6966,7 +6851,7 @@ impl eframe::App for EditorApp { self.commit_raster_floating(); } - if pending_exit_clip { + if scratch.pending_exit_clip { if let Some(entry) = self.editing_context.pop() { self.selection.clear(); self.active_layer_id = entry.saved_active_layer_id; @@ -7030,8 +6915,8 @@ impl eframe::App for EditorApp { // Event::Copy/Cut/Paste instead of regular key events, so // check_shortcuts won't see them via key_pressed(). // Skip if a pane (e.g. piano roll) already handled the clipboard event. - let mut clipboard_handled = clipboard_consumed; - if !clipboard_consumed { + let mut clipboard_handled = scratch.clipboard_consumed; + if !scratch.clipboard_consumed { for event in &i.events { match event { egui::Event::Copy => { @@ -7194,6 +7079,156 @@ struct RenderContext<'a> { pane_instances: &'a mut HashMap, } +/// Per-frame scratch state used to build the `RenderContext`. These values live only +/// for the duration of one `update()` frame: panes write into them during rendering +/// and the post-render drain consumes them. Grouping them lets [`EditorApp::build_frame`] +/// hand back a context that borrows both `self` and this scratch, so the desktop and +/// mobile render paths share one construction site for the ~80-field `SharedPaneState`. +#[derive(Default)] +struct FrameScratch { + fallback_pane_priority: Option, + pending_handlers: Vec, + pending_enter_clip: Option<(Uuid, Uuid, Uuid)>, + pending_exit_clip: bool, + pending_actions: Vec>, + pending_menu_actions: Vec, + effect_thumbnail_requests: Vec, + clipboard_consumed: bool, + empty_thumbnail_cache: HashMap>, + #[cfg(debug_assertions)] + synthetic_input: Option, +} + +/// Everything `update()` hands to a per-frame layout renderer: the [`RenderContext`] +/// plus the layout-editing borrows that ride alongside it (these are passed to +/// `render_layout_node` as separate `&mut` args and are disjoint from `SharedPaneState`). +/// Built by [`EditorApp::build_frame`] from a single `&mut self`, which is what lets the +/// huge `SharedPaneState` literal have one home shared by the desktop and mobile paths. +struct FrameBundle<'a> { + rc: RenderContext<'a>, + current_layout: &'a mut LayoutNode, + drag_state: &'a mut DragState, + hovered_divider: &'a mut Option<(NodePath, bool)>, + selected_pane: &'a mut Option, + split_preview_mode: &'a mut SplitPreviewMode, +} + +impl EditorApp { + /// Build the per-frame [`FrameBundle`]. The returned bundle borrows `self` and + /// `scratch` for `'a`; it must be dropped before the post-render drain touches + /// `self`/`scratch` again. This is the single source of truth for `SharedPaneState`. + fn build_frame<'a>(&'a mut self, scratch: &'a mut FrameScratch) -> FrameBundle<'a> { + FrameBundle { + rc: RenderContext { + shared: panes::SharedPaneState { + container_path: self.current_file_path.clone(), + onion: { + // Onion skinning is disabled during playback. + let mut o = self.onion_skin; + o.enabled = o.enabled && !self.is_playing; + o + }, + onion_skin: &mut self.onion_skin, + tool_icon_cache: &mut self.tool_icon_cache, + icon_cache: &mut self.icon_cache, + selected_tool: &mut self.selected_tool, + fill_color: &mut self.fill_color, + stroke_color: &mut self.stroke_color, + active_color_mode: &mut self.active_color_mode, + pending_view_action: &mut self.pending_view_action, + fallback_pane_priority: &mut scratch.fallback_pane_priority, + pending_handlers: &mut scratch.pending_handlers, + theme: &self.theme, + action_executor: &mut self.action_executor, + selection: &mut self.selection, + focus: &mut self.focus, + editing_clip_id: self.editing_context.current_clip_id(), + editing_instance_id: self.editing_context.current_instance_id(), + editing_parent_layer_id: self.editing_context.current_parent_layer_id(), + pending_enter_clip: &mut scratch.pending_enter_clip, + pending_exit_clip: &mut scratch.pending_exit_clip, + active_layer_id: &mut self.active_layer_id, + tool_state: &mut self.tool_state, + pending_actions: &mut scratch.pending_actions, + draw_simplify_mode: &mut self.draw_simplify_mode, + rdp_tolerance: &mut self.rdp_tolerance, + schneider_max_error: &mut self.schneider_max_error, + raster_settings: &mut self.raster_settings, + audio_controller: self.audio_controller.as_ref(), + clip_snapshot: self.audio_controller.as_ref().map(|arc| { + arc.lock().unwrap().clip_snapshot() + }), + audio_input_opener: &mut self.audio_input, + audio_input_stream: &mut self.audio_input_stream, + audio_buffer_size: self.audio_buffer_size, + video_manager: &self.video_manager, + playback_time: &mut self.playback_time, + is_playing: &mut self.is_playing, + is_recording: &mut self.is_recording, + metronome_enabled: &mut self.metronome_enabled, + count_in_enabled: &mut self.count_in_enabled, + recording_clips: &mut self.recording_clips, + recording_start_time: &mut self.recording_start_time, + recording_layer_ids: &mut self.recording_layer_ids, + dragging_asset: &mut self.dragging_asset, + stroke_width: &mut self.stroke_width, + fill_enabled: &mut self.fill_enabled, + snap_enabled: &mut self.snap_enabled, + paint_bucket_gap_tolerance: &mut self.paint_bucket_gap_tolerance, + polygon_sides: &mut self.polygon_sides, + layer_to_track_map: &self.layer_to_track_map, + clip_instance_to_backend_map: &self.clip_instance_to_backend_map, + midi_event_cache: &mut self.midi_event_cache, + audio_pools_with_new_waveforms: &self.audio_pools_with_new_waveforms, + raw_audio_cache: &self.raw_audio_cache, + waveform_gpu_dirty: &mut self.waveform_gpu_dirty, + waveform_minmax_pools: &self.waveform_minmax_pools, + raster_fault_requests: &self.raster_fault_requests, + effect_to_load: &mut self.effect_to_load, + effect_thumbnail_requests: &mut scratch.effect_thumbnail_requests, + effect_thumbnail_cache: self.effect_thumbnail_generator.as_ref() + .map(|g| g.thumbnail_cache()) + .unwrap_or(&scratch.empty_thumbnail_cache), + effect_thumbnails_to_invalidate: &mut self.effect_thumbnails_to_invalidate, + webcam_frame: self.webcam_frame.clone(), + webcam_record_command: &mut self.webcam_record_command, + target_format: self.target_format, + pending_menu_actions: &mut scratch.pending_menu_actions, + clipboard_manager: &mut self.clipboard_manager, + input_level: self.input_level, + output_level: self.output_level, + track_levels: &self.track_levels, + track_to_layer_map: &self.track_to_layer_map, + waveform_stereo: self.config.waveform_stereo, + project_generation: &mut self.project_generation, + graph_topology_generation: &mut self.graph_topology_generation, + script_to_edit: &mut self.script_to_edit, + script_saved: &mut self.script_saved, + pending_region_cut_base: &mut self.pending_region_cut_base, + region_select_mode: &mut self.region_select_mode, + lasso_mode: &mut self.lasso_mode, + pending_graph_loads: &self.pending_graph_loads, + clipboard_consumed: &mut scratch.clipboard_consumed, + keymap: &self.keymap, + pending_node_group: &mut self.pending_node_group, + pending_node_ungroup: &mut self.pending_node_ungroup, + #[cfg(debug_assertions)] + test_mode: &mut self.test_mode, + #[cfg(debug_assertions)] + synthetic_input: &mut scratch.synthetic_input, + brush_preview_pixels: &self.brush_preview_pixels, + }, + pane_instances: &mut self.pane_instances, + }, + current_layout: &mut self.current_layout, + drag_state: &mut self.drag_state, + hovered_divider: &mut self.hovered_divider, + selected_pane: &mut self.selected_pane, + split_preview_mode: &mut self.split_preview_mode, + } + } +} + /// Find which GroupLayer (if any) contains the given layer as a direct child. /// Returns None if the layer is at document root level. fn find_parent_group_id(doc: &lightningbeam_core::document::Document, layer_id: &uuid::Uuid) -> Option { From 5ee2df5db762a53122f06e9751a2c60061c96fa2 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 29 Jun 2026 22:40:54 -0400 Subject: [PATCH 03/43] Add mobile UI shell scaffold behind LB_MOBILE_UI New src/mobile/ module renders a phone-style shell: top surface tabs (Stage/Time/Nodes/Mixer/Tree), a single full-bleed hero pane reusing the existing PaneInstance renderers, a resizable timeline ribbon (peek/half/full snap tiers riding above the floor), and a fixed transport bar wired to the audio controller (play/pause, timecode, project scrub). Gated entirely on the LB_MOBILE_UI env var: when set, main() opens a phone-aspect window and update() routes the central panel through render_mobile_shell instead of render_layout_node, sharing the same build_frame context and post-render drain. Desktop is unchanged when unset. Tabs mirror the pane list; per the wireframe the Toolbar and Infopanel do not become tabs (they become the omnibutton and inspector in later phases), and the Nodes surface is a placeholder pending the focus/patch rework. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/main.rs | 69 ++++-- .../lightningbeam-editor/src/mobile/mod.rs | 229 ++++++++++++++++++ .../lightningbeam-editor/src/mobile/ribbon.rs | 57 +++++ .../src/mobile/surface.rs | 33 +++ .../lightningbeam-editor/src/mobile/topbar.rs | 70 ++++++ .../src/mobile/transport.rs | 106 ++++++++ 6 files changed, 549 insertions(+), 15 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/surface.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 3bb7c96..81d8b10 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use clap::Parser; use uuid::Uuid; +mod mobile; mod panes; use panes::{PaneInstance, PaneRenderer}; @@ -197,8 +198,17 @@ fn main() -> eframe::Result { } }; + // When developing the mobile UI on desktop (LB_MOBILE_UI), open a phone-aspect window so + // the shell can be exercised at a realistic size. Gating the shell itself is done at + // render time on the flag, not on window aspect. + let initial_size = if mobile::is_mobile_env() { + [402.0, 874.0] // iPhone-ish portrait + } else { + [1920.0, 1080.0] + }; + let mut viewport_builder = egui::ViewportBuilder::default() - .with_inner_size([1920.0, 1080.0]) + .with_inner_size(initial_size) .with_title("Lightningbeam Editor") .with_app_id("lightningbeam-editor"); // Set app_id for Wayland @@ -942,6 +952,13 @@ struct EditorApp { hovered_divider: Option<(NodePath, bool)>, // (path, is_horizontal) selected_pane: Option, // Currently selected pane for editing split_preview_mode: SplitPreviewMode, + // === Mobile / phone UI (developed on desktop behind LB_MOBILE_UI) === + /// True if the mobile shell is requested via the env var (read once at startup). + mobile_ui: bool, + /// Runtime override of the mobile flag (None = follow `mobile_ui`). + mobile_ui_override: Option, + /// Persistent state for the mobile shell (active surface, ribbon tier). + mobile_state: mobile::MobileState, icon_cache: IconCache, tool_icon_cache: ToolIconCache, focus_icon_cache: FocusIconCache, // Focus card icons (start screen) @@ -1297,6 +1314,9 @@ impl EditorApp { hovered_divider: None, selected_pane: None, split_preview_mode: SplitPreviewMode::default(), + mobile_ui: mobile::is_mobile_env(), + mobile_ui_override: None, + mobile_state: mobile::MobileState::default(), icon_cache: IconCache::new(), tool_icon_cache: ToolIconCache::new(), focus_icon_cache: FocusIconCache::new(), @@ -6584,22 +6604,34 @@ impl eframe::App for EditorApp { } // Build the per-frame context (single source of truth for SharedPaneState, - // shared with the mobile path). The bundle borrows `self` + `scratch`; it - // is dropped before the post-render drain below re-touches them. + // shared by the desktop and mobile paths). The bundle borrows `self` + `scratch`; + // it is dropped before the post-render drain below re-touches them. + let is_mobile = self.mobile_active(); let mut bundle = self.build_frame(&mut scratch); - render_layout_node( - ui, - bundle.current_layout, - available_rect, - bundle.drag_state, - bundle.hovered_divider, - bundle.selected_pane, - &mut layout_action, - bundle.split_preview_mode, - &Vec::new(), // Root path - &mut bundle.rc, - ); + if is_mobile { + // Phone shell: a single hero surface + top tabs + resizable timeline ribbon + // + a fixed transport floor, reusing the same panes the desktop layout uses. + mobile::render_mobile_shell( + ui, + available_rect, + &mut bundle.rc, + bundle.mobile_state, + ); + } else { + render_layout_node( + ui, + bundle.current_layout, + available_rect, + bundle.drag_state, + bundle.hovered_divider, + bundle.selected_pane, + &mut layout_action, + bundle.split_preview_mode, + &Vec::new(), // Root path + &mut bundle.rc, + ); + } drop(bundle); // Process collected effect thumbnail requests @@ -7111,9 +7143,15 @@ struct FrameBundle<'a> { hovered_divider: &'a mut Option<(NodePath, bool)>, selected_pane: &'a mut Option, split_preview_mode: &'a mut SplitPreviewMode, + mobile_state: &'a mut mobile::MobileState, } impl EditorApp { + /// Whether the mobile shell is active this frame (runtime override wins over the env flag). + fn mobile_active(&self) -> bool { + self.mobile_ui_override.unwrap_or(self.mobile_ui) + } + /// Build the per-frame [`FrameBundle`]. The returned bundle borrows `self` and /// `scratch` for `'a`; it must be dropped before the post-render drain touches /// `self`/`scratch` again. This is the single source of truth for `SharedPaneState`. @@ -7225,6 +7263,7 @@ impl EditorApp { hovered_divider: &mut self.hovered_divider, selected_pane: &mut self.selected_pane, split_preview_mode: &mut self.split_preview_mode, + mobile_state: &mut self.mobile_state, } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs new file mode 100644 index 0000000..56c76a0 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -0,0 +1,229 @@ +//! Mobile / phone UI shell. +//! +//! Developed on desktop behind the `LB_MOBILE_UI` env var (see [`is_mobile_env`]). +//! The shell reuses the existing egui panes (`PaneInstance` / `render_content`) for its +//! "hero" surfaces, composing them as a single surface at a time with a top tab bar, a +//! resizable timeline ribbon, and a fixed transport "floor" at the bottom — per the +//! `phone-ui-sketches.html` wireframe spec. +//! +//! Not every pane becomes a surface: per the spec the Toolbar becomes the omnibutton and +//! the Infopanel becomes the selection inspector (later phases). The tab list here mirrors +//! the *mobile-relevant* subset of `PaneType`. + +use eframe::egui; +use lightningbeam_core::pane::PaneType; + +use crate::panes::NodePath; +use crate::RenderContext; + +mod ribbon; +mod surface; +mod topbar; +mod transport; + +/// Reserved sentinel namespace for mobile pane-instance paths. Desktop layout paths are +/// built from small child indices, so prefixing with `usize::MAX` guarantees mobile slots +/// never alias a real layout path in the shared `pane_instances` map. +pub const MOBILE_NS: usize = usize::MAX; + +const TOPBAR_H: f32 = 50.0; +const TRANSPORT_H: f32 = 60.0; +const GRABBER_H: f32 = 16.0; + +/// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. +/// Any non-empty value other than "0" enables it. +pub fn is_mobile_env() -> bool { + std::env::var("LB_MOBILE_UI") + .map(|v| !v.is_empty() && v != "0") + .unwrap_or(false) +} + +/// The hero surfaces selectable from the top tab bar. These mirror the pane list minus the +/// panes that become other mobile affordances (Toolbar → omnibutton, Infopanel → inspector). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MobileSurface { + Stage, + Time, + Nodes, + Mixer, + Tree, +} + +impl MobileSurface { + /// All surfaces, in tab order. + pub const TABS: [MobileSurface; 5] = [ + MobileSurface::Stage, + MobileSurface::Time, + MobileSurface::Nodes, + MobileSurface::Mixer, + MobileSurface::Tree, + ]; + + /// The existing pane that backs this surface. Phase 1 reuses panes directly; the Nodes + /// surface is a placeholder for the focus/patch rework (it currently shows the desktop + /// node editor) and Mixer maps to the closest existing audio pane. + pub fn pane_type(self) -> PaneType { + match self { + MobileSurface::Stage => PaneType::Stage, + MobileSurface::Time => PaneType::Timeline, + MobileSurface::Nodes => PaneType::NodeEditor, + MobileSurface::Mixer => PaneType::VirtualPiano, + MobileSurface::Tree => PaneType::Outliner, + } + } + + pub fn label(self) -> &'static str { + match self { + MobileSurface::Stage => "Stage", + MobileSurface::Time => "Time", + MobileSurface::Nodes => "Nodes", + MobileSurface::Mixer => "Mixer", + MobileSurface::Tree => "Tree", + } + } + + fn index(self) -> usize { + match self { + MobileSurface::Stage => 0, + MobileSurface::Time => 1, + MobileSurface::Nodes => 2, + MobileSurface::Mixer => 3, + MobileSurface::Tree => 4, + } + } + + /// Stable `pane_instances` key for this surface's cached pane. + fn path(self) -> NodePath { + vec![MOBILE_NS, self.index()] + } +} + +/// How far the timeline ribbon is expanded. The transport floor is always present, so the +/// ribbon never collapses to zero — `Peek` is the floor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RibbonTier { + Peek, + Half, + Full, +} + +impl RibbonTier { + /// Ribbon body height (excluding the grabber) for a given available middle-region + /// height (the space between the top bar and the transport floor). + fn height(self, region_h: f32) -> f32 { + match self { + RibbonTier::Peek => 96.0_f32.min(region_h * 0.45), + RibbonTier::Half => region_h * 0.45, + RibbonTier::Full => region_h * 0.72, + } + } + + fn snap_from(region_h: f32, target_h: f32) -> RibbonTier { + // Snap to whichever tier height is closest to the dragged target. + let candidates = [RibbonTier::Peek, RibbonTier::Half, RibbonTier::Full]; + *candidates + .iter() + .min_by(|a, b| { + let da = (a.height(region_h) - target_h).abs(); + let db = (b.height(region_h) - target_h).abs(); + da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap_or(&RibbonTier::Peek) + } +} + +/// Persistent mobile-shell UI state, cached on `EditorApp`. +pub struct MobileState { + pub active_surface: MobileSurface, + pub ribbon_tier: RibbonTier, + /// Transient live-drag offset (px) applied to the ribbon height while the grabber is + /// held; folded into `ribbon_tier` on release. + pub ribbon_drag: f32, +} + +impl Default for MobileState { + fn default() -> Self { + Self { + active_surface: MobileSurface::Stage, + ribbon_tier: RibbonTier::Peek, + ribbon_drag: 0.0, + } + } +} + +/// Render the whole mobile shell into `available_rect`. Reuses `rc` (the shared +/// `RenderContext`) for all pane content and `state` for the persistent shell state. +pub fn render_mobile_shell( + ui: &mut egui::Ui, + available_rect: egui::Rect, + rc: &mut RenderContext, + state: &mut MobileState, +) { + // Background (wireframe device color; the bands below paint over most of it). + let bg = egui::Color32::from_rgb(0x14, 0x16, 0x1b); + ui.painter().rect_filled(available_rect, 0.0, bg); + + let top = available_rect.top(); + let bottom = available_rect.bottom(); + let left = available_rect.left(); + let right = available_rect.right(); + + // Fixed bands. + let topbar_rect = egui::Rect::from_min_max( + egui::pos2(left, top), + egui::pos2(right, top + TOPBAR_H), + ); + let transport_rect = egui::Rect::from_min_max( + egui::pos2(left, bottom - TRANSPORT_H), + egui::pos2(right, bottom), + ); + + // Middle region between the top bar and the transport floor. + let region_top = topbar_rect.bottom(); + let region_bottom = transport_rect.top(); + let region_h = (region_bottom - region_top).max(0.0); + + // The ribbon is hidden when the Time surface is the hero (it already *is* the timeline). + let show_ribbon = state.active_surface != MobileSurface::Time && region_h > GRABBER_H + 40.0; + + let (hero_rect, ribbon_rects) = if show_ribbon { + let base_h = state.ribbon_tier.height(region_h); + let ribbon_h = (base_h + state.ribbon_drag) + .clamp(40.0, region_h - GRABBER_H - 80.0); + let grabber_top = region_bottom - ribbon_h - GRABBER_H; + let grabber_rect = egui::Rect::from_min_max( + egui::pos2(left, grabber_top), + egui::pos2(right, grabber_top + GRABBER_H), + ); + let ribbon_body = egui::Rect::from_min_max( + egui::pos2(left, grabber_rect.bottom()), + egui::pos2(right, region_bottom), + ); + let hero = egui::Rect::from_min_max( + egui::pos2(left, region_top), + egui::pos2(right, grabber_top), + ); + (hero, Some((grabber_rect, ribbon_body))) + } else { + let hero = egui::Rect::from_min_max( + egui::pos2(left, region_top), + egui::pos2(right, region_bottom), + ); + (hero, None) + }; + + // Hero surface (reuses an existing pane full-bleed). + let surface = state.active_surface; + surface::render_surface_fullbleed(ui, hero_rect, &surface.path(), surface.pane_type(), rc); + + // Resizable timeline ribbon. + if let Some((grabber_rect, ribbon_body)) = ribbon_rects { + ribbon::render(ui, grabber_rect, ribbon_body, region_h, state, rc); + } + + // Top tab bar (drawn after the hero so its hit area wins along the top edge). + topbar::render(ui, topbar_rect, state, rc); + + // Transport floor (drawn last = always on top, the persistent spine). + transport::render(ui, transport_rect, &mut rc.shared); +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs new file mode 100644 index 0000000..ee6a46a --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs @@ -0,0 +1,57 @@ +//! Resizable timeline ribbon. A grabber strip drags the ribbon through three snap tiers +//! (Peek / Half / Full); the ribbon body reuses the existing `TimelinePane` full-bleed. +//! The transport floor below it is always present, so the ribbon never collapses to zero. + +use eframe::egui; +use lightningbeam_core::pane::PaneType; + +use super::{surface, MobileState, RibbonTier, MOBILE_NS}; +use crate::panes::NodePath; +use crate::RenderContext; + +const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); + +/// Stable `pane_instances` key for the ribbon's timeline — distinct from the Time surface so +/// their zoom/scroll state don't fight. +fn ribbon_path() -> NodePath { + vec![MOBILE_NS, 100] +} + +pub fn render( + ui: &mut egui::Ui, + grabber_rect: egui::Rect, + ribbon_body: egui::Rect, + region_h: f32, + state: &mut MobileState, + rc: &mut RenderContext, +) { + // Grabber strip. + let painter = ui.painter_at(grabber_rect); + painter.rect_filled(grabber_rect, 0.0, C_PANEL); + painter.hline(grabber_rect.x_range(), grabber_rect.top(), egui::Stroke::new(1.0, C_LINE)); + // Handle pill. + let pill = egui::Rect::from_center_size(grabber_rect.center(), egui::vec2(34.0, 4.0)); + painter.rect_filled(pill, 2.0, C_LINE); + + let resp = ui.interact( + grabber_rect, + ui.id().with("mobile_ribbon_grabber"), + egui::Sense::drag(), + ); + if resp.dragged() { + // Dragging up (negative y) expands the ribbon. + state.ribbon_drag -= resp.drag_delta().y; + } + if resp.drag_stopped() { + let target_h = state.ribbon_tier.height(region_h) + state.ribbon_drag; + state.ribbon_tier = RibbonTier::snap_from(region_h, target_h); + state.ribbon_drag = 0.0; + } + if resp.hovered() || resp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); + } + + // Ribbon body = the timeline, full-bleed. + surface::render_surface_fullbleed(ui, ribbon_body, &ribbon_path(), PaneType::Timeline, rc); +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/surface.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/surface.rs new file mode 100644 index 0000000..7f2fbba --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/surface.rs @@ -0,0 +1,33 @@ +//! Full-bleed hero surface rendering: maps a mobile surface to an existing pane and renders +//! its content into a rect with no header/divider chrome. This is the de-chromed core of +//! `render_pane` (main.rs) — see the get-or-create + `render_content` pattern there. + +use eframe::egui; +use lightningbeam_core::pane::PaneType; + +use crate::panes::{NodePath, PaneInstance, PaneRenderer}; +use crate::RenderContext; + +/// Render `pane_type` full-bleed into `rect`, reusing (or creating) the cached pane instance +/// keyed by `path`. The pane clips its own drawing to `rect`, so no extra clipping is needed. +pub fn render_surface_fullbleed( + ui: &mut egui::Ui, + rect: egui::Rect, + path: &NodePath, + pane_type: PaneType, + rc: &mut RenderContext, +) { + // Get-or-create the instance for this slot (recreate if the type changed). + let needs_new = rc + .pane_instances + .get(path) + .map(|inst| inst.pane_type() != pane_type) + .unwrap_or(true); + if needs_new { + rc.pane_instances.insert(path.clone(), PaneInstance::new(pane_type)); + } + + if let Some(inst) = rc.pane_instances.get_mut(path) { + inst.render_content(ui, rect, path, &mut rc.shared); + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs new file mode 100644 index 0000000..d71b1b2 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs @@ -0,0 +1,70 @@ +//! Top surface-tab bar. One tap switches the hero surface. Mirrors the wireframe's top tabs +//! (Stage / Time / Nodes / Mixer / Tree); the active tab is marked with an amber underline. + +use eframe::egui; + +use super::{MobileState, MobileSurface}; +use crate::RenderContext; + +// Wireframe palette. +const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); + +pub fn render( + ui: &mut egui::Ui, + rect: egui::Rect, + state: &mut MobileState, + _rc: &mut RenderContext, +) { + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, C_PANEL); + // Bottom rule. + painter.hline(rect.x_range(), rect.bottom(), egui::Stroke::new(1.0, C_LINE)); + + let tabs = MobileSurface::TABS; + let tab_w = rect.width() / tabs.len() as f32; + + for (i, surface) in tabs.iter().enumerate() { + let tab_rect = egui::Rect::from_min_max( + egui::pos2(rect.left() + i as f32 * tab_w, rect.top()), + egui::pos2(rect.left() + (i as f32 + 1.0) * tab_w, rect.bottom()), + ); + let id = ui.id().with(("mobile_tab", i)); + let resp = ui.interact(tab_rect, id, egui::Sense::click()); + if resp.clicked() { + state.active_surface = *surface; + } + + let active = state.active_surface == *surface; + let color = if active { + C_AMBER + } else if resp.hovered() { + C_BRIGHT + } else { + C_DIM + }; + painter.text( + tab_rect.center(), + egui::Align2::CENTER_CENTER, + surface.label(), + egui::FontId::proportional(13.0), + color, + ); + if active { + // Underline marking the active surface. + let y = tab_rect.bottom() - 1.5; + painter.hline( + (tab_rect.left() + 10.0)..=(tab_rect.right() - 10.0), + y, + egui::Stroke::new(2.0, C_AMBER), + ); + } + // Vertical separators between tabs. + if i > 0 { + painter.vline(tab_rect.left(), rect.y_range(), egui::Stroke::new(1.0, C_LINE)); + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs new file mode 100644 index 0000000..1623b72 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs @@ -0,0 +1,106 @@ +//! The transport "floor": the always-present bottom bar with play/pause, the timecode, and a +//! project scrub. Wired to the audio controller exactly like `TimelinePane`'s header. + +use eframe::egui; + +use crate::panes::SharedPaneState; + +const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_DARK: egui::Color32 = egui::Color32::from_rgb(0x1b, 0x13, 0x0a); + +pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) { + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, C_PANEL); + painter.hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, C_LINE)); + + let cy = rect.center().y; + + // --- Play / pause button (circle on the left) --- + let btn_r = 18.0; + let btn_center = egui::pos2(rect.left() + 20.0 + btn_r, cy); + let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0)); + let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click()); + painter.circle_filled(btn_center, btn_r, C_AMBER); + let glyph = if *shared.is_playing { "⏸" } else { "▶" }; + painter.text( + btn_center, + egui::Align2::CENTER_CENTER, + glyph, + egui::FontId::proportional(16.0), + C_DARK, + ); + if btn_resp.clicked() { + *shared.is_playing = !*shared.is_playing; + if let Some(controller_arc) = shared.audio_controller { + let mut controller = controller_arc.lock().unwrap(); + if *shared.is_playing { + controller.seek(*shared.playback_time); + controller.play(); + } else { + controller.pause(); + } + } + } + + // --- Timecode (MM:SS:FF) --- + let fps = shared.action_executor.document().framerate.max(1.0); + let tc = format_timecode(*shared.playback_time, fps); + let tc_left = btn_rect.right() + 12.0; + painter.text( + egui::pos2(tc_left, cy), + egui::Align2::LEFT_CENTER, + &tc, + egui::FontId::monospace(13.0), + C_BRIGHT, + ); + let tc_width = 78.0; + + // --- Project scrub (fills the remaining width) --- + let duration = shared.action_executor.document().duration.max(1.0); + let scrub_left = tc_left + tc_width; + let scrub_rect = egui::Rect::from_min_max( + egui::pos2(scrub_left, cy - 3.0), + egui::pos2(rect.right() - 14.0, cy + 3.0), + ); + let scrub_resp = ui.interact( + scrub_rect.expand2(egui::vec2(0.0, 12.0)), + ui.id().with("mobile_transport_scrub"), + egui::Sense::click_and_drag(), + ); + painter.rect_filled(scrub_rect, 3.0, C_LINE); + let frac = (*shared.playback_time / duration).clamp(0.0, 1.0) as f32; + let filled = egui::Rect::from_min_max( + scrub_rect.min, + egui::pos2(scrub_rect.left() + scrub_rect.width() * frac, scrub_rect.bottom()), + ); + painter.rect_filled(filled, 3.0, C_AMBER.gamma_multiply(0.5)); + let head_x = scrub_rect.left() + scrub_rect.width() * frac; + painter.vline( + head_x, + (scrub_rect.top() - 4.0)..=(scrub_rect.bottom() + 4.0), + egui::Stroke::new(2.0, C_AMBER), + ); + + if (scrub_resp.dragged() || scrub_resp.clicked()) && scrub_rect.width() > 0.0 { + if let Some(pos) = scrub_resp.interact_pointer_pos() { + let f = ((pos.x - scrub_rect.left()) / scrub_rect.width()).clamp(0.0, 1.0) as f64; + let new_time = f * duration; + *shared.playback_time = new_time; + if let Some(controller_arc) = shared.audio_controller { + let mut controller = controller_arc.lock().unwrap(); + controller.seek(new_time); + } + } + } +} + +fn format_timecode(seconds: f64, fps: f64) -> String { + let total = seconds.max(0.0); + let minutes = (total / 60.0).floor() as u32; + let secs = (total % 60.0).floor() as u32; + let frames = ((total - total.floor()) * fps).floor() as u32; + format!("{:02}:{:02}:{:02}", minutes, secs, frames) +} From 79f063d00c4a22a7fe3ae4ca588588a9c21d8fa9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 29 Jun 2026 23:57:29 -0400 Subject: [PATCH 04/43] Replace mobile tabbed shell with vertical sliding-window stack Per design feedback, drop the top-tabs + single-ribbon model. The mobile UI is now one vertical stack of all panes in a fixed order (Outliner, Asset Library, Stage, Timeline, Piano Roll, Virtual Piano, Node/Instrument, Script editor) with a window of 2-3 consecutive panes visible at a time. Dragging the dividers between panes and the top/bottom screen edges grows, shrinks, or slides the window via a small state machine (R1-R6 in stack.rs), with continuous interpolation during the drag and snap-on-release. The Node/Instrument band toggles between the node editor and the preset browser. The transport stays pinned as the bottom floor. Each visible band reuses PaneInstance::render_content full-bleed (surface.rs). Removes topbar.rs and ribbon.rs; rewrites mod.rs state (window_top, window_count, show_instruments, drag); adds stack.rs. --- .../lightningbeam-editor/src/mobile/mod.rs | 263 ++++++--------- .../lightningbeam-editor/src/mobile/ribbon.rs | 57 ---- .../lightningbeam-editor/src/mobile/stack.rs | 310 ++++++++++++++++++ .../lightningbeam-editor/src/mobile/topbar.rs | 70 ---- 4 files changed, 411 insertions(+), 289 deletions(-) delete mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs delete mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 56c76a0..907afca 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -1,14 +1,15 @@ //! Mobile / phone UI shell. //! //! Developed on desktop behind the `LB_MOBILE_UI` env var (see [`is_mobile_env`]). -//! The shell reuses the existing egui panes (`PaneInstance` / `render_content`) for its -//! "hero" surfaces, composing them as a single surface at a time with a top tab bar, a -//! resizable timeline ribbon, and a fixed transport "floor" at the bottom — per the -//! `phone-ui-sketches.html` wireframe spec. //! -//! Not every pane becomes a surface: per the spec the Toolbar becomes the omnibutton and -//! the Infopanel becomes the selection inspector (later phases). The tab list here mirrors -//! the *mobile-relevant* subset of `PaneType`. +//! The shell is a single **vertical sliding-window stack** of the existing panes: a fixed +//! top→bottom order, with a window of 2 or 3 consecutive panes visible at once. Dragging the +//! dividers between panes and the top/bottom screen edges grows, shrinks, or slides the window +//! (the R1–R6 state machine in `stack.rs`). A transport bar is pinned at the bottom as the floor. +//! +//! Each visible pane reuses the existing `PaneInstance::render_content` full-bleed (see +//! `surface.rs`). Per the wireframe, the Toolbar becomes the omnibutton and the Infopanel becomes +//! the selection inspector (later phases), so neither is in the stack. use eframe::egui; use lightningbeam_core::pane::PaneType; @@ -16,19 +17,16 @@ use lightningbeam_core::pane::PaneType; use crate::panes::NodePath; use crate::RenderContext; -mod ribbon; +mod stack; mod surface; -mod topbar; mod transport; -/// Reserved sentinel namespace for mobile pane-instance paths. Desktop layout paths are -/// built from small child indices, so prefixing with `usize::MAX` guarantees mobile slots -/// never alias a real layout path in the shared `pane_instances` map. +/// Reserved sentinel namespace for mobile pane-instance paths. Desktop layout paths are built +/// from small child indices, so prefixing with `usize::MAX` guarantees mobile slots never alias a +/// real layout path in the shared `pane_instances` map. pub const MOBILE_NS: usize = usize::MAX; -const TOPBAR_H: f32 = 50.0; const TRANSPORT_H: f32 = 60.0; -const GRABBER_H: f32 = 16.0; /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Any non-empty value other than "0" enables it. @@ -38,192 +36,133 @@ pub fn is_mobile_env() -> bool { .unwrap_or(false) } -/// The hero surfaces selectable from the top tab bar. These mirror the pane list minus the -/// panes that become other mobile affordances (Toolbar → omnibutton, Infopanel → inspector). +/// The panes that make up the vertical stack, in fixed top→bottom order. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MobileSurface { +pub enum StackPane { + Outliner, + AssetLibrary, Stage, - Time, - Nodes, - Mixer, - Tree, + Timeline, + PianoRoll, + VirtualPiano, + /// Node editor, or (toggled) the instrument/preset browser. + NodeInstrument, + ScriptEditor, } -impl MobileSurface { - /// All surfaces, in tab order. - pub const TABS: [MobileSurface; 5] = [ - MobileSurface::Stage, - MobileSurface::Time, - MobileSurface::Nodes, - MobileSurface::Mixer, - MobileSurface::Tree, - ]; +/// The stack order. Index into this is a pane's stable slot id. +pub const STACK: [StackPane; 8] = [ + StackPane::Outliner, + StackPane::AssetLibrary, + StackPane::Stage, + StackPane::Timeline, + StackPane::PianoRoll, + StackPane::VirtualPiano, + StackPane::NodeInstrument, + StackPane::ScriptEditor, +]; - /// The existing pane that backs this surface. Phase 1 reuses panes directly; the Nodes - /// surface is a placeholder for the focus/patch rework (it currently shows the desktop - /// node editor) and Mixer maps to the closest existing audio pane. - pub fn pane_type(self) -> PaneType { +impl StackPane { + /// The backing pane type. The Node/Instrument slot picks NodeEditor or PresetBrowser based on + /// the toggle. + pub fn pane_type(self, show_instruments: bool) -> PaneType { match self { - MobileSurface::Stage => PaneType::Stage, - MobileSurface::Time => PaneType::Timeline, - MobileSurface::Nodes => PaneType::NodeEditor, - MobileSurface::Mixer => PaneType::VirtualPiano, - MobileSurface::Tree => PaneType::Outliner, + StackPane::Outliner => PaneType::Outliner, + StackPane::AssetLibrary => PaneType::AssetLibrary, + StackPane::Stage => PaneType::Stage, + StackPane::Timeline => PaneType::Timeline, + StackPane::PianoRoll => PaneType::PianoRoll, + StackPane::VirtualPiano => PaneType::VirtualPiano, + StackPane::NodeInstrument => { + if show_instruments { + PaneType::PresetBrowser + } else { + PaneType::NodeEditor + } + } + StackPane::ScriptEditor => PaneType::ScriptEditor, } } - pub fn label(self) -> &'static str { + pub fn label(self, show_instruments: bool) -> &'static str { match self { - MobileSurface::Stage => "Stage", - MobileSurface::Time => "Time", - MobileSurface::Nodes => "Nodes", - MobileSurface::Mixer => "Mixer", - MobileSurface::Tree => "Tree", + StackPane::Outliner => "Outliner", + StackPane::AssetLibrary => "Assets", + StackPane::Stage => "Stage", + StackPane::Timeline => "Timeline", + StackPane::PianoRoll => "Piano Roll", + StackPane::VirtualPiano => "Keys", + StackPane::NodeInstrument => { + if show_instruments { + "Instruments" + } else { + "Nodes" + } + } + StackPane::ScriptEditor => "Script", } } - - fn index(self) -> usize { - match self { - MobileSurface::Stage => 0, - MobileSurface::Time => 1, - MobileSurface::Nodes => 2, - MobileSurface::Mixer => 3, - MobileSurface::Tree => 4, - } - } - - /// Stable `pane_instances` key for this surface's cached pane. - fn path(self) -> NodePath { - vec![MOBILE_NS, self.index()] - } } -/// How far the timeline ribbon is expanded. The transport floor is always present, so the -/// ribbon never collapses to zero — `Peek` is the floor. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RibbonTier { - Peek, - Half, - Full, +/// `pane_instances` key for a stack slot. +fn slot_path(slot: usize) -> NodePath { + vec![MOBILE_NS, slot] } -impl RibbonTier { - /// Ribbon body height (excluding the grabber) for a given available middle-region - /// height (the space between the top bar and the transport floor). - fn height(self, region_h: f32) -> f32 { - match self { - RibbonTier::Peek => 96.0_f32.min(region_h * 0.45), - RibbonTier::Half => region_h * 0.45, - RibbonTier::Full => region_h * 0.72, - } - } - - fn snap_from(region_h: f32, target_h: f32) -> RibbonTier { - // Snap to whichever tier height is closest to the dragged target. - let candidates = [RibbonTier::Peek, RibbonTier::Half, RibbonTier::Full]; - *candidates - .iter() - .min_by(|a, b| { - let da = (a.height(region_h) - target_h).abs(); - let db = (b.height(region_h) - target_h).abs(); - da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) - }) - .unwrap_or(&RibbonTier::Peek) - } +/// An in-progress drag of a stack handle. +#[derive(Debug, Clone, Copy)] +pub struct StackDrag { + pub handle: stack::Handle, + /// Accumulated pointer offset (px) since the drag began (downward positive). + pub offset: f32, } -/// Persistent mobile-shell UI state, cached on `EditorApp`. +/// Persistent mobile-shell state, cached on `EditorApp`. pub struct MobileState { - pub active_surface: MobileSurface, - pub ribbon_tier: RibbonTier, - /// Transient live-drag offset (px) applied to the ribbon height while the grabber is - /// held; folded into `ribbon_tier` on release. - pub ribbon_drag: f32, + /// Index into `STACK` of the topmost visible pane. + pub window_top: usize, + /// Number of visible panes (2 or 3). + pub window_count: usize, + /// Node/Instrument band: false = node editor, true = instrument/preset browser. + pub show_instruments: bool, + /// Active handle drag (transient). + pub drag: Option, } impl Default for MobileState { fn default() -> Self { Self { - active_surface: MobileSurface::Stage, - ribbon_tier: RibbonTier::Peek, - ribbon_drag: 0.0, + // Launch on {Stage, Timeline}. + window_top: 2, + window_count: 2, + show_instruments: false, + drag: None, } } } -/// Render the whole mobile shell into `available_rect`. Reuses `rc` (the shared -/// `RenderContext`) for all pane content and `state` for the persistent shell state. +/// Render the whole mobile shell into `available_rect`. pub fn render_mobile_shell( ui: &mut egui::Ui, available_rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, ) { - // Background (wireframe device color; the bands below paint over most of it). - let bg = egui::Color32::from_rgb(0x14, 0x16, 0x1b); - ui.painter().rect_filled(available_rect, 0.0, bg); + // Background (wireframe device color; bands paint over most of it). + ui.painter() + .rect_filled(available_rect, 0.0, egui::Color32::from_rgb(0x14, 0x16, 0x1b)); - let top = available_rect.top(); - let bottom = available_rect.bottom(); - let left = available_rect.left(); - let right = available_rect.right(); - - // Fixed bands. - let topbar_rect = egui::Rect::from_min_max( - egui::pos2(left, top), - egui::pos2(right, top + TOPBAR_H), - ); let transport_rect = egui::Rect::from_min_max( - egui::pos2(left, bottom - TRANSPORT_H), - egui::pos2(right, bottom), + egui::pos2(available_rect.left(), available_rect.bottom() - TRANSPORT_H), + available_rect.max, + ); + let stack_rect = egui::Rect::from_min_max( + available_rect.min, + egui::pos2(available_rect.right(), transport_rect.top()), ); - // Middle region between the top bar and the transport floor. - let region_top = topbar_rect.bottom(); - let region_bottom = transport_rect.top(); - let region_h = (region_bottom - region_top).max(0.0); + stack::render(ui, stack_rect, rc, state); - // The ribbon is hidden when the Time surface is the hero (it already *is* the timeline). - let show_ribbon = state.active_surface != MobileSurface::Time && region_h > GRABBER_H + 40.0; - - let (hero_rect, ribbon_rects) = if show_ribbon { - let base_h = state.ribbon_tier.height(region_h); - let ribbon_h = (base_h + state.ribbon_drag) - .clamp(40.0, region_h - GRABBER_H - 80.0); - let grabber_top = region_bottom - ribbon_h - GRABBER_H; - let grabber_rect = egui::Rect::from_min_max( - egui::pos2(left, grabber_top), - egui::pos2(right, grabber_top + GRABBER_H), - ); - let ribbon_body = egui::Rect::from_min_max( - egui::pos2(left, grabber_rect.bottom()), - egui::pos2(right, region_bottom), - ); - let hero = egui::Rect::from_min_max( - egui::pos2(left, region_top), - egui::pos2(right, grabber_top), - ); - (hero, Some((grabber_rect, ribbon_body))) - } else { - let hero = egui::Rect::from_min_max( - egui::pos2(left, region_top), - egui::pos2(right, region_bottom), - ); - (hero, None) - }; - - // Hero surface (reuses an existing pane full-bleed). - let surface = state.active_surface; - surface::render_surface_fullbleed(ui, hero_rect, &surface.path(), surface.pane_type(), rc); - - // Resizable timeline ribbon. - if let Some((grabber_rect, ribbon_body)) = ribbon_rects { - ribbon::render(ui, grabber_rect, ribbon_body, region_h, state, rc); - } - - // Top tab bar (drawn after the hero so its hit area wins along the top edge). - topbar::render(ui, topbar_rect, state, rc); - - // Transport floor (drawn last = always on top, the persistent spine). + // Transport floor: drawn last = always on top, the persistent spine. transport::render(ui, transport_rect, &mut rc.shared); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs deleted file mode 100644 index ee6a46a..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/ribbon.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Resizable timeline ribbon. A grabber strip drags the ribbon through three snap tiers -//! (Peek / Half / Full); the ribbon body reuses the existing `TimelinePane` full-bleed. -//! The transport floor below it is always present, so the ribbon never collapses to zero. - -use eframe::egui; -use lightningbeam_core::pane::PaneType; - -use super::{surface, MobileState, RibbonTier, MOBILE_NS}; -use crate::panes::NodePath; -use crate::RenderContext; - -const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); - -/// Stable `pane_instances` key for the ribbon's timeline — distinct from the Time surface so -/// their zoom/scroll state don't fight. -fn ribbon_path() -> NodePath { - vec![MOBILE_NS, 100] -} - -pub fn render( - ui: &mut egui::Ui, - grabber_rect: egui::Rect, - ribbon_body: egui::Rect, - region_h: f32, - state: &mut MobileState, - rc: &mut RenderContext, -) { - // Grabber strip. - let painter = ui.painter_at(grabber_rect); - painter.rect_filled(grabber_rect, 0.0, C_PANEL); - painter.hline(grabber_rect.x_range(), grabber_rect.top(), egui::Stroke::new(1.0, C_LINE)); - // Handle pill. - let pill = egui::Rect::from_center_size(grabber_rect.center(), egui::vec2(34.0, 4.0)); - painter.rect_filled(pill, 2.0, C_LINE); - - let resp = ui.interact( - grabber_rect, - ui.id().with("mobile_ribbon_grabber"), - egui::Sense::drag(), - ); - if resp.dragged() { - // Dragging up (negative y) expands the ribbon. - state.ribbon_drag -= resp.drag_delta().y; - } - if resp.drag_stopped() { - let target_h = state.ribbon_tier.height(region_h) + state.ribbon_drag; - state.ribbon_tier = RibbonTier::snap_from(region_h, target_h); - state.ribbon_drag = 0.0; - } - if resp.hovered() || resp.dragged() { - ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); - } - - // Ribbon body = the timeline, full-bleed. - surface::render_surface_fullbleed(ui, ribbon_body, &ribbon_path(), PaneType::Timeline, rc); -} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs new file mode 100644 index 0000000..39ca14b --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -0,0 +1,310 @@ +//! The vertical sliding-window stack engine. +//! +//! A window of 2–3 consecutive panes (from [`super::STACK`]) is visible. Dragging the dividers +//! between visible panes and the top/bottom screen edges grows, shrinks, or slides the window via +//! the operations below (verified against the user's transition table): +//! +//! ```text +//! R1 upper divider, UP -> slide window DOWN one (top+=1) +//! R4 upper divider, DOWN -> drop BOTTOM pane (count 3->2) +//! R3 lower divider, UP -> drop TOP pane (top+=1, count 3->2) +//! R2 lower divider, DOWN -> slide window UP one (top-=1) +//! R5 bottom edge, UP -> grow at bottom; if already 3, slide down +//! R6 top edge, DOWN -> grow at top; if already 3, slide up +//! ``` +//! +//! At count==2 the single divider is "upper" when dragged up (R1) and "lower" when dragged +//! down (R2). The window content interpolates continuously during a drag and snaps on release. + +use eframe::egui; + +use super::{slot_path, MobileState, StackDrag, StackPane, STACK}; +use crate::RenderContext; + +const N: usize = STACK.len(); + +const EDGE_GRAB_H: f32 = 16.0; +const DIV_GRAB_H: f32 = 18.0; + +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); +const C_CHIP_BG: egui::Color32 = egui::Color32::from_rgba_premultiplied(0x1b, 0x1f, 0x27, 0xcc); + +/// A draggable boundary of the stack. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Handle { + TopEdge, + /// Divider below visible pane `k` (between visible pane `k` and `k+1`). + Divider(usize), + BottomEdge, +} + +// --- R1..R6 as pure ops on (top, count) -> Option<(top, count)> --- + +fn op_r1(top: usize, count: usize) -> Option<(usize, usize)> { + (top + count < N).then_some((top + 1, count)) // slide down +} +fn op_r2(top: usize, count: usize) -> Option<(usize, usize)> { + (top > 0).then_some((top - 1, count)) // slide up +} +fn op_r3(top: usize, count: usize) -> Option<(usize, usize)> { + (count == 3).then_some((top + 1, 2)) // drop top +} +fn op_r4(top: usize, count: usize) -> Option<(usize, usize)> { + (count == 3).then_some((top, 2)) // drop bottom +} +fn op_r5(top: usize, count: usize) -> Option<(usize, usize)> { + if count < 3 && top + count < N { + Some((top, count + 1)) // grow bottom + } else if count == 3 && top + count < N { + Some((top + 1, count)) // slide down + } else { + None + } +} +fn op_r6(top: usize, count: usize) -> Option<(usize, usize)> { + if count < 3 && top > 0 { + Some((top - 1, count + 1)) // grow top + } else if count == 3 && top > 0 { + Some((top - 1, count)) // slide up + } else { + None + } +} + +/// Given a handle and signed drag offset, resolve the target window config and progress `t`. +/// Returns None if the drag direction has no valid operation (e.g. at a list boundary). +fn resolve( + handle: Handle, + offset: f32, + top: usize, + count: usize, + pane_h: f32, +) -> Option<(usize, usize, f32)> { + let going_up = offset < 0.0; + let t = (offset.abs() / pane_h.max(1.0)).clamp(0.0, 1.0); + let target = match handle { + Handle::TopEdge => (!going_up).then(|| op_r6(top, count)).flatten(), + Handle::BottomEdge => going_up.then(|| op_r5(top, count)).flatten(), + Handle::Divider(k) => { + if count == 2 { + if going_up { op_r1(top, count) } else { op_r2(top, count) } + } else if k == 0 { + // upper divider + if going_up { op_r1(top, count) } else { op_r4(top, count) } + } else { + // lower divider + if going_up { op_r3(top, count) } else { op_r2(top, count) } + } + } + }; + target.map(|(tt, tc)| (tt, tc, t)) +} + +// --- rect layout --- + +fn config_rects(top: usize, count: usize, rect: egui::Rect) -> Vec<(usize, egui::Rect)> { + let h = rect.height() / count as f32; + (0..count) + .map(|i| { + let y0 = rect.top() + i as f32 * h; + ( + top + i, + egui::Rect::from_min_max( + egui::pos2(rect.left(), y0), + egui::pos2(rect.right(), y0 + h), + ), + ) + }) + .collect() +} + +fn lerp_f(a: f32, b: f32, t: f32) -> f32 { + a + (b - a) * t +} +fn lerp_rect(a: egui::Rect, b: egui::Rect, t: f32) -> egui::Rect { + egui::Rect::from_min_max( + egui::pos2(lerp_f(a.min.x, b.min.x, t), lerp_f(a.min.y, b.min.y, t)), + egui::pos2(lerp_f(a.max.x, b.max.x, t), lerp_f(a.max.y, b.max.y, t)), + ) +} +fn collapsed(rect: egui::Rect, at_top: bool) -> egui::Rect { + let y = if at_top { rect.top() } else { rect.bottom() }; + egui::Rect::from_min_max(egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)) +} + +/// Interpolate the visible-pane rects from config C toward config T by progress `t`. +fn interp_rects( + (top_c, count_c): (usize, usize), + (top_t, count_t): (usize, usize), + t: f32, + rect: egui::Rect, +) -> Vec<(usize, egui::Rect)> { + let c = config_rects(top_c, count_c, rect); + let tt = config_rects(top_t, count_t, rect); + let find = |v: &[(usize, egui::Rect)], slot: usize| v.iter().find(|(s, _)| *s == slot).map(|(_, r)| *r); + + let lo = top_c.min(top_t); + let hi = (top_c + count_c).max(top_t + count_t); + let mut out = Vec::new(); + for slot in lo..hi { + let in_c = find(&c, slot); + let in_t = find(&tt, slot); + let r = match (in_c, in_t) { + (Some(rc), Some(rt)) => lerp_rect(rc, rt, t), + (Some(rc), None) => lerp_rect(rc, collapsed(rect, slot < top_t), t), // exiting + (None, Some(rt)) => lerp_rect(collapsed(rect, slot < top_c), rt, t), // entering + (None, None) => continue, + }; + out.push((slot, r)); + } + out +} + +// --- rendering --- + +pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { + let top = state.window_top; + let count = state.window_count; + let pane_h = rect.height() / count as f32; + + // Layout from the (previous frame's) drag, if any has a valid op; else the resting config. + let layout = state + .drag + .and_then(|d| resolve(d.handle, d.offset, top, count, pane_h)) + .map(|(tt, tc, t)| interp_rects((top, count), (tt, tc), t, rect)) + .unwrap_or_else(|| config_rects(top, count, rect)); + + // 1) Pane content (top to bottom). + for (slot, prect) in &layout { + if prect.height() < 1.0 { + continue; + } + let sp = STACK[*slot]; + super::surface::render_surface_fullbleed( + ui, + *prect, + &slot_path(*slot), + sp.pane_type(state.show_instruments), + rc, + ); + } + + // 2) Per-band label chips + the Node/Instrument toggle (drawn over content). + for (slot, prect) in &layout { + if prect.height() < 24.0 { + continue; + } + draw_band_chip(ui, *prect, STACK[*slot], state); + } + + // 3) Handles (interacted last so they win the initial press on their thin strips). + handle_interactions(ui, rect, state); +} + +fn draw_band_chip(ui: &mut egui::Ui, prect: egui::Rect, sp: StackPane, state: &mut MobileState) { + let label = sp.label(state.show_instruments); + let pos = prect.left_top() + egui::vec2(8.0, 6.0); + let galley = ui.painter().layout_no_wrap( + label.to_string(), + egui::FontId::proportional(11.0), + C_DIM, + ); + let chip = egui::Rect::from_min_size(pos, galley.size() + egui::vec2(12.0, 5.0)); + ui.painter().rect_filled(chip, 4.0, C_CHIP_BG); + ui.painter() + .galley(chip.min + egui::vec2(6.0, 2.0), galley, C_DIM); + + // Node/Instrument toggle sits just to the right of the label chip. + if sp == StackPane::NodeInstrument { + let tog = egui::Rect::from_min_size( + egui::pos2(chip.right() + 6.0, chip.top()), + egui::vec2(24.0, chip.height()), + ); + let resp = ui.interact(tog, ui.id().with("mobile_node_toggle"), egui::Sense::click()); + ui.painter().rect_filled(tog, 4.0, C_CHIP_BG); + ui.painter().text( + tog.center(), + egui::Align2::CENTER_CENTER, + "⇄", + egui::FontId::proportional(13.0), + if resp.hovered() { C_AMBER } else { C_DIM }, + ); + if resp.clicked() { + state.show_instruments = !state.show_instruments; + } + } +} + +fn handle_key(h: Handle) -> (usize, usize) { + match h { + Handle::TopEdge => (0, 0), + Handle::Divider(k) => (1, k + 1), + Handle::BottomEdge => (2, 0), + } +} + +fn handle_interactions(ui: &mut egui::Ui, rect: egui::Rect, state: &mut MobileState) { + let count = state.window_count; + let pane_h = rect.height() / count as f32; + + let mut handles: Vec<(Handle, egui::Rect)> = Vec::new(); + handles.push(( + Handle::TopEdge, + egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.right(), rect.top() + EDGE_GRAB_H)), + )); + for k in 0..count.saturating_sub(1) { + let y = rect.top() + (k + 1) as f32 * pane_h; + handles.push(( + Handle::Divider(k), + egui::Rect::from_min_max( + egui::pos2(rect.left(), y - DIV_GRAB_H * 0.5), + egui::pos2(rect.right(), y + DIV_GRAB_H * 0.5), + ), + )); + } + handles.push(( + Handle::BottomEdge, + egui::Rect::from_min_max(egui::pos2(rect.left(), rect.bottom() - EDGE_GRAB_H), rect.max), + )); + + for (handle, hrect) in handles { + let id = ui.id().with(("mobile_stack_handle", handle_key(handle))); + let resp = ui.interact(hrect, id, egui::Sense::drag()); + + let active = state.drag.map(|d| d.handle == handle).unwrap_or(false); + // Grab pill. + let pill = egui::Rect::from_center_size(hrect.center(), egui::vec2(40.0, 4.0)); + let pill_col = if resp.hovered() || active { C_AMBER } else { C_LINE }; + ui.painter().rect_filled(pill, 2.0, pill_col); + + if resp.drag_started() { + state.drag = Some(StackDrag { handle, offset: 0.0 }); + } + if resp.dragged() { + if let Some(d) = &mut state.drag { + if d.handle == handle { + d.offset += resp.drag_delta().y; + } + } + } + if resp.drag_stopped() { + if let Some(d) = state.drag.take() { + commit_drag(d, state, pane_h); + } + } + if resp.hovered() || active { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); + } + } +} + +fn commit_drag(d: StackDrag, state: &mut MobileState, pane_h: f32) { + if let Some((tt, tc, t)) = resolve(d.handle, d.offset, state.window_top, state.window_count, pane_h) { + if t >= 0.5 { + state.window_top = tt; + state.window_count = tc; + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs deleted file mode 100644 index d71b1b2..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Top surface-tab bar. One tap switches the hero surface. Mirrors the wireframe's top tabs -//! (Stage / Time / Nodes / Mixer / Tree); the active tab is marked with an amber underline. - -use eframe::egui; - -use super::{MobileState, MobileSurface}; -use crate::RenderContext; - -// Wireframe palette. -const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); - -pub fn render( - ui: &mut egui::Ui, - rect: egui::Rect, - state: &mut MobileState, - _rc: &mut RenderContext, -) { - let painter = ui.painter_at(rect); - painter.rect_filled(rect, 0.0, C_PANEL); - // Bottom rule. - painter.hline(rect.x_range(), rect.bottom(), egui::Stroke::new(1.0, C_LINE)); - - let tabs = MobileSurface::TABS; - let tab_w = rect.width() / tabs.len() as f32; - - for (i, surface) in tabs.iter().enumerate() { - let tab_rect = egui::Rect::from_min_max( - egui::pos2(rect.left() + i as f32 * tab_w, rect.top()), - egui::pos2(rect.left() + (i as f32 + 1.0) * tab_w, rect.bottom()), - ); - let id = ui.id().with(("mobile_tab", i)); - let resp = ui.interact(tab_rect, id, egui::Sense::click()); - if resp.clicked() { - state.active_surface = *surface; - } - - let active = state.active_surface == *surface; - let color = if active { - C_AMBER - } else if resp.hovered() { - C_BRIGHT - } else { - C_DIM - }; - painter.text( - tab_rect.center(), - egui::Align2::CENTER_CENTER, - surface.label(), - egui::FontId::proportional(13.0), - color, - ); - if active { - // Underline marking the active surface. - let y = tab_rect.bottom() - 1.5; - painter.hline( - (tab_rect.left() + 10.0)..=(tab_rect.right() - 10.0), - y, - egui::Stroke::new(2.0, C_AMBER), - ); - } - // Vertical separators between tabs. - if i > 0 { - painter.vline(tab_rect.left(), rect.y_range(), egui::Stroke::new(1.0, C_LINE)); - } - } -} From 0c59ed8b4b12af81247402fb3ace67b7f4a8f9f3 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 00:13:08 -0400 Subject: [PATCH 05/43] Fix stack overflow at list ends; full-height drag headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix panic "attempt to subtract with overflow" when sliding to the end: the R-ops used then_some(), which eagerly evaluates top-1 even when the guard is false. Switch to lazy .then(|| ...). - Replace the thin divider/edge grab bars with full-width header bars: each band has a labeled header (top of band) that is its drag handle, plus a reserved bottom footer for the BottomEdge "pull up for more" handle. Band 0's header = TopEdge, band i's header = the divider above it, footer = BottomEdge. The Node/Instrument ⇄ toggle lives in that band's header. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/mobile/stack.rs | 210 +++++++++++------- 1 file changed, 125 insertions(+), 85 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 39ca14b..0db8bb7 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -23,13 +23,16 @@ use crate::RenderContext; const N: usize = STACK.len(); -const EDGE_GRAB_H: f32 = 16.0; -const DIV_GRAB_H: f32 = 18.0; +/// Height of each band's drag header (and the bottom-edge footer). The whole header is the grab +/// target — there's no thin divider bar. +const HEADER_H: f32 = 26.0; const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); -const C_CHIP_BG: egui::Color32 = egui::Color32::from_rgba_premultiplied(0x1b, 0x1f, 0x27, 0xcc); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_HEADER: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_HEADER_HOT: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); /// A draggable boundary of the stack. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -43,16 +46,16 @@ pub enum Handle { // --- R1..R6 as pure ops on (top, count) -> Option<(top, count)> --- fn op_r1(top: usize, count: usize) -> Option<(usize, usize)> { - (top + count < N).then_some((top + 1, count)) // slide down + (top + count < N).then(|| (top + 1, count)) // slide down } fn op_r2(top: usize, count: usize) -> Option<(usize, usize)> { - (top > 0).then_some((top - 1, count)) // slide up + (top > 0).then(|| (top - 1, count)) // slide up } fn op_r3(top: usize, count: usize) -> Option<(usize, usize)> { - (count == 3).then_some((top + 1, 2)) // drop top + (count == 3).then(|| (top + 1, 2)) // drop top } fn op_r4(top: usize, count: usize) -> Option<(usize, usize)> { - (count == 3).then_some((top, 2)) // drop bottom + (count == 3).then(|| (top, 2)) // drop bottom } fn op_r5(top: usize, count: usize) -> Option<(usize, usize)> { if count < 3 && top + count < N { @@ -164,79 +167,112 @@ fn interp_rects( // --- rendering --- +/// The interaction (drag) target for a band's boundary is its full-width header bar. Bands are +/// laid out in the area above a reserved bottom footer (the BottomEdge handle). Band 0's header is +/// the TopEdge handle; band i's header (i≥1) is the divider above it; the footer is the BottomEdge. pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { let top = state.window_top; let count = state.window_count; - let pane_h = rect.height() / count as f32; - // Layout from the (previous frame's) drag, if any has a valid op; else the resting config. - let layout = state + // Reserve a footer bar at the very bottom for the BottomEdge handle. + let footer_rect = egui::Rect::from_min_max( + egui::pos2(rect.left(), rect.bottom() - HEADER_H), + rect.max, + ); + let content_area = egui::Rect::from_min_max( + rect.min, + egui::pos2(rect.right(), footer_rect.top()), + ); + let pane_h = content_area.height() / count as f32; + + // Resting band rects (used for interaction so drags don't chase the animated layout) and the + // possibly-animated draw layout. + let rest_bands = config_rects(top, count, content_area); + let draw_layout = state .drag .and_then(|d| resolve(d.handle, d.offset, top, count, pane_h)) - .map(|(tt, tc, t)| interp_rects((top, count), (tt, tc), t, rect)) - .unwrap_or_else(|| config_rects(top, count, rect)); + .map(|(tt, tc, t)| interp_rects((top, count), (tt, tc), t, content_area)) + .unwrap_or_else(|| rest_bands.clone()); - // 1) Pane content (top to bottom). - for (slot, prect) in &layout { - if prect.height() < 1.0 { - continue; - } - let sp = STACK[*slot]; - super::surface::render_surface_fullbleed( - ui, - *prect, - &slot_path(*slot), - sp.pane_type(state.show_instruments), - rc, + // 1) Pane content, carving the header off the top of each band. + for (slot, brect) in &draw_layout { + let header_h = HEADER_H.min(brect.height()); + let content_rect = egui::Rect::from_min_max( + egui::pos2(brect.left(), brect.top() + header_h), + brect.max, ); + if content_rect.height() > 1.0 { + let sp = STACK[*slot]; + super::surface::render_surface_fullbleed( + ui, + content_rect, + &slot_path(*slot), + sp.pane_type(state.show_instruments), + rc, + ); + } } - // 2) Per-band label chips + the Node/Instrument toggle (drawn over content). - for (slot, prect) in &layout { - if prect.height() < 24.0 { + // 2) Header visuals (animated positions). + for (slot, brect) in &draw_layout { + if brect.height() < 6.0 { continue; } - draw_band_chip(ui, *prect, STACK[*slot], state); + let hr = egui::Rect::from_min_max( + brect.left_top(), + egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), + ); + draw_header(ui, hr, STACK[*slot], state.show_instruments, false); } + draw_footer(ui, footer_rect, top + count >= N); - // 3) Handles (interacted last so they win the initial press on their thin strips). - handle_interactions(ui, rect, state); + // 3) Interactions on the resting header/footer rects (added last → they win the press). + handle_interactions(ui, &rest_bands, footer_rect, state); } -fn draw_band_chip(ui: &mut egui::Ui, prect: egui::Rect, sp: StackPane, state: &mut MobileState) { - let label = sp.label(state.show_instruments); - let pos = prect.left_top() + egui::vec2(8.0, 6.0); - let galley = ui.painter().layout_no_wrap( - label.to_string(), - egui::FontId::proportional(11.0), - C_DIM, +fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, hot: bool) { + let p = ui.painter(); + p.rect_filled(hr, 0.0, if hot { C_HEADER_HOT } else { C_HEADER }); + p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); + // Grip dots on the left. + let cy = hr.center().y; + for i in 0..2 { + let x = hr.left() + 9.0 + i as f32 * 4.0; + p.circle_filled(egui::pos2(x, cy), 1.3, C_DIM); + } + p.text( + egui::pos2(hr.left() + 22.0, cy), + egui::Align2::LEFT_CENTER, + sp.label(show_instruments), + egui::FontId::proportional(12.0), + C_BRIGHT, ); - let chip = egui::Rect::from_min_size(pos, galley.size() + egui::vec2(12.0, 5.0)); - ui.painter().rect_filled(chip, 4.0, C_CHIP_BG); - ui.painter() - .galley(chip.min + egui::vec2(6.0, 2.0), galley, C_DIM); - - // Node/Instrument toggle sits just to the right of the label chip. + // Node/Instrument toggle marker (its hit area is wired in handle_interactions). if sp == StackPane::NodeInstrument { - let tog = egui::Rect::from_min_size( - egui::pos2(chip.right() + 6.0, chip.top()), - egui::vec2(24.0, chip.height()), - ); - let resp = ui.interact(tog, ui.id().with("mobile_node_toggle"), egui::Sense::click()); - ui.painter().rect_filled(tog, 4.0, C_CHIP_BG); - ui.painter().text( - tog.center(), + p.text( + egui::pos2(hr.right() - 14.0, cy), egui::Align2::CENTER_CENTER, "⇄", - egui::FontId::proportional(13.0), - if resp.hovered() { C_AMBER } else { C_DIM }, + egui::FontId::proportional(14.0), + C_AMBER, ); - if resp.clicked() { - state.show_instruments = !state.show_instruments; - } } } +fn draw_footer(ui: &egui::Ui, fr: egui::Rect, at_end: bool) { + let p = ui.painter(); + p.rect_filled(fr, 0.0, C_HEADER); + p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, C_LINE)); + let col = if at_end { C_LINE } else { C_DIM }; + p.text( + fr.center(), + egui::Align2::CENTER_CENTER, + "▲ pull up for more", + egui::FontId::proportional(11.0), + col, + ); +} + fn handle_key(h: Handle) -> (usize, usize) { match h { Handle::TopEdge => (0, 0), @@ -245,39 +281,43 @@ fn handle_key(h: Handle) -> (usize, usize) { } } -fn handle_interactions(ui: &mut egui::Ui, rect: egui::Rect, state: &mut MobileState) { - let count = state.window_count; - let pane_h = rect.height() / count as f32; +fn handle_interactions( + ui: &mut egui::Ui, + rest_bands: &[(usize, egui::Rect)], + footer_rect: egui::Rect, + state: &mut MobileState, +) { + let pane_h = rest_bands.first().map(|(_, r)| r.height()).unwrap_or(1.0); - let mut handles: Vec<(Handle, egui::Rect)> = Vec::new(); - handles.push(( - Handle::TopEdge, - egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.right(), rect.top() + EDGE_GRAB_H)), - )); - for k in 0..count.saturating_sub(1) { - let y = rect.top() + (k + 1) as f32 * pane_h; - handles.push(( - Handle::Divider(k), - egui::Rect::from_min_max( - egui::pos2(rect.left(), y - DIV_GRAB_H * 0.5), - egui::pos2(rect.right(), y + DIV_GRAB_H * 0.5), - ), - )); + // (handle, header_rect, optional node-toggle slot) + let mut handles: Vec<(Handle, egui::Rect, bool)> = Vec::new(); + for (i, (slot, brect)) in rest_bands.iter().enumerate() { + let hr = egui::Rect::from_min_max( + brect.left_top(), + egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), + ); + let handle = if i == 0 { Handle::TopEdge } else { Handle::Divider(i - 1) }; + let is_node = STACK[*slot] == StackPane::NodeInstrument; + handles.push((handle, hr, is_node)); } - handles.push(( - Handle::BottomEdge, - egui::Rect::from_min_max(egui::pos2(rect.left(), rect.bottom() - EDGE_GRAB_H), rect.max), - )); + handles.push((Handle::BottomEdge, footer_rect, false)); - for (handle, hrect) in handles { + for (handle, hrect, is_node) in handles { let id = ui.id().with(("mobile_stack_handle", handle_key(handle))); - let resp = ui.interact(hrect, id, egui::Sense::drag()); + let resp = ui.interact(hrect, id, egui::Sense::click_and_drag()); - let active = state.drag.map(|d| d.handle == handle).unwrap_or(false); - // Grab pill. - let pill = egui::Rect::from_center_size(hrect.center(), egui::vec2(40.0, 4.0)); - let pill_col = if resp.hovered() || active { C_AMBER } else { C_LINE }; - ui.painter().rect_filled(pill, 2.0, pill_col); + // Node/Instrument toggle: a small hit area on the right of the header, interacted AFTER the + // header (so it's on top and wins clicks there); the rest of the header drives the drag. + if is_node { + let tog = egui::Rect::from_min_max( + egui::pos2(hrect.right() - 28.0, hrect.top()), + hrect.max, + ); + let tresp = ui.interact(tog, ui.id().with("mobile_node_toggle"), egui::Sense::click()); + if tresp.clicked() { + state.show_instruments = !state.show_instruments; + } + } if resp.drag_started() { state.drag = Some(StackDrag { handle, offset: 0.0 }); @@ -294,7 +334,7 @@ fn handle_interactions(ui: &mut egui::Ui, rect: egui::Rect, state: &mut MobileSt commit_drag(d, state, pane_h); } } - if resp.hovered() || active { + if resp.hovered() || state.drag.map(|d| d.handle == handle).unwrap_or(false) { ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); } } From 597697d0a6971dccda7d3e05f988faa0e88e302c Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 00:33:35 -0400 Subject: [PATCH 06/43] Taller stack headers, fullscreen toggle, icon-font glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Double header height (52px) so the drag targets are easy to grab; footer stays compact (28px). - Add a fullscreen button on the right of each header that collapses the window to that single pane; dragging the sole pane's header down or the footer up splits it with the adjacent pane (count can now be 1). The button toggles back to a 2-pane split. - Use egui's bundled emoji-icon-font glyphs (⛶ fullscreen / ▣ restore) instead of hand-drawn primitives. - Cap the drag-to-commit distance (TRIGGER_MAX) so transitions don't require dragging half the screen. --- .../lightningbeam-editor/src/mobile/stack.rs | 113 ++++++++++++------ 1 file changed, 75 insertions(+), 38 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 0db8bb7..52a8cb6 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -25,14 +25,18 @@ const N: usize = STACK.len(); /// Height of each band's drag header (and the bottom-edge footer). The whole header is the grab /// target — there's no thin divider bar. -const HEADER_H: f32 = 26.0; +const HEADER_H: f32 = 52.0; +const FOOTER_H: f32 = 28.0; +/// Width of a header right-side button (fullscreen, node toggle). +const BTN_W: f32 = 44.0; +/// Max pixels of drag to complete a window transition (so you don't drag half the screen). +const TRIGGER_MAX: f32 = 150.0; const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); const C_HEADER: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_HEADER_HOT: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); /// A draggable boundary of the stack. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -83,10 +87,10 @@ fn resolve( offset: f32, top: usize, count: usize, - pane_h: f32, + trigger: f32, ) -> Option<(usize, usize, f32)> { let going_up = offset < 0.0; - let t = (offset.abs() / pane_h.max(1.0)).clamp(0.0, 1.0); + let t = (offset.abs() / trigger.max(1.0)).clamp(0.0, 1.0); let target = match handle { Handle::TopEdge => (!going_up).then(|| op_r6(top, count)).flatten(), Handle::BottomEdge => going_up.then(|| op_r5(top, count)).flatten(), @@ -176,7 +180,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state // Reserve a footer bar at the very bottom for the BottomEdge handle. let footer_rect = egui::Rect::from_min_max( - egui::pos2(rect.left(), rect.bottom() - HEADER_H), + egui::pos2(rect.left(), rect.bottom() - FOOTER_H), rect.max, ); let content_area = egui::Rect::from_min_max( @@ -184,13 +188,14 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state egui::pos2(rect.right(), footer_rect.top()), ); let pane_h = content_area.height() / count as f32; + let trigger = pane_h.min(TRIGGER_MAX); // Resting band rects (used for interaction so drags don't chase the animated layout) and the // possibly-animated draw layout. let rest_bands = config_rects(top, count, content_area); let draw_layout = state .drag - .and_then(|d| resolve(d.handle, d.offset, top, count, pane_h)) + .and_then(|d| resolve(d.handle, d.offset, top, count, trigger)) .map(|(tt, tc, t)| interp_rects((top, count), (tt, tc), t, content_area)) .unwrap_or_else(|| rest_bands.clone()); @@ -213,7 +218,9 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state } } - // 2) Header visuals (animated positions). + // 2) Header visuals (animated positions). The fullscreen icon shows "restore" when a single + // pane fills the stack. + let fullscreen = count == 1; for (slot, brect) in &draw_layout { if brect.height() < 6.0 { continue; @@ -222,38 +229,46 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state brect.left_top(), egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), ); - draw_header(ui, hr, STACK[*slot], state.show_instruments, false); + draw_header(ui, hr, STACK[*slot], state.show_instruments, fullscreen); } draw_footer(ui, footer_rect, top + count >= N); // 3) Interactions on the resting header/footer rects (added last → they win the press). - handle_interactions(ui, &rest_bands, footer_rect, state); + handle_interactions(ui, &rest_bands, footer_rect, trigger, state); } -fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, hot: bool) { +fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool) { let p = ui.painter(); - p.rect_filled(hr, 0.0, if hot { C_HEADER_HOT } else { C_HEADER }); + p.rect_filled(hr, 0.0, C_HEADER); p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); - // Grip dots on the left. let cy = hr.center().y; + // Grip dots on the left. for i in 0..2 { - let x = hr.left() + 9.0 + i as f32 * 4.0; - p.circle_filled(egui::pos2(x, cy), 1.3, C_DIM); + let x = hr.left() + 11.0 + i as f32 * 4.0; + p.circle_filled(egui::pos2(x, cy), 1.5, C_DIM); } p.text( - egui::pos2(hr.left() + 22.0, cy), + egui::pos2(hr.left() + 26.0, cy), egui::Align2::LEFT_CENTER, sp.label(show_instruments), - egui::FontId::proportional(12.0), + egui::FontId::proportional(15.0), C_BRIGHT, ); - // Node/Instrument toggle marker (its hit area is wired in handle_interactions). + // Right-side buttons: fullscreen / restore rightmost, Node/Instrument toggle just left of it. + // These glyphs are covered by egui's bundled emoji-icon-font (⛶ ▣ ⇄). + p.text( + egui::pos2(hr.right() - BTN_W * 0.5, cy), + egui::Align2::CENTER_CENTER, + if fullscreen { "▣" } else { "⛶" }, + egui::FontId::proportional(18.0), + C_DIM, + ); if sp == StackPane::NodeInstrument { p.text( - egui::pos2(hr.right() - 14.0, cy), + egui::pos2(hr.right() - BTN_W * 1.5, cy), egui::Align2::CENTER_CENTER, "⇄", - egui::FontId::proportional(14.0), + egui::FontId::proportional(17.0), C_AMBER, ); } @@ -281,41 +296,63 @@ fn handle_key(h: Handle) -> (usize, usize) { } } +/// Toggle a slot between filling the stack (count==1) and a 2-pane split with an adjacent pane. +fn toggle_fullscreen(state: &mut MobileState, slot: usize) { + if state.window_count == 1 && state.window_top == slot { + // Restore: split with the pane below if possible, else above. + if let Some((t, c)) = op_r5(slot, 1).or_else(|| op_r6(slot, 1)) { + state.window_top = t; + state.window_count = c; + } + } else { + state.window_top = slot; + state.window_count = 1; + } +} + fn handle_interactions( ui: &mut egui::Ui, rest_bands: &[(usize, egui::Rect)], footer_rect: egui::Rect, + trigger: f32, state: &mut MobileState, ) { - let pane_h = rest_bands.first().map(|(_, r)| r.height()).unwrap_or(1.0); - - // (handle, header_rect, optional node-toggle slot) - let mut handles: Vec<(Handle, egui::Rect, bool)> = Vec::new(); + // (handle, header_rect, slot) — slot is Some for band headers, None for the footer. + let mut handles: Vec<(Handle, egui::Rect, Option)> = Vec::new(); for (i, (slot, brect)) in rest_bands.iter().enumerate() { let hr = egui::Rect::from_min_max( brect.left_top(), egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), ); let handle = if i == 0 { Handle::TopEdge } else { Handle::Divider(i - 1) }; - let is_node = STACK[*slot] == StackPane::NodeInstrument; - handles.push((handle, hr, is_node)); + handles.push((handle, hr, Some(*slot))); } - handles.push((Handle::BottomEdge, footer_rect, false)); + handles.push((Handle::BottomEdge, footer_rect, None)); - for (handle, hrect, is_node) in handles { + for (handle, hrect, slot_opt) in handles { let id = ui.id().with(("mobile_stack_handle", handle_key(handle))); let resp = ui.interact(hrect, id, egui::Sense::click_and_drag()); - // Node/Instrument toggle: a small hit area on the right of the header, interacted AFTER the - // header (so it's on top and wins clicks there); the rest of the header drives the drag. - if is_node { - let tog = egui::Rect::from_min_max( - egui::pos2(hrect.right() - 28.0, hrect.top()), + // Right-side header buttons are interacted AFTER the header (so they're on top and win the + // press there); the rest of the header drives the drag. + if let Some(slot) = slot_opt { + let fs = egui::Rect::from_min_max( + egui::pos2(hrect.right() - BTN_W, hrect.top()), hrect.max, ); - let tresp = ui.interact(tog, ui.id().with("mobile_node_toggle"), egui::Sense::click()); - if tresp.clicked() { - state.show_instruments = !state.show_instruments; + let fsresp = ui.interact(fs, ui.id().with(("mobile_fs", slot)), egui::Sense::click()); + if fsresp.clicked() { + toggle_fullscreen(state, slot); + } + if STACK[slot] == StackPane::NodeInstrument { + let nt = egui::Rect::from_min_max( + egui::pos2(hrect.right() - 2.0 * BTN_W, hrect.top()), + egui::pos2(hrect.right() - BTN_W, hrect.bottom()), + ); + let ntresp = ui.interact(nt, ui.id().with("mobile_node_toggle"), egui::Sense::click()); + if ntresp.clicked() { + state.show_instruments = !state.show_instruments; + } } } @@ -331,7 +368,7 @@ fn handle_interactions( } if resp.drag_stopped() { if let Some(d) = state.drag.take() { - commit_drag(d, state, pane_h); + commit_drag(d, state, trigger); } } if resp.hovered() || state.drag.map(|d| d.handle == handle).unwrap_or(false) { @@ -340,8 +377,8 @@ fn handle_interactions( } } -fn commit_drag(d: StackDrag, state: &mut MobileState, pane_h: f32) { - if let Some((tt, tc, t)) = resolve(d.handle, d.offset, state.window_top, state.window_count, pane_h) { +fn commit_drag(d: StackDrag, state: &mut MobileState, trigger: f32) { + if let Some((tt, tc, t)) = resolve(d.handle, d.offset, state.window_top, state.window_count, trigger) { if t >= 0.5 { state.window_top = tt; state.window_count = tc; From 76e7963f0380022baa50c6cfb54b549fbfd9e7f3 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 00:51:29 -0400 Subject: [PATCH 07/43] Add Lucide icon font for the mobile UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle Lucide (ISC license) at assets/fonts/lucide.ttf and register it as the egui font family "lucide" via mobile/icons.rs::install (called in EditorApp::new). Icons are drawn as text with icons::font(size) + PUA codepoint constants. Switch the stack headers (grip, maximize/minimize, node⇄instrument), the footer chevron, and the transport play/pause to Lucide glyphs instead of relying on egui's limited built-in emoji-icon-font. --- .../assets/fonts/LICENSE-lucide.txt | 43 ++++++++++++++++ .../assets/fonts/lucide.ttf | Bin 0 -> 841240 bytes .../lightningbeam-editor/src/main.rs | 3 ++ .../lightningbeam-editor/src/mobile/icons.rs | 47 ++++++++++++++++++ .../lightningbeam-editor/src/mobile/mod.rs | 1 + .../lightningbeam-editor/src/mobile/stack.rs | 44 ++++++++++------ .../src/mobile/transport.rs | 5 +- 7 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/assets/fonts/LICENSE-lucide.txt create mode 100644 lightningbeam-ui/lightningbeam-editor/assets/fonts/lucide.ttf create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs diff --git a/lightningbeam-ui/lightningbeam-editor/assets/fonts/LICENSE-lucide.txt b/lightningbeam-ui/lightningbeam-editor/assets/fonts/LICENSE-lucide.txt new file mode 100644 index 0000000..718bb3f --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/assets/fonts/LICENSE-lucide.txt @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lightningbeam-ui/lightningbeam-editor/assets/fonts/lucide.ttf b/lightningbeam-ui/lightningbeam-editor/assets/fonts/lucide.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0a69bf12805d9f7d3af03f3300fa647156786396 GIT binary patch literal 841240 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

JQL%mJ$9fEs&&TJ?b1Lx4KVfWvro9X^noX)q!s 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 =E(wvlySpwief&Xoh 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⁢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 Self { let current_layout = layouts[0].layout.clone(); + // Register the bundled Lucide icon font (used by the mobile UI; harmless on desktop). + mobile::icons::install(&cc.egui_ctx); + // Disable egui's "Unaligned" debug overlay (on by default in debug builds) #[cfg(debug_assertions)] cc.egui_ctx.style_mut(|style| style.debug.show_unaligned = false); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs new file mode 100644 index 0000000..db7a954 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs @@ -0,0 +1,47 @@ +//! Lucide icon font (ISC license — see `assets/fonts/LICENSE-lucide.txt`). +#![allow(dead_code)] // a forward-looking icon palette; not all are wired up yet +//! +//! The font is embedded and registered as a named egui font family ("lucide"). Icons are drawn as +//! text using [`font`] for the `FontId`, with the codepoint constants below (Lucide's private-use +//! area). Run [`install`] once at startup. + +use std::sync::Arc; + +use eframe::egui; + +/// The egui font-family name the Lucide glyphs live under. +pub const FAMILY: &str = "lucide"; + +/// Embed + register the Lucide icon font. Call once with the egui context at startup. +pub fn install(ctx: &egui::Context) { + let mut fonts = egui::FontDefinitions::default(); + fonts.font_data.insert( + "lucide".to_owned(), + Arc::new(egui::FontData::from_static(include_bytes!( + "../../assets/fonts/lucide.ttf" + ))), + ); + fonts + .families + .insert(egui::FontFamily::Name(FAMILY.into()), vec!["lucide".to_owned()]); + ctx.set_fonts(fonts); +} + +/// A `FontId` in the Lucide family at `size`. +pub fn font(size: f32) -> egui::FontId { + egui::FontId::new(size, egui::FontFamily::Name(FAMILY.into())) +} + +// --- Codepoints (Lucide PUA). Add more as the mobile UI needs them. --- +pub const MAXIMIZE: &str = "\u{e112}"; +pub const MINIMIZE: &str = "\u{e11a}"; +pub const ARROW_LEFT_RIGHT: &str = "\u{e24a}"; +pub const GRIP_HORIZONTAL: &str = "\u{e0ea}"; +pub const CHEVRONS_UP: &str = "\u{e074}"; +pub const PLAY: &str = "\u{e13c}"; +pub const PAUSE: &str = "\u{e12e}"; +pub const SETTINGS: &str = "\u{e154}"; +pub const SEARCH: &str = "\u{e151}"; +pub const PLUS: &str = "\u{e13d}"; +pub const X: &str = "\u{e1b2}"; +pub const MENU: &str = "\u{e115}"; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 907afca..2d873f3 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -17,6 +17,7 @@ use lightningbeam_core::pane::PaneType; use crate::panes::NodePath; use crate::RenderContext; +pub mod icons; mod stack; mod surface; mod transport; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 52a8cb6..53dfa4b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -18,7 +18,7 @@ use eframe::egui; -use super::{slot_path, MobileState, StackDrag, StackPane, STACK}; +use super::{icons, slot_path, MobileState, StackDrag, StackPane, STACK}; use crate::RenderContext; const N: usize = STACK.len(); @@ -242,33 +242,35 @@ fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: b p.rect_filled(hr, 0.0, C_HEADER); p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); let cy = hr.center().y; - // Grip dots on the left. - for i in 0..2 { - let x = hr.left() + 11.0 + i as f32 * 4.0; - p.circle_filled(egui::pos2(x, cy), 1.5, C_DIM); - } + // Grip glyph on the left (Lucide). p.text( - egui::pos2(hr.left() + 26.0, cy), + egui::pos2(hr.left() + 15.0, cy), + egui::Align2::CENTER_CENTER, + icons::GRIP_HORIZONTAL, + icons::font(16.0), + C_DIM, + ); + p.text( + egui::pos2(hr.left() + 32.0, cy), egui::Align2::LEFT_CENTER, sp.label(show_instruments), egui::FontId::proportional(15.0), C_BRIGHT, ); // Right-side buttons: fullscreen / restore rightmost, Node/Instrument toggle just left of it. - // These glyphs are covered by egui's bundled emoji-icon-font (⛶ ▣ ⇄). p.text( egui::pos2(hr.right() - BTN_W * 0.5, cy), egui::Align2::CENTER_CENTER, - if fullscreen { "▣" } else { "⛶" }, - egui::FontId::proportional(18.0), + if fullscreen { icons::MINIMIZE } else { icons::MAXIMIZE }, + icons::font(17.0), C_DIM, ); if sp == StackPane::NodeInstrument { p.text( egui::pos2(hr.right() - BTN_W * 1.5, cy), egui::Align2::CENTER_CENTER, - "⇄", - egui::FontId::proportional(17.0), + icons::ARROW_LEFT_RIGHT, + icons::font(17.0), C_AMBER, ); } @@ -279,13 +281,23 @@ fn draw_footer(ui: &egui::Ui, fr: egui::Rect, at_end: bool) { p.rect_filled(fr, 0.0, C_HEADER); p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, C_LINE)); let col = if at_end { C_LINE } else { C_DIM }; - p.text( - fr.center(), - egui::Align2::CENTER_CENTER, - "▲ pull up for more", + let cy = fr.center().y; + let galley = p.layout_no_wrap( + "pull up for more".to_string(), egui::FontId::proportional(11.0), col, ); + let total_w = 22.0 + galley.size().x; + let start_x = fr.center().x - total_w * 0.5; + p.text( + egui::pos2(start_x + 8.0, cy), + egui::Align2::CENTER_CENTER, + icons::CHEVRONS_UP, + icons::font(14.0), + col, + ); + let gy = cy - galley.size().y * 0.5; + p.galley(egui::pos2(start_x + 22.0, gy), galley, col); } fn handle_key(h: Handle) -> (usize, usize) { diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs index 1623b72..90b2a50 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs @@ -3,6 +3,7 @@ use eframe::egui; +use super::icons; use crate::panes::SharedPaneState; const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); @@ -24,12 +25,12 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0)); let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click()); painter.circle_filled(btn_center, btn_r, C_AMBER); - let glyph = if *shared.is_playing { "⏸" } else { "▶" }; + let glyph = if *shared.is_playing { icons::PAUSE } else { icons::PLAY }; painter.text( btn_center, egui::Align2::CENTER_CENTER, glyph, - egui::FontId::proportional(16.0), + icons::font(16.0), C_DARK, ); if btn_resp.clicked() { From 0e3c437d5095191697832a608e6dbdaee4bbb77a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 01:02:13 -0400 Subject: [PATCH 08/43] Round the top corners of stack pane headers Each header now has rounded top corners (square at the bottom where it meets the pane content) so it reads as a tab atop the pane. --- .../lightningbeam-editor/src/mobile/stack.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 53dfa4b..89e9501 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -26,6 +26,8 @@ const N: usize = STACK.len(); /// Height of each band's drag header (and the bottom-edge footer). The whole header is the grab /// target — there's no thin divider bar. const HEADER_H: f32 = 52.0; +/// Corner radius (px) for the rounded top of each header. +const HEADER_RADIUS: u8 = 9; const FOOTER_H: f32 = 28.0; /// Width of a header right-side button (fullscreen, node toggle). const BTN_W: f32 = 44.0; @@ -239,7 +241,13 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool) { let p = ui.painter(); - p.rect_filled(hr, 0.0, C_HEADER); + // Rounded top corners so the header reads as a tab atop the pane; square at the bottom where + // it meets the pane content. + p.rect_filled( + hr, + egui::CornerRadius { nw: HEADER_RADIUS, ne: HEADER_RADIUS, sw: 0, se: 0 }, + C_HEADER, + ); p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); let cy = hr.center().y; // Grip glyph on the left (Lucide). From 3b39e80e408168d624b7f0ee24940e737945ab1d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 01:18:13 -0400 Subject: [PATCH 09/43] Add intermediate divider snapping (uneven pane sizes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragging a divider now resizes its two adjacent panes live and, on release, snaps the boundary to the nearest of 1/4, 1/3, 1/2, 2/3, 3/4 — so one pane can be made dominant within a 2- or 3-pane window. Dragging far enough to squeeze a pane past the collapse threshold still triggers the membership transition (slide/shrink per R1-R6), after which sizing resets to even. MobileState gains a `weights: [f32;3]` (normalized on use); config_rects is weight-aware; edge drags keep animating toward the even-split target. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/mobile/mod.rs | 6 +- .../lightningbeam-editor/src/mobile/stack.rs | 206 ++++++++++++++---- 2 files changed, 166 insertions(+), 46 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 2d873f3..ab6e6ae 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -122,8 +122,11 @@ pub struct StackDrag { pub struct MobileState { /// Index into `STACK` of the topmost visible pane. pub window_top: usize, - /// Number of visible panes (2 or 3). + /// Number of visible panes (1, 2, or 3; 1 = a single pane filling the stack). pub window_count: usize, + /// Relative heights of the visible panes (first `window_count` entries are used; normalized on + /// use). Reset to even on any membership change; adjusted by intermediate divider snapping. + pub weights: [f32; 3], /// Node/Instrument band: false = node editor, true = instrument/preset browser. pub show_instruments: bool, /// Active handle drag (transient). @@ -136,6 +139,7 @@ impl Default for MobileState { // Launch on {Stage, Timeline}. window_top: 2, window_count: 2, + weights: [1.0, 1.0, 1.0], show_instruments: false, drag: None, } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 89e9501..d290a56 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -113,20 +113,43 @@ fn resolve( // --- rect layout --- -fn config_rects(top: usize, count: usize, rect: egui::Rect) -> Vec<(usize, egui::Rect)> { - let h = rect.height() / count as f32; - (0..count) - .map(|i| { - let y0 = rect.top() + i as f32 * h; - ( - top + i, - egui::Rect::from_min_max( - egui::pos2(rect.left(), y0), - egui::pos2(rect.right(), y0 + h), - ), - ) - }) - .collect() +/// Intermediate divider snap fractions (pane k's share of the {k, k+1} span). +const SNAP_FRACS: [f32; 5] = [0.25, 1.0 / 3.0, 0.5, 2.0 / 3.0, 0.75]; +/// Below/above these local fractions, a divider release collapses a pane → membership change. +const COLLAPSE_LO: f32 = 0.12; +const COLLAPSE_HI: f32 = 0.88; +/// Minimum normalized size a pane may be squeezed to during a live divider drag. +const MIN_FRAC: f32 = 0.05; + +/// Normalized pane weights (the first `count` stored weights, summing to 1). +fn nweights(weights: &[f32; 3], count: usize) -> Vec { + let mut w: Vec = weights[..count].iter().map(|x| x.max(0.0001)).collect(); + let s: f32 = w.iter().sum(); + if s > 0.0 { + for x in &mut w { + *x /= s; + } + } else { + let e = 1.0 / count as f32; + w.iter_mut().for_each(|x| *x = e); + } + w +} + +/// Lay out `count` panes in `rect` using normalized weights `nw`. +fn config_rects(top: usize, count: usize, rect: egui::Rect, nw: &[f32]) -> Vec<(usize, egui::Rect)> { + let h = rect.height(); + let mut out = Vec::with_capacity(count); + let mut y = rect.top(); + for i in 0..count { + let ph = nw[i] * h; + out.push(( + top + i, + egui::Rect::from_min_max(egui::pos2(rect.left(), y), egui::pos2(rect.right(), y + ph)), + )); + y += ph; + } + out } fn lerp_f(a: f32, b: f32, t: f32) -> f32 { @@ -143,24 +166,51 @@ fn collapsed(rect: egui::Rect, at_top: bool) -> egui::Rect { egui::Rect::from_min_max(egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)) } -/// Interpolate the visible-pane rects from config C toward config T by progress `t`. -fn interp_rects( - (top_c, count_c): (usize, usize), - (top_t, count_t): (usize, usize), +/// The local boundary fraction (pane k's share of the {k, k+1} span) after applying a normalized +/// `offset_frac` to divider `k`, clamped so neither pane is squeezed past MIN_FRAC. +fn divider_local(nw: &[f32], k: usize, offset_frac: f32) -> f32 { + let span_lo: f32 = nw[..k].iter().sum(); + let span = nw[k] + nw[k + 1]; + if span <= 0.0 { + return 0.5; + } + let base_b = span_lo + nw[k]; + let b = (base_b + offset_frac).clamp(span_lo + MIN_FRAC * span, span_lo + span - MIN_FRAC * span); + ((b - span_lo) / span).clamp(0.0, 1.0) +} + +/// Live pane rects while dragging divider `k` (resize-only; membership is unchanged until release). +fn divider_live( + top: usize, + count: usize, + rect: egui::Rect, + nw: &[f32], + k: usize, + offset_frac: f32, +) -> Vec<(usize, egui::Rect)> { + let f = divider_local(nw, k, offset_frac); + let span = nw[k] + nw[k + 1]; + let mut w = nw.to_vec(); + w[k] = f * span; + w[k + 1] = (1.0 - f) * span; + config_rects(top, count, rect, &w) +} + +/// Interpolate between two precomputed rect lists by `t` (used for edge membership transitions). +fn interp_layout( + c: &[(usize, egui::Rect)], + tt: &[(usize, egui::Rect)], + top_c: usize, + top_t: usize, t: f32, rect: egui::Rect, ) -> Vec<(usize, egui::Rect)> { - let c = config_rects(top_c, count_c, rect); - let tt = config_rects(top_t, count_t, rect); let find = |v: &[(usize, egui::Rect)], slot: usize| v.iter().find(|(s, _)| *s == slot).map(|(_, r)| *r); - - let lo = top_c.min(top_t); - let hi = (top_c + count_c).max(top_t + count_t); + let lo = c.first().map(|(s, _)| *s).unwrap_or(0).min(tt.first().map(|(s, _)| *s).unwrap_or(0)); + let hi = c.last().map(|(s, _)| *s + 1).unwrap_or(0).max(tt.last().map(|(s, _)| *s + 1).unwrap_or(0)); let mut out = Vec::new(); for slot in lo..hi { - let in_c = find(&c, slot); - let in_t = find(&tt, slot); - let r = match (in_c, in_t) { + let r = match (find(c, slot), find(tt, slot)) { (Some(rc), Some(rt)) => lerp_rect(rc, rt, t), (Some(rc), None) => lerp_rect(rc, collapsed(rect, slot < top_t), t), // exiting (None, Some(rt)) => lerp_rect(collapsed(rect, slot < top_c), rt, t), // entering @@ -192,14 +242,28 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state let pane_h = content_area.height() / count as f32; let trigger = pane_h.min(TRIGGER_MAX); - // Resting band rects (used for interaction so drags don't chase the animated layout) and the - // possibly-animated draw layout. - let rest_bands = config_rects(top, count, content_area); - let draw_layout = state - .drag - .and_then(|d| resolve(d.handle, d.offset, top, count, trigger)) - .map(|(tt, tc, t)| interp_rects((top, count), (tt, tc), t, content_area)) - .unwrap_or_else(|| rest_bands.clone()); + // Resting band rects (weighted; used for interaction so drags don't chase the animated layout). + let nw = nweights(&state.weights, count); + let rest_bands = config_rects(top, count, content_area, &nw); + + // The draw layout. A divider drag resizes its two panes live; an edge drag animates toward the + // membership transition. + let draw_layout = match state.drag { + Some(d) => match d.handle { + Handle::Divider(k) if k + 1 < count => { + let off_frac = d.offset / content_area.height().max(1.0); + divider_live(top, count, content_area, &nw, k, off_frac) + } + _ => resolve(d.handle, d.offset, top, count, trigger) + .map(|(tt, tc, t)| { + let even = vec![1.0 / tc as f32; tc]; + let target = config_rects(tt, tc, content_area, &even); + interp_layout(&rest_bands, &target, top, tt, t, content_area) + }) + .unwrap_or_else(|| rest_bands.clone()), + }, + None => rest_bands.clone(), + }; // 1) Pane content, carving the header off the top of each band. for (slot, brect) in &draw_layout { @@ -236,7 +300,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state draw_footer(ui, footer_rect, top + count >= N); // 3) Interactions on the resting header/footer rects (added last → they win the press). - handle_interactions(ui, &rest_bands, footer_rect, trigger, state); + handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, state); } fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool) { @@ -321,18 +385,17 @@ fn toggle_fullscreen(state: &mut MobileState, slot: usize) { if state.window_count == 1 && state.window_top == slot { // Restore: split with the pane below if possible, else above. if let Some((t, c)) = op_r5(slot, 1).or_else(|| op_r6(slot, 1)) { - state.window_top = t; - state.window_count = c; + set_window(state, t, c); } } else { - state.window_top = slot; - state.window_count = 1; + set_window(state, slot, 1); } } fn handle_interactions( ui: &mut egui::Ui, rest_bands: &[(usize, egui::Rect)], + content_area: egui::Rect, footer_rect: egui::Rect, trigger: f32, state: &mut MobileState, @@ -388,7 +451,7 @@ fn handle_interactions( } if resp.drag_stopped() { if let Some(d) = state.drag.take() { - commit_drag(d, state, trigger); + commit_drag(d, state, content_area, trigger); } } if resp.hovered() || state.drag.map(|d| d.handle == handle).unwrap_or(false) { @@ -397,11 +460,64 @@ fn handle_interactions( } } -fn commit_drag(d: StackDrag, state: &mut MobileState, trigger: f32) { - if let Some((tt, tc, t)) = resolve(d.handle, d.offset, state.window_top, state.window_count, trigger) { - if t >= 0.5 { - state.window_top = tt; - state.window_count = tc; +fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32) { + match d.handle { + Handle::Divider(k) if k + 1 < state.window_count => { + commit_divider(state, k, d.offset / content_area.height().max(1.0)); + } + _ => { + // Edges (and degenerate dividers): membership transition, then reset to even sizing. + if let Some((tt, tc, t)) = + resolve(d.handle, d.offset, state.window_top, state.window_count, trigger) + { + if t >= 0.5 { + set_window(state, tt, tc); + } + } } } } + +/// Apply a divider release: collapse a pane (→ membership change) past the thresholds, otherwise +/// snap the boundary to the nearest intermediate fraction. +fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32) { + let top = state.window_top; + let count = state.window_count; + let nw = nweights(&state.weights, count); + let f = divider_local(&nw, k, offset_frac); + + if f <= COLLAPSE_LO { + // Pane k squeezed out at the top. + let op = if k == 0 { op_r1(top, count) } else { op_r3(top, count) }; + if let Some((tt, tc)) = op { + set_window(state, tt, tc); + } + } else if f >= COLLAPSE_HI { + // Pane k+1 squeezed out at the bottom. + let op = if k == count - 2 { op_r2(top, count) } else { op_r4(top, count) }; + if let Some((tt, tc)) = op { + set_window(state, tt, tc); + } + } else { + // Snap the boundary to the nearest intermediate fraction, preserving the {k, k+1} span and + // the other panes' sizes. + let sf = *SNAP_FRACS + .iter() + .min_by(|a, b| (**a - f).abs().total_cmp(&(**b - f).abs())) + .unwrap(); + let span = nw[k] + nw[k + 1]; + let mut w = nw; + w[k] = sf * span; + w[k + 1] = (1.0 - sf) * span; + for i in 0..count { + state.weights[i] = w[i]; + } + } +} + +/// Switch the visible window and reset to even pane sizing. +fn set_window(state: &mut MobileState, top: usize, count: usize) { + state.window_top = top; + state.window_count = count; + state.weights = [1.0, 1.0, 1.0]; +} From 01fe14f1973522cf2cf7dfd4593728c1e401aeff Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 01:28:08 -0400 Subject: [PATCH 10/43] Ease divider snap into place over ~100ms After releasing a divider, the boundary now eases (cubic ease-out, ~100ms) from where the finger lifted to the snapped fraction instead of jumping. Starting a new drag cancels an in-flight ease; membership changes still apply instantly. --- .../lightningbeam-editor/src/mobile/mod.rs | 12 ++++ .../lightningbeam-editor/src/mobile/stack.rs | 57 +++++++++++++++---- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index ab6e6ae..7433f1c 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -118,6 +118,15 @@ pub struct StackDrag { pub offset: f32, } +/// A short ease of the visible pane weights toward their snapped values after a divider release. +#[derive(Debug, Clone, Copy)] +pub struct SnapAnim { + pub from: [f32; 3], + pub to: [f32; 3], + /// egui time (seconds) when the animation started. + pub start: f64, +} + /// Persistent mobile-shell state, cached on `EditorApp`. pub struct MobileState { /// Index into `STACK` of the topmost visible pane. @@ -131,6 +140,8 @@ pub struct MobileState { pub show_instruments: bool, /// Active handle drag (transient). pub drag: Option, + /// In-flight snap ease after a divider release (transient). + pub snap_anim: Option, } impl Default for MobileState { @@ -142,6 +153,7 @@ impl Default for MobileState { weights: [1.0, 1.0, 1.0], show_instruments: false, drag: None, + snap_anim: None, } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index d290a56..1f7a8d1 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -18,7 +18,7 @@ use eframe::egui; -use super::{icons, slot_path, MobileState, StackDrag, StackPane, STACK}; +use super::{icons, slot_path, MobileState, SnapAnim, StackDrag, StackPane, STACK}; use crate::RenderContext; const N: usize = STACK.len(); @@ -120,6 +120,12 @@ const COLLAPSE_LO: f32 = 0.12; const COLLAPSE_HI: f32 = 0.88; /// Minimum normalized size a pane may be squeezed to during a live divider drag. const MIN_FRAC: f32 = 0.05; +/// Duration (seconds) of the ease into a snapped divider position. +const SNAP_ANIM_SECS: f64 = 0.10; + +fn ease_out(p: f32) -> f32 { + 1.0 - (1.0 - p).powi(3) +} /// Normalized pane weights (the first `count` stored weights, summing to 1). fn nweights(weights: &[f32; 3], count: usize) -> Vec { @@ -243,7 +249,22 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state let trigger = pane_h.min(TRIGGER_MAX); // Resting band rects (weighted; used for interaction so drags don't chase the animated layout). - let nw = nweights(&state.weights, count); + // While a snap ease is in flight (and we're not dragging), override the weights with the eased + // values so the boundary glides into place. + let now = ui.input(|i| i.time); + let mut nw = nweights(&state.weights, count); + if state.drag.is_none() { + if let Some(a) = state.snap_anim { + let p = ((now - a.start) / SNAP_ANIM_SECS).clamp(0.0, 1.0) as f32; + if p >= 1.0 { + state.snap_anim = None; + } else { + let e = ease_out(p); + nw = (0..count).map(|i| lerp_f(a.from[i], a.to[i], e)).collect(); + ui.ctx().request_repaint(); + } + } + } let rest_bands = config_rects(top, count, content_area, &nw); // The draw layout. A divider drag resizes its two panes live; an edge drag animates toward the @@ -300,7 +321,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state draw_footer(ui, footer_rect, top + count >= N); // 3) Interactions on the resting header/footer rects (added last → they win the press). - handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, state); + handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, now, state); } fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool) { @@ -398,6 +419,7 @@ fn handle_interactions( content_area: egui::Rect, footer_rect: egui::Rect, trigger: f32, + now: f64, state: &mut MobileState, ) { // (handle, header_rect, slot) — slot is Some for band headers, None for the footer. @@ -441,6 +463,7 @@ fn handle_interactions( if resp.drag_started() { state.drag = Some(StackDrag { handle, offset: 0.0 }); + state.snap_anim = None; // a fresh drag cancels any in-flight snap ease } if resp.dragged() { if let Some(d) = &mut state.drag { @@ -451,7 +474,7 @@ fn handle_interactions( } if resp.drag_stopped() { if let Some(d) = state.drag.take() { - commit_drag(d, state, content_area, trigger); + commit_drag(d, state, content_area, trigger, now); } } if resp.hovered() || state.drag.map(|d| d.handle == handle).unwrap_or(false) { @@ -460,10 +483,10 @@ fn handle_interactions( } } -fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32) { +fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32, now: f64) { match d.handle { Handle::Divider(k) if k + 1 < state.window_count => { - commit_divider(state, k, d.offset / content_area.height().max(1.0)); + commit_divider(state, k, d.offset / content_area.height().max(1.0), now); } _ => { // Edges (and degenerate dividers): membership transition, then reset to even sizing. @@ -480,7 +503,7 @@ fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, /// Apply a divider release: collapse a pane (→ membership change) past the thresholds, otherwise /// snap the boundary to the nearest intermediate fraction. -fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32) { +fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32, now: f64) { let top = state.window_top; let count = state.window_count; let nw = nweights(&state.weights, count); @@ -500,18 +523,27 @@ fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32) { } } else { // Snap the boundary to the nearest intermediate fraction, preserving the {k, k+1} span and - // the other panes' sizes. + // the other panes' sizes — and ease into it. let sf = *SNAP_FRACS .iter() .min_by(|a, b| (**a - f).abs().total_cmp(&(**b - f).abs())) .unwrap(); let span = nw[k] + nw[k + 1]; - let mut w = nw; - w[k] = sf * span; - w[k + 1] = (1.0 - sf) * span; + + // `from` = where the finger released; `to` = the snapped target. + let mut from = [0.0_f32; 3]; + let mut to = [0.0_f32; 3]; for i in 0..count { - state.weights[i] = w[i]; + from[i] = nw[i]; + to[i] = nw[i]; } + from[k] = f * span; + from[k + 1] = (1.0 - f) * span; + to[k] = sf * span; + to[k + 1] = (1.0 - sf) * span; + + state.weights = to; + state.snap_anim = Some(SnapAnim { from, to, start: now }); } } @@ -520,4 +552,5 @@ fn set_window(state: &mut MobileState, top: usize, count: usize) { state.window_top = top; state.window_count = count; state.weights = [1.0, 1.0, 1.0]; + state.snap_anim = None; } From b25e639a1cf80efff165a8d47170310bc86a8bcf Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 02:07:20 -0400 Subject: [PATCH 11/43] Linked-divider preset snapping + animated transitions for the stack Rework 3-pane divider behavior and animate every layout change: - All transitions now ease over ~100ms via a LayoutAnim between two full stack configs (resize, slide, collapse, fullscreen) instead of jumping; edge transitions continue from the drag's progress. - 3-pane dividers are linked along a single path parameter through the three presets ([.5,.25,.25] / even / [.25,.25,.5]) so both dividers move together and reach their snaps simultaneously. Over-grow past the extreme preset is anchored to the path endpoint so the non-dragged divider stays continuous. - Growing a pane past ~66% collapses the window to 2 panes (dropping the opposite-end pane); the surviving divider snaps to the nearest 2-pane preset (1/4, 1/2, 3/4) based on the drop position. Dragging to the slide extreme still slides the window. - 2-pane snaps to 1/4, 1/2, 3/4. --- .../lightningbeam-editor/src/mobile/mod.rs | 22 +- .../lightningbeam-editor/src/mobile/stack.rs | 374 +++++++++++++----- 2 files changed, 282 insertions(+), 114 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 7433f1c..970c367 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -118,12 +118,18 @@ pub struct StackDrag { pub offset: f32, } -/// A short ease of the visible pane weights toward their snapped values after a divider release. +/// A short ease between two stack layouts (resize snap, membership change, or fullscreen toggle). +/// Both endpoints are full configs so the same animation covers panes resizing AND panes +/// entering/leaving the window. #[derive(Debug, Clone, Copy)] -pub struct SnapAnim { - pub from: [f32; 3], - pub to: [f32; 3], - /// egui time (seconds) when the animation started. +pub struct LayoutAnim { + pub from_top: usize, + pub from_count: usize, + pub from_w: [f32; 3], + pub to_top: usize, + pub to_count: usize, + pub to_w: [f32; 3], + /// egui time (seconds) when the animation started (may be back-dated to continue a drag). pub start: f64, } @@ -140,8 +146,8 @@ pub struct MobileState { pub show_instruments: bool, /// Active handle drag (transient). pub drag: Option, - /// In-flight snap ease after a divider release (transient). - pub snap_anim: Option, + /// In-flight layout ease (transient). + pub anim: Option, } impl Default for MobileState { @@ -153,7 +159,7 @@ impl Default for MobileState { weights: [1.0, 1.0, 1.0], show_instruments: false, drag: None, - snap_anim: None, + anim: None, } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 1f7a8d1..ab46feb 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -18,7 +18,7 @@ use eframe::egui; -use super::{icons, slot_path, MobileState, SnapAnim, StackDrag, StackPane, STACK}; +use super::{icons, slot_path, LayoutAnim, MobileState, StackDrag, StackPane, STACK}; use crate::RenderContext; const N: usize = STACK.len(); @@ -113,11 +113,93 @@ fn resolve( // --- rect layout --- -/// Intermediate divider snap fractions (pane k's share of the {k, k+1} span). -const SNAP_FRACS: [f32; 5] = [0.25, 1.0 / 3.0, 0.5, 2.0 / 3.0, 0.75]; -/// Below/above these local fractions, a divider release collapses a pane → membership change. +/// Below/above these boundary positions, a divider release collapses a group → membership change. const COLLAPSE_LO: f32 = 0.12; const COLLAPSE_HI: f32 = 0.88; + +// The only allowed pane-weight distributions per window size. Snapping to these keeps the minimum +// pane height manageable (no pane below ~1/4). +const PRESETS_1: [[f32; 3]; 1] = [[1.0, 0.0, 0.0]]; +const PRESETS_2: [[f32; 3]; 3] = [[0.25, 0.75, 0.0], [0.5, 0.5, 0.0], [0.75, 0.25, 0.0]]; +const PRESETS_3: [[f32; 3]; 3] = [ + [0.5, 0.25, 0.25], + [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], + [0.25, 0.25, 0.5], +]; + +fn presets(count: usize) -> &'static [[f32; 3]] { + match count { + 2 => &PRESETS_2, + 3 => &PRESETS_3, + _ => &PRESETS_1, + } +} + +/// The allowed preset nearest (L2) to the current weights. +fn nearest_preset(cur: &[f32], count: usize) -> [f32; 3] { + let dist = |p: &[f32; 3]| (0..count).map(|i| (p[i] - cur[i]).powi(2)).sum::(); + presets(count) + .iter() + .min_by(|a, b| dist(a).total_cmp(&dist(b))) + .copied() + .unwrap_or([1.0, 0.0, 0.0]) +} + +// 3-pane snapping rides a single path parameter s in [0, 2] through the three presets, so the two +// dividers move together and reach their snaps simultaneously. The dragged divider's boundary +// position (pane0 for the upper divider; pane0+pane1 for the lower) maps onto s. +const PATH3_UPPER: [f32; 3] = [0.5, 1.0 / 3.0, 0.25]; // pane0 at s = 0, 1, 2 +const PATH3_LOWER: [f32; 3] = [0.75, 2.0 / 3.0, 0.5]; // pane0+pane1 at s = 0, 1, 2 +/// A pane grown beyond this collapses the 3-pane window to 2 panes on release. +const COLLAPSE_GROW: f32 = 0.66; + +fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] { + [ + lerp_f(a[0], b[0], t), + lerp_f(a[1], b[1], t), + lerp_f(a[2], b[2], t), + ] +} + +/// Weights along the 3-pane preset path at parameter `s` ∈ [0, 2]. +fn path3(s: f32) -> [f32; 3] { + let s = s.clamp(0.0, 2.0); + if s <= 1.0 { + lerp3(PRESETS_3[0], PRESETS_3[1], s) + } else { + lerp3(PRESETS_3[1], PRESETS_3[2], s - 1.0) + } +} + +/// Path parameter for divider `k` whose boundary position is `b` (within the path's range). +fn s_from_boundary(k: usize, b: f32) -> f32 { + let pts = if k == 0 { PATH3_UPPER } else { PATH3_LOWER }; + if b >= pts[1] { + (pts[0] - b) / (pts[0] - pts[1]) // 0 at pts[0] … 1 at pts[1] + } else { + 1.0 + (pts[1] - b) / (pts[1] - pts[2]) // 1 at pts[1] … 2 at pts[2] + } +} + +/// The 3-pane path boundary range for divider `k`: (min, max) of its position across the presets. +fn path3_range(k: usize) -> (f32, f32) { + let pts = if k == 0 { PATH3_UPPER } else { PATH3_LOWER }; + (pts[2], pts[0]) +} + +/// Live weights while dragging divider `k` to boundary `b` in a 3-pane window: follow the linked +/// path within range, and beyond it group-resize from the nearer path endpoint (so the motion is +/// continuous across the extreme presets — no jump in the other divider). +fn live_weights3(k: usize, b_unclamped: f32) -> [f32; 3] { + let (lo, hi) = path3_range(k); + if b_unclamped >= lo && b_unclamped <= hi { + path3(s_from_boundary(k, b_unclamped)) + } else { + let anchor = if b_unclamped > hi { path3(0.0) } else { path3(2.0) }; + let (bmin, bmax) = boundary_bounds(3, k); + to_arr(&weights_for_boundary(&anchor, k, b_unclamped.clamp(bmin, bmax))) + } +} /// Minimum normalized size a pane may be squeezed to during a live divider drag. const MIN_FRAC: f32 = 0.05; /// Duration (seconds) of the ease into a snapped divider position. @@ -172,34 +254,52 @@ fn collapsed(rect: egui::Rect, at_top: bool) -> egui::Rect { egui::Rect::from_min_max(egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)) } -/// The local boundary fraction (pane k's share of the {k, k+1} span) after applying a normalized -/// `offset_frac` to divider `k`, clamped so neither pane is squeezed past MIN_FRAC. -fn divider_local(nw: &[f32], k: usize, offset_frac: f32) -> f32 { - let span_lo: f32 = nw[..k].iter().sum(); - let span = nw[k] + nw[k + 1]; - if span <= 0.0 { - return 0.5; - } - let base_b = span_lo + nw[k]; - let b = (base_b + offset_frac).clamp(span_lo + MIN_FRAC * span, span_lo + span - MIN_FRAC * span); - ((b - span_lo) / span).clamp(0.0, 1.0) +/// Allowed range of divider `k`'s boundary position (normalized) given MIN_FRAC per pane. +/// The boundary splits the window into the group above (panes 0..=k) and below (panes k+1..). +fn boundary_bounds(count: usize, k: usize) -> (f32, f32) { + let above = (k + 1) as f32; + let below = (count - 1 - k) as f32; + (above * MIN_FRAC, 1.0 - below * MIN_FRAC) } -/// Live pane rects while dragging divider `k` (resize-only; membership is unchanged until release). -fn divider_live( - top: usize, - count: usize, - rect: egui::Rect, - nw: &[f32], - k: usize, - offset_frac: f32, -) -> Vec<(usize, egui::Rect)> { - let f = divider_local(nw, k, offset_frac); - let span = nw[k] + nw[k + 1]; +/// New normalized weights when divider `k` is moved to boundary position `b`: the panes above the +/// divider are scaled to fill `[0, b]` and those below to fill `[b, 1]`, each group keeping its +/// internal proportions. This makes the *other* dividers in the group move with the dragged one. +fn weights_for_boundary(nw: &[f32], k: usize, b: f32) -> Vec { + let count = nw.len(); + let above_sum: f32 = nw[..=k].iter().sum(); + let below_sum: f32 = nw[k + 1..].iter().sum(); let mut w = nw.to_vec(); - w[k] = f * span; - w[k + 1] = (1.0 - f) * span; - config_rects(top, count, rect, &w) + if above_sum > 0.0 { + let s = b / above_sum; + for x in &mut w[..=k] { + *x *= s; + } + } + if below_sum > 0.0 { + let s = (1.0 - b) / below_sum; + for x in &mut w[k + 1..count] { + *x *= s; + } + } + w +} + +fn to_arr(v: &[f32]) -> [f32; 3] { + let mut a = [0.0; 3]; + for (i, x) in v.iter().take(3).enumerate() { + a[i] = *x; + } + a +} + +fn even_arr(count: usize) -> [f32; 3] { + let mut a = [0.0; 3]; + let e = 1.0 / count as f32; + for x in a.iter_mut().take(count) { + *x = e; + } + a } /// Interpolate between two precomputed rect lists by `t` (used for edge membership transitions). @@ -248,42 +348,50 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state let pane_h = content_area.height() / count as f32; let trigger = pane_h.min(TRIGGER_MAX); - // Resting band rects (weighted; used for interaction so drags don't chase the animated layout). - // While a snap ease is in flight (and we're not dragging), override the weights with the eased - // values so the boundary glides into place. let now = ui.input(|i| i.time); - let mut nw = nweights(&state.weights, count); - if state.drag.is_none() { - if let Some(a) = state.snap_anim { - let p = ((now - a.start) / SNAP_ANIM_SECS).clamp(0.0, 1.0) as f32; - if p >= 1.0 { - state.snap_anim = None; - } else { - let e = ease_out(p); - nw = (0..count).map(|i| lerp_f(a.from[i], a.to[i], e)).collect(); - ui.ctx().request_repaint(); - } - } - } + // Resting band rects from the committed weights — used for interaction so handles sit at their + // final positions even while the visuals animate. + let nw = nweights(&state.weights, count); let rest_bands = config_rects(top, count, content_area, &nw); - // The draw layout. A divider drag resizes its two panes live; an edge drag animates toward the - // membership transition. - let draw_layout = match state.drag { - Some(d) => match d.handle { + // The draw layout: a live drag, an in-flight layout ease, or the resting config. + let draw_layout = if let Some(d) = state.drag { + match d.handle { Handle::Divider(k) if k + 1 < count => { + // Live resize: move divider k's boundary. In 3-pane the dividers are linked along the + // preset path; in 2-pane it's a simple group resize. let off_frac = d.offset / content_area.height().max(1.0); - divider_live(top, count, content_area, &nw, k, off_frac) + let b0: f32 = nw[..=k].iter().sum(); + let b = b0 + off_frac; + let w = if count == 3 { + live_weights3(k, b) + } else { + let (bmin, bmax) = boundary_bounds(count, k); + to_arr(&weights_for_boundary(&nw, k, b.clamp(bmin, bmax))) + }; + config_rects(top, count, content_area, &nweights(&w, count)) } _ => resolve(d.handle, d.offset, top, count, trigger) .map(|(tt, tc, t)| { - let even = vec![1.0 / tc as f32; tc]; - let target = config_rects(tt, tc, content_area, &even); + let target = config_rects(tt, tc, content_area, &nweights(&even_arr(tc), tc)); interp_layout(&rest_bands, &target, top, tt, t, content_area) }) .unwrap_or_else(|| rest_bands.clone()), - }, - None => rest_bands.clone(), + } + } else if let Some(a) = state.anim { + let p = (((now - a.start) / SNAP_ANIM_SECS) as f32).clamp(0.0, 1.0); + if p >= 1.0 { + state.anim = None; + rest_bands.clone() + } else { + ui.ctx().request_repaint(); + let e = ease_out(p); + let from = config_rects(a.from_top, a.from_count, content_area, &nweights(&a.from_w, a.from_count)); + let to = config_rects(a.to_top, a.to_count, content_area, &nweights(&a.to_w, a.to_count)); + interp_layout(&from, &to, a.from_top, a.to_top, e, content_area) + } + } else { + rest_bands.clone() }; // 1) Pane content, carving the header off the top of each band. @@ -402,14 +510,14 @@ fn handle_key(h: Handle) -> (usize, usize) { } /// Toggle a slot between filling the stack (count==1) and a 2-pane split with an adjacent pane. -fn toggle_fullscreen(state: &mut MobileState, slot: usize) { +fn toggle_fullscreen(state: &mut MobileState, slot: usize, now: f64) { if state.window_count == 1 && state.window_top == slot { // Restore: split with the pane below if possible, else above. if let Some((t, c)) = op_r5(slot, 1).or_else(|| op_r6(slot, 1)) { - set_window(state, t, c); + set_window(state, t, c, now); } } else { - set_window(state, slot, 1); + set_window(state, slot, 1, now); } } @@ -447,7 +555,7 @@ fn handle_interactions( ); let fsresp = ui.interact(fs, ui.id().with(("mobile_fs", slot)), egui::Sense::click()); if fsresp.clicked() { - toggle_fullscreen(state, slot); + toggle_fullscreen(state, slot, now); } if STACK[slot] == StackPane::NodeInstrument { let nt = egui::Rect::from_min_max( @@ -463,7 +571,7 @@ fn handle_interactions( if resp.drag_started() { state.drag = Some(StackDrag { handle, offset: 0.0 }); - state.snap_anim = None; // a fresh drag cancels any in-flight snap ease + state.anim = None; // a fresh drag cancels any in-flight ease } if resp.dragged() { if let Some(d) = &mut state.drag { @@ -484,73 +592,127 @@ fn handle_interactions( } fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32, now: f64) { + let h = content_area.height().max(1.0); match d.handle { Handle::Divider(k) if k + 1 < state.window_count => { - commit_divider(state, k, d.offset / content_area.height().max(1.0), now); + commit_divider(state, k, d.offset / h, now); } _ => { - // Edges (and degenerate dividers): membership transition, then reset to even sizing. - if let Some((tt, tc, t)) = - resolve(d.handle, d.offset, state.window_top, state.window_count, trigger) - { + // Edges (and degenerate dividers): membership transition. Animate from the current + // config to the new one, continuing from where the drag's interp left off (~progress t). + let top = state.window_top; + let count = state.window_count; + if let Some((tt, tc, t)) = resolve(d.handle, d.offset, top, count, trigger) { if t >= 0.5 { - set_window(state, tt, tc); + let from_w = to_arr(&nweights(&state.weights, count)); + begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), t, now); } } } } } -/// Apply a divider release: collapse a pane (→ membership change) past the thresholds, otherwise -/// snap the boundary to the nearest intermediate fraction. +/// Apply a divider release. 3-pane and 2-pane have different snap rules (see below). Either way the +/// result is eased in from whatever was on screen at release. fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32, now: f64) { let top = state.window_top; let count = state.window_count; let nw = nweights(&state.weights, count); - let f = divider_local(&nw, k, offset_frac); + let b0: f32 = nw[..=k].iter().sum(); + let b = b0 + offset_frac; // unclamped boundary position of divider k - if f <= COLLAPSE_LO { - // Pane k squeezed out at the top. - let op = if k == 0 { op_r1(top, count) } else { op_r3(top, count) }; - if let Some((tt, tc)) = op { - set_window(state, tt, tc); + if count == 3 { + // `from` = what's on screen at release. + let from_w = live_weights3(k, b); + // Sizes of the two extreme panes after this drag, to test the "grown past 66%" rule. + let pane_top = from_w[0]; // grown when dragging the upper divider down + let pane_bot = from_w[2]; // grown when dragging the lower divider up + + if k == 0 && pane_top >= COLLAPSE_GROW { + // Upper pane grown out → drop the bottom pane; the surviving {0,1} divider snaps to + // where it was dropped. + if let Some((tt, tc)) = op_r4(top, 3) { + let to_w = collapse_2pane(from_w[0], from_w[1]); + begin_anim(state, top, 3, from_w, tt, tc, to_w, 0.0, now); + } + } else if k == 1 && pane_bot >= COLLAPSE_GROW { + // Bottom pane grown out → drop the top pane; surviving {1,2} divider snaps to drop. + if let Some((tt, tc)) = op_r3(top, 3) { + let to_w = collapse_2pane(from_w[1], from_w[2]); + begin_anim(state, top, 3, from_w, tt, tc, to_w, 0.0, now); + } + } else if b <= COLLAPSE_LO { + if let Some((tt, tc)) = op_r1(top, 3) { + begin_anim(state, top, 3, from_w, tt, tc, even_arr(tc), 0.0, now); + } + } else if b >= COLLAPSE_HI { + if let Some((tt, tc)) = op_r2(top, 3) { + begin_anim(state, top, 3, from_w, tt, tc, even_arr(tc), 0.0, now); + } + } else { + // Snap both dividers together: round the path parameter to the nearest preset. + let (lo, hi) = path3_range(k); + let s = s_from_boundary(k, b.clamp(lo, hi)).round().clamp(0.0, 2.0); + let to_w = PRESETS_3[s as usize]; + begin_anim(state, top, 3, from_w, top, 3, to_w, 0.0, now); } - } else if f >= COLLAPSE_HI { - // Pane k+1 squeezed out at the bottom. - let op = if k == count - 2 { op_r2(top, count) } else { op_r4(top, count) }; - if let Some((tt, tc)) = op { - set_window(state, tt, tc); + return; + } + + // 2-pane: group resize, snap to nearest 2-pane preset, slide off at the extremes. + let (bmin, bmax) = boundary_bounds(count, k); + let from_w = to_arr(&weights_for_boundary(&nw, k, b.clamp(bmin, bmax))); + if b <= COLLAPSE_LO { + if let Some((tt, tc)) = op_r1(top, count) { + begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), 0.0, now); + } + } else if b >= COLLAPSE_HI { + if let Some((tt, tc)) = op_r2(top, count) { + begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), 0.0, now); } } else { - // Snap the boundary to the nearest intermediate fraction, preserving the {k, k+1} span and - // the other panes' sizes — and ease into it. - let sf = *SNAP_FRACS - .iter() - .min_by(|a, b| (**a - f).abs().total_cmp(&(**b - f).abs())) - .unwrap(); - let span = nw[k] + nw[k + 1]; - - // `from` = where the finger released; `to` = the snapped target. - let mut from = [0.0_f32; 3]; - let mut to = [0.0_f32; 3]; - for i in 0..count { - from[i] = nw[i]; - to[i] = nw[i]; - } - from[k] = f * span; - from[k + 1] = (1.0 - f) * span; - to[k] = sf * span; - to[k + 1] = (1.0 - sf) * span; - - state.weights = to; - state.snap_anim = Some(SnapAnim { from, to, start: now }); + let to_w = nearest_preset(&from_w, count); + begin_anim(state, top, count, from_w, top, count, to_w, 0.0, now); } } -/// Switch the visible window and reset to even pane sizing. -fn set_window(state: &mut MobileState, top: usize, count: usize) { - state.window_top = top; - state.window_count = count; - state.weights = [1.0, 1.0, 1.0]; - state.snap_anim = None; +/// Given the two surviving panes' (unnormalized) sizes after a 3→2 collapse, snap to the nearest +/// 2-pane preset by the first pane's proportion. +fn collapse_2pane(a: f32, b: f32) -> [f32; 3] { + let total = (a + b).max(1e-4); + let p0 = a / total; + nearest_preset(&[p0, 1.0 - p0, 0.0], 2) +} + +/// Switch to a new window config, easing from `from_*` to the new state over the snap duration. +/// `start_p` back-dates the animation so it can continue an in-progress drag. +fn begin_anim( + state: &mut MobileState, + from_top: usize, + from_count: usize, + from_w: [f32; 3], + to_top: usize, + to_count: usize, + to_w: [f32; 3], + start_p: f32, + now: f64, +) { + state.window_top = to_top; + state.window_count = to_count; + state.weights = to_w; + state.anim = Some(LayoutAnim { + from_top, + from_count, + from_w, + to_top, + to_count, + to_w, + start: now - start_p as f64 * SNAP_ANIM_SECS, + }); +} + +/// Switch the visible window and reset to even pane sizing, easing the transition. +fn set_window(state: &mut MobileState, top: usize, count: usize, now: f64) { + let from_w = to_arr(&nweights(&state.weights, state.window_count)); + begin_anim(state, state.window_top, state.window_count, from_w, top, count, even_arr(count), 0.0, now); } From f2cb516e8ac1a887afea3cb9c98e45cc24fcceb9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 02:33:49 -0400 Subject: [PATCH 12/43] Add mobile new-file intent picker (P2) When LB_MOBILE_UI is set, the desktop start screen is replaced by a phone intent picker: a 2x3 grid of icon cards (Draw / Animate / Compose / Record / Edit video / Blank) taking the top ~2/3, and a scrollable Recent-projects list in the bottom ~1/3. Each intent reuses EditorApp::create_new_project_with_focus and then sets the initial stack window (e.g. Draw -> fullscreen Stage, Compose -> Timeline + Piano Roll + Keys). Recent rows load the project and enter the editor. Cards are icon-only (Lucide glyph + label) for legibility on a phone. Adds the intent icons to the Lucide set. --- .../lightningbeam-editor/src/main.rs | 6 +- .../lightningbeam-editor/src/mobile/icons.rs | 8 + .../lightningbeam-editor/src/mobile/intent.rs | 171 ++++++++++++++++++ .../lightningbeam-editor/src/mobile/mod.rs | 1 + 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index a936514..90fc1a0 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6533,7 +6533,11 @@ impl eframe::App for EditorApp { // Render start screen or editor based on app mode if self.app_mode == AppMode::StartScreen { - self.render_start_screen(ctx); + if self.mobile_active() { + mobile::intent::render(self, ctx); + } else { + self.render_start_screen(ctx); + } return; // Skip editor rendering } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs index db7a954..d13b6a8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs @@ -45,3 +45,11 @@ pub const SEARCH: &str = "\u{e151}"; pub const PLUS: &str = "\u{e13d}"; pub const X: &str = "\u{e1b2}"; pub const MENU: &str = "\u{e115}"; +// Intent-picker icons. +pub const BRUSH: &str = "\u{e1d3}"; +pub const FILM: &str = "\u{e0d0}"; +pub const MUSIC: &str = "\u{e122}"; +pub const MIC: &str = "\u{e118}"; +pub const CLAPPERBOARD: &str = "\u{e29b}"; +pub const SQUARE_DASHED: &str = "\u{e1cb}"; +pub const FOLDER_OPEN: &str = "\u{e247}"; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs new file mode 100644 index 0000000..664a69c --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs @@ -0,0 +1,171 @@ +//! Mobile new-file intent picker — the phone equivalent of the desktop start screen. +//! +//! Mirrors the desktop "new project" flow: each intent calls +//! [`EditorApp::create_new_project_with_focus`] (which builds the document, picks a default layer, +//! and switches to editor mode) and then sets the initial mobile stack window so the relevant panes +//! are in view. + +use eframe::egui; + +use super::icons; +use crate::EditorApp; + +const C_DEVICE: egui::Color32 = egui::Color32::from_rgb(0x14, 0x16, 0x1b); +const C_CARD: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_CARD_HOT: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); + +/// One intent card. +struct Intent { + label: &'static str, + icon: &'static str, + accent: egui::Color32, + /// Argument to `create_new_project_with_focus` (0=Animation, 1=Video, 2=Music, 5=Painting). + focus: usize, + /// Initial mobile stack window: (window_top, window_count). + window: (usize, usize), +} + +fn intents() -> [Intent; 6] { + let coral = egui::Color32::from_rgb(0xe8, 0x82, 0x6b); + let cyan = egui::Color32::from_rgb(0x54, 0xc3, 0xe8); + let amber = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); + let pink = egui::Color32::from_rgb(0xc7, 0x5b, 0x8a); + let violet = egui::Color32::from_rgb(0x6a, 0x46, 0x90); + [ + // Stage indices (see super::STACK): Stage=2, Timeline=3, PianoRoll=4, VirtualPiano=5. + Intent { label: "Draw", icon: icons::BRUSH, accent: coral, focus: 5, window: (2, 1) }, + Intent { label: "Animate", icon: icons::FILM, accent: cyan, focus: 0, window: (2, 2) }, + Intent { label: "Compose", icon: icons::MUSIC, accent: amber, focus: 2, window: (3, 3) }, + Intent { label: "Record", icon: icons::MIC, accent: pink, focus: 2, window: (3, 3) }, + Intent { label: "Edit video", icon: icons::CLAPPERBOARD, accent: violet, focus: 1, window: (2, 2) }, + Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: C_DIM, focus: 0, window: (2, 2) }, + ] +} + +pub fn render(app: &mut EditorApp, ctx: &egui::Context) { + egui::CentralPanel::default() + .frame(egui::Frame::NONE.fill(C_DEVICE)) + .show(ctx, |ui| { + let rect = ui.available_rect_before_wrap(); + let margin = 16.0; + let left = rect.left() + margin; + let right = rect.right() - margin; + + // Header. + ui.painter().text( + egui::pos2(left, rect.top() + 22.0), + egui::Align2::LEFT_TOP, + "Start something", + egui::FontId::proportional(22.0), + C_BRIGHT, + ); + + // Vertical budget: ~2/3 for the intent grid, ~1/3 for the recent list. + let content_top = rect.top() + 62.0; + let content_h = (rect.bottom() - margin) - content_top; + let gap = 10.0; + let grid_h = content_h * 0.66; + + // 2×3 grid of intent cards filling the grid budget. + let col_w = (right - left - gap) / 2.0; + let card_h = (grid_h - 2.0 * gap) / 3.0; + for (i, intent) in intents().iter().enumerate() { + let col = (i % 2) as f32; + let row = (i / 2) as f32; + let cx = left + col * (col_w + gap); + let cy = content_top + row * (card_h + gap); + let card = egui::Rect::from_min_size(egui::pos2(cx, cy), egui::vec2(col_w, card_h)); + + let resp = ui.interact(card, ui.id().with(("mobile_intent", i)), egui::Sense::click()); + let p = ui.painter(); + p.rect_filled(card, 12.0, if resp.hovered() { C_CARD_HOT } else { C_CARD }); + p.rect_stroke(card, 12.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + p.text( + egui::pos2(card.center().x, card.top() + card.height() * 0.40), + egui::Align2::CENTER_CENTER, + intent.icon, + icons::font(30.0), + intent.accent, + ); + p.text( + egui::pos2(card.center().x, card.bottom() - 18.0), + egui::Align2::CENTER_CENTER, + intent.label, + egui::FontId::proportional(15.0), + C_BRIGHT, + ); + + if resp.clicked() { + app.create_new_project_with_focus(intent.focus); + app.mobile_state.window_top = intent.window.0; + app.mobile_state.window_count = intent.window.1; + app.mobile_state.weights = [1.0, 1.0, 1.0]; + } + } + + // Recent projects list in the bottom third. + let recent_top = content_top + grid_h + 14.0; + ui.painter().text( + egui::pos2(left, recent_top), + egui::Align2::LEFT_TOP, + "Recent", + egui::FontId::proportional(13.0), + C_DIM, + ); + let list_top = recent_top + 22.0; + let recents = app.config.get_recent_files(); + if recents.is_empty() { + ui.painter().text( + egui::pos2(left, list_top + 8.0), + egui::Align2::LEFT_TOP, + "No recent projects", + egui::FontId::proportional(12.0), + C_DIM, + ); + } else { + let row_h = 38.0; + let row_gap = 6.0; + let avail = rect.bottom() - margin - list_top; + let max_rows = ((avail + row_gap) / (row_h + row_gap)).floor().max(0.0) as usize; + let mut chosen: Option = None; + for (j, path) in recents.iter().take(max_rows).enumerate() { + let ry = list_top + j as f32 * (row_h + row_gap); + let row_rect = + egui::Rect::from_min_max(egui::pos2(left, ry), egui::pos2(right, ry + row_h)); + let resp = + ui.interact(row_rect, ui.id().with(("mobile_recent", j)), egui::Sense::click()); + let p = ui.painter(); + p.rect_filled(row_rect, 8.0, if resp.hovered() { C_CARD_HOT } else { C_CARD }); + p.rect_stroke(row_rect, 8.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + p.text( + egui::pos2(row_rect.left() + 12.0, row_rect.center().y), + egui::Align2::LEFT_CENTER, + icons::FOLDER_OPEN, + icons::font(15.0), + C_DIM, + ); + let name = path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "Untitled".to_string()); + p.text( + egui::pos2(row_rect.left() + 36.0, row_rect.center().y), + egui::Align2::LEFT_CENTER, + &name, + egui::FontId::proportional(13.0), + C_BRIGHT, + ); + if resp.clicked() { + chosen = Some(path.clone()); + } + } + if let Some(path) = chosen { + app.load_from_file(path); + app.app_mode = crate::AppMode::Editor; + } + } + }); +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 970c367..2403af8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -18,6 +18,7 @@ use crate::panes::NodePath; use crate::RenderContext; pub mod icons; +pub mod intent; mod stack; mod surface; mod transport; From 87263489c0fbee2a5e40ee93d76779fd1fa76194 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 30 Jun 2026 22:16:14 -0400 Subject: [PATCH 13/43] Add mobile selection inspector sheet (P3); editable layer properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New mobile/inspector.rs: when something is selected/focused, a bottom sheet overlays the lower stack (no reflow) above the transport, showing the object's properties by reusing InfopanelPane full-bleed. Header shows a title + ✕ to deselect; jump-to chips (Timeline, Nodes) reframe the stack to that surface with the selection kept. Grab handle resizes the sheet. - Make the Infopanel Layer section editable (benefits desktop too): Name (text, commit on Enter/blur), Opacity and Volume sliders (live preview, single-undo on release), and Mute/Solo/Lock toggles. Backs the plan to reduce mobile layer headers to a minimal swatch. Co-Authored-By: Claude Opus 4.8 --- .../src/mobile/inspector.rs | 179 ++++++++++++++++++ .../lightningbeam-editor/src/mobile/mod.rs | 19 +- .../src/panes/infopanel.rs | 175 +++++++++++++---- 3 files changed, 338 insertions(+), 35 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs new file mode 100644 index 0000000..7145a91 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -0,0 +1,179 @@ +//! Selection inspector bottom sheet. When something is selected/focused it rises above the +//! transport, showing the focused object's properties by reusing `InfopanelPane` full-bleed. +//! Jump-to chips slide the stack window to the related surface; the ✕ deselects. + +use eframe::egui; +use lightningbeam_core::pane::PaneType; +use lightningbeam_core::selection::FocusSelection; + +use super::{surface, MobileState, MOBILE_NS}; +use crate::panes::{NodePath, SharedPaneState}; +use crate::RenderContext; + +const C_SHEET: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_CYAN: egui::Color32 = egui::Color32::from_rgb(0x54, 0xc3, 0xe8); + +const GRAB_H: f32 = 16.0; +const HEAD_H: f32 = 30.0; +const CHIP_H: f32 = 30.0; + +fn inspector_path() -> NodePath { + vec![MOBILE_NS, 200] +} + +/// Whether anything is selected/focused (i.e. the inspector should be shown). +pub fn is_active(shared: &SharedPaneState) -> bool { + !shared.focus.is_none() || !shared.selection.is_empty() +} + +/// A short title describing what's selected. +fn title(shared: &SharedPaneState) -> String { + use lightningbeam_core::layer::{AnyLayer, AudioLayerType}; + let plural = |n: usize, s: &str| { + if n == 1 { + format!("1 {s}") + } else { + format!("{n} {s}s") + } + }; + match &*shared.focus { + FocusSelection::Layers(ids) => { + let doc = shared.action_executor.document(); + match ids.len() { + 0 => "Layer".to_string(), + 1 => { + if let Some(l) = doc.get_layer(&ids[0]) { + let ty = match l { + AnyLayer::Vector(_) => "Vector", + AnyLayer::Audio(a) => match a.audio_layer_type { + AudioLayerType::Midi => "MIDI", + AudioLayerType::Sampled => "Audio", + }, + AnyLayer::Video(_) => "Video", + AnyLayer::Effect(_) => "Effect", + AnyLayer::Group(_) => "Group", + AnyLayer::Raster(_) => "Raster", + AnyLayer::Text(_) => "Text", + }; + format!("{} · {} layer", l.name(), ty) + } else { + "Layer".to_string() + } + } + n => plural(n, "layer"), + } + } + FocusSelection::ClipInstances(ids) => plural(ids.len(), "clip"), + FocusSelection::Notes { indices, .. } => plural(indices.len().max(1), "note"), + FocusSelection::Nodes(ids) => plural(ids.len(), "node"), + FocusSelection::Assets(ids) => plural(ids.len(), "asset"), + FocusSelection::Geometry { .. } | FocusSelection::None => "Selection".to_string(), + } +} + +/// A jump-to chip: its label and the stack window it brings into view (top, count). These reframe +/// to a related surface with the object still selected. +struct Chip { + label: &'static str, + window: (usize, usize), +} + +const CHIPS: [Chip; 2] = [ + Chip { label: "Timeline", window: (3, 1) }, // Timeline = STACK index 3 + Chip { label: "Nodes", window: (6, 1) }, // Node/Instrument = STACK index 6 +]; + +pub fn render( + ui: &mut egui::Ui, + rect: egui::Rect, + region_h: f32, + rc: &mut RenderContext, + state: &mut MobileState, +) { + // Sheet background with rounded top. + ui.painter().rect_filled( + rect, + egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }, + C_SHEET, + ); + ui.painter().hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, C_LINE)); + + // Grab handle — drag to resize the sheet. + let grab = egui::Rect::from_min_max( + rect.left_top(), + egui::pos2(rect.right(), rect.top() + GRAB_H), + ); + let gresp = ui.interact(grab, ui.id().with("mobile_inspector_grab"), egui::Sense::drag()); + let pill = egui::Rect::from_center_size(grab.center(), egui::vec2(34.0, 4.0)); + ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { C_CYAN } else { C_LINE }); + if gresp.dragged() && region_h > 1.0 { + state.inspector_frac = (state.inspector_frac - gresp.drag_delta().y / region_h).clamp(0.2, 0.85); + } + if gresp.hovered() || gresp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); + } + + // Header row: title + close. + let head_y = rect.top() + GRAB_H; + let head = egui::Rect::from_min_max( + egui::pos2(rect.left(), head_y), + egui::pos2(rect.right(), head_y + HEAD_H), + ); + ui.painter().text( + egui::pos2(head.left() + 14.0, head.center().y), + egui::Align2::LEFT_CENTER, + title(&rc.shared), + egui::FontId::proportional(13.0), + C_BRIGHT, + ); + let close = egui::Rect::from_min_size(egui::pos2(head.right() - 36.0, head.top()), egui::vec2(36.0, HEAD_H)); + let cresp = ui.interact(close, ui.id().with("mobile_inspector_close"), egui::Sense::click()); + ui.painter().text( + close.center(), + egui::Align2::CENTER_CENTER, + super::icons::X, + super::icons::font(16.0), + if cresp.hovered() { C_BRIGHT } else { C_DIM }, + ); + if cresp.clicked() { + rc.shared.selection.clear(); + *rc.shared.focus = FocusSelection::None; + } + + // Jump-to chips (reframe to a related surface, keeping the selection). + let chip_y = head.bottom(); + let mut cx = rect.left() + 12.0; + for (i, chip) in CHIPS.iter().enumerate() { + let galley = ui.painter().layout_no_wrap( + chip.label.to_string(), + egui::FontId::proportional(11.0), + C_BRIGHT, + ); + let w = galley.size().x + 18.0; + let chip_rect = egui::Rect::from_min_size(egui::pos2(cx, chip_y + 4.0), egui::vec2(w, CHIP_H - 8.0)); + let resp = ui.interact(chip_rect, ui.id().with(("mobile_inspector_chip", i)), egui::Sense::click()); + ui.painter().rect_filled(chip_rect, 11.0, if resp.hovered() { C_LINE } else { C_SHEET }); + ui.painter().rect_stroke(chip_rect, 11.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + ui.painter().galley(chip_rect.center() - galley.size() * 0.5, galley, C_BRIGHT); + if resp.clicked() { + let (top, count) = chip.window; + state.window_top = top; + state.window_count = count; + state.weights = [1.0, 1.0, 1.0]; + state.anim = None; + } + cx += w + 6.0; + } + + // Properties content — reuse the Infopanel full-bleed. + let content = egui::Rect::from_min_max( + egui::pos2(rect.left(), chip_y + CHIP_H), + rect.max, + ); + if content.height() > 1.0 { + surface::render_surface_fullbleed(ui, content, &inspector_path(), PaneType::Infopanel, rc); + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 2403af8..9d90bd4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -18,6 +18,7 @@ use crate::panes::NodePath; use crate::RenderContext; pub mod icons; +mod inspector; pub mod intent; mod stack; mod surface; @@ -145,6 +146,8 @@ pub struct MobileState { pub weights: [f32; 3], /// Node/Instrument band: false = node editor, true = instrument/preset browser. pub show_instruments: bool, + /// Inspector sheet height as a fraction of the region above the transport. + pub inspector_frac: f32, /// Active handle drag (transient). pub drag: Option, /// In-flight layout ease (transient). @@ -159,6 +162,7 @@ impl Default for MobileState { window_count: 2, weights: [1.0, 1.0, 1.0], show_instruments: false, + inspector_frac: 0.45, drag: None, anim: None, } @@ -180,12 +184,23 @@ pub fn render_mobile_shell( egui::pos2(available_rect.left(), available_rect.bottom() - TRANSPORT_H), available_rect.max, ); - let stack_rect = egui::Rect::from_min_max( + // Region above the transport, shared between the stack and (when something is selected) the + // inspector sheet that rises above the transport. + let region = egui::Rect::from_min_max( available_rect.min, egui::pos2(available_rect.right(), transport_rect.top()), ); + // The stack always fills the region; the inspector sheet overlays its lower part (no reflow). + stack::render(ui, region, rc, state); - stack::render(ui, stack_rect, rc, state); + if inspector::is_active(&rc.shared) { + let sheet_h = (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0); + let sheet_rect = egui::Rect::from_min_max( + egui::pos2(region.left(), region.bottom() - sheet_h), + region.max, + ); + inspector::render(ui, sheet_rect, region.height(), rc, state); + } // Transport floor: drawn last = always on top, the persistent spine. transport::render(ui, transport_rect, &mut rc.shared); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs index 0391dcd..f776dcf 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -44,6 +44,11 @@ pub struct InfopanelPane { selected_tool_gradient_stop: Option, /// FPS value captured when a drag/focus-in starts (for single-undo-action on commit) fps_drag_start: Option, + /// In-progress layer-name edit buffer, keyed by the layer being edited. + layer_name_edit: Option<(Uuid, String)>, + /// Layer opacity/volume captured when a slider drag starts (single-undo on commit). + layer_opacity_drag_start: Option, + layer_volume_drag_start: Option, /// Resize mode for the active raster layer's "to document size" action (scale vs canvas). raster_resize_mode: lightningbeam_core::raster_layer::RasterResizeMode, /// Font families already registered into egui for the family-picker previews @@ -69,6 +74,9 @@ impl InfopanelPane { selected_shape_gradient_stop: None, selected_tool_gradient_stop: None, fps_drag_start: None, + layer_name_edit: None, + layer_opacity_drag_start: None, + layer_volume_drag_start: None, raster_resize_mode: lightningbeam_core::raster_layer::RasterResizeMode::Scale, registered_preview_fonts: std::collections::HashSet::new(), } @@ -1165,8 +1173,9 @@ impl InfopanelPane { } /// Render layer info section - fn render_layer_section(&self, ui: &mut Ui, path: &NodePath, shared: &SharedPaneState, layer_ids: &[Uuid]) { - let document = shared.action_executor.document(); + fn render_layer_section(&mut self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState, layer_ids: &[Uuid]) { + use lightningbeam_core::actions::{set_layer_properties::LayerProperty, SetLayerPropertiesAction}; + use lightningbeam_core::layer::AudioLayerType; egui::CollapsingHeader::new("Layer") .id_salt(("layer_info", path)) @@ -1174,18 +1183,22 @@ impl InfopanelPane { .show(ui, |ui| { ui.add_space(4.0); - if layer_ids.len() == 1 { - if let Some(layer) = document.get_layer(&layer_ids[0]) { - ui.horizontal(|ui| { - ui.label("Name:"); - ui.label(layer.name()); - }); + if layer_ids.len() != 1 { + ui.label(format!("{} layers selected", layer_ids.len())); + ui.add_space(4.0); + return; + } + let layer_id = layer_ids[0]; + // Snapshot the current values, then drop the document borrow before mutating shared. + let Some((type_name, is_audio, name, opacity, volume, muted, soloed, locked)) = ({ + let document = shared.action_executor.document(); + document.get_layer(&layer_id).map(|layer| { let type_name = match layer { AnyLayer::Vector(_) => "Vector", AnyLayer::Audio(a) => match a.audio_layer_type { - lightningbeam_core::layer::AudioLayerType::Midi => "MIDI", - lightningbeam_core::layer::AudioLayerType::Sampled => "Audio", + AudioLayerType::Midi => "MIDI", + AudioLayerType::Sampled => "Audio", }, AnyLayer::Video(_) => "Video", AnyLayer::Effect(_) => "Effect", @@ -1193,34 +1206,130 @@ impl InfopanelPane { AnyLayer::Raster(_) => "Raster", AnyLayer::Text(_) => "Text", }; - ui.horizontal(|ui| { - ui.label("Type:"); - ui.label(type_name); - }); + ( + type_name, + matches!(layer, AnyLayer::Audio(_)), + layer.name().to_string(), + layer.opacity(), + layer.volume(), + layer.muted(), + layer.soloed(), + layer.locked(), + ) + }) + }) else { + return; + }; - ui.horizontal(|ui| { - ui.label("Opacity:"); - ui.label(format!("{:.0}%", layer.opacity() * 100.0)); - }); - - if matches!(layer, AnyLayer::Audio(_)) { - ui.horizontal(|ui| { - ui.label("Volume:"); - ui.label(format!("{:.0}%", layer.volume() * 100.0)); - }); - } - - if layer.muted() { - ui.label("Muted"); - } - if layer.locked() { - ui.label("Locked"); + // Name (editable; commits on Enter / focus loss). + if self.layer_name_edit.as_ref().map(|(id, _)| *id) != Some(layer_id) { + self.layer_name_edit = Some((layer_id, name.clone())); + } + ui.horizontal(|ui| { + ui.label("Name:"); + let buf = &mut self.layer_name_edit.as_mut().unwrap().1; + let resp = ui.text_edit_singleline(buf); + let commit = resp.lost_focus() + && (ui.input(|i| i.key_pressed(egui::Key::Enter)) || !resp.has_focus()); + if commit { + let new_name = buf.trim().to_string(); + if !new_name.is_empty() && new_name != name { + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Name(new_name), + ))); } } - } else { - ui.label(format!("{} layers selected", layer_ids.len())); + }); + + ui.horizontal(|ui| { + ui.label("Type:"); + ui.label(type_name); + }); + + // Opacity slider (single-undo: live-preview while dragging, commit on release). + ui.horizontal(|ui| { + ui.label("Opacity:"); + let mut op = opacity; + let r = ui.add(egui::Slider::new(&mut op, 0.0..=1.0).show_value(false)); + ui.label(format!("{:.0}%", op * 100.0)); + if (r.drag_started() || r.gained_focus()) && self.layer_opacity_drag_start.is_none() { + self.layer_opacity_drag_start = Some(opacity); + } + if r.changed() { + if let Some(l) = shared.action_executor.document_mut().get_layer_mut(&layer_id) { + l.set_opacity(op); + } + } + if r.drag_stopped() || r.lost_focus() { + if let Some(start) = self.layer_opacity_drag_start.take() { + if (start - op).abs() > 1e-6 { + if let Some(l) = shared.action_executor.document_mut().get_layer_mut(&layer_id) { + l.set_opacity(start); // revert so the action owns the change + } + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Opacity(op), + ))); + } + } + } + }); + + if is_audio { + ui.horizontal(|ui| { + ui.label("Volume:"); + let mut vol = volume; + let r = ui.add(egui::Slider::new(&mut vol, 0.0..=1.0).show_value(false)); + ui.label(format!("{:.0}%", vol * 100.0)); + if (r.drag_started() || r.gained_focus()) && self.layer_volume_drag_start.is_none() { + self.layer_volume_drag_start = Some(volume); + } + if r.changed() { + if let Some(l) = shared.action_executor.document_mut().get_layer_mut(&layer_id) { + l.set_volume(vol); + } + } + if r.drag_stopped() || r.lost_focus() { + if let Some(start) = self.layer_volume_drag_start.take() { + if (start - vol).abs() > 1e-6 { + if let Some(l) = shared.action_executor.document_mut().get_layer_mut(&layer_id) { + l.set_volume(start); + } + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Volume(vol), + ))); + } + } + } + }); } + // Toggles: mute / solo (audio) / lock. + ui.horizontal(|ui| { + if is_audio { + if ui.selectable_label(muted, "Mute").clicked() { + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Muted(!muted), + ))); + } + if ui.selectable_label(soloed, "Solo").clicked() { + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Soloed(!soloed), + ))); + } + } + if ui.selectable_label(locked, "Lock").clicked() { + shared.pending_actions.push(Box::new(SetLayerPropertiesAction::new( + layer_id, + LayerProperty::Locked(!locked), + ))); + } + }); + ui.add_space(4.0); }); } From 08b6aa01390fe036c529ef7200fed284446f8706 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 01:14:31 -0400 Subject: [PATCH 14/43] Compress mobile layer headers to a swatch; conditional inspector reflow - Add `is_mobile` to SharedPaneState (set in build_frame). On mobile the timeline layer-header column collapses to a minimal type-colored swatch (1:2 w:h, ~30px), brighter/outlined when active/selected; tapping it selects the layer (handled by the existing header-column input path, which now uses the narrower width). Desktop headers unchanged. - Inspector reflow: the sheet overlaps the stack by default, but when the selected pane would be covered by the sheet, the stack reflows (renders above the sheet) so it stays visible. Restore on dismiss is automatic since reflow only changes the render rect; manual divider moves while the sheet is up persist. Adds stack::pane_bottom_in and inspector::target_slot. --- .../lightningbeam-editor/src/main.rs | 2 + .../src/mobile/inspector.rs | 12 +++++ .../lightningbeam-editor/src/mobile/mod.rs | 33 +++++++++---- .../lightningbeam-editor/src/mobile/stack.rs | 13 +++++ .../lightningbeam-editor/src/panes/mod.rs | 2 + .../src/panes/timeline.rs | 47 +++++++++++++++---- 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 90fc1a0..e2f8e0d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -7163,9 +7163,11 @@ impl EditorApp { /// `scratch` for `'a`; it must be dropped before the post-render drain touches /// `self`/`scratch` again. This is the single source of truth for `SharedPaneState`. fn build_frame<'a>(&'a mut self, scratch: &'a mut FrameScratch) -> FrameBundle<'a> { + let is_mobile = self.mobile_active(); FrameBundle { rc: RenderContext { shared: panes::SharedPaneState { + is_mobile, container_path: self.current_file_path.clone(), onion: { // Onion skinning is disabled during playback. diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index 7145a91..2cbeb4f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -29,6 +29,18 @@ pub fn is_active(shared: &SharedPaneState) -> bool { !shared.focus.is_none() || !shared.selection.is_empty() } +/// The stack slot (see `super::STACK`) where the current selection lives, so we can tell if the +/// sheet would cover it. Geometry/selection lives on the Stage; clips/layers on the Timeline; etc. +pub fn target_slot(shared: &SharedPaneState) -> usize { + match &*shared.focus { + FocusSelection::Notes { .. } => 4, // PianoRoll + FocusSelection::Nodes(_) => 6, // Node/Instrument + FocusSelection::Assets(_) => 1, // Asset Library + FocusSelection::ClipInstances(_) | FocusSelection::Layers(_) => 3, // Timeline + FocusSelection::Geometry { .. } | FocusSelection::None => 2, // Stage + } +} + /// A short title describing what's selected. fn title(shared: &SharedPaneState) -> String { use lightningbeam_core::layer::{AnyLayer, AudioLayerType}; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 9d90bd4..0c12ca5 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -190,15 +190,32 @@ pub fn render_mobile_shell( available_rect.min, egui::pos2(available_rect.right(), transport_rect.top()), ); - // The stack always fills the region; the inspector sheet overlays its lower part (no reflow). - stack::render(ui, region, rc, state); + // When the inspector is up, the sheet overlays the lower part of the stack. If the selected + // pane would be *covered* by the sheet, reflow (shrink the stack above the sheet) so it stays + // visible; otherwise leave the stack full-height and just overlay. Restoring on dismiss is + // automatic — reflow only changes the render rect, not the window state. + let inspector_shown = inspector::is_active(&rc.shared); + let sheet_h = if inspector_shown { + (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0) + } else { + 0.0 + }; + let sheet_top = region.bottom() - sheet_h; - if inspector::is_active(&rc.shared) { - let sheet_h = (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0); - let sheet_rect = egui::Rect::from_min_max( - egui::pos2(region.left(), region.bottom() - sheet_h), - region.max, - ); + let covered = inspector_shown + && stack::pane_bottom_in(state, region, inspector::target_slot(&rc.shared)) + .map(|bottom| bottom > sheet_top + 1.0) + .unwrap_or(false); + let stack_rect = if covered { + egui::Rect::from_min_max(region.min, egui::pos2(region.right(), sheet_top)) + } else { + region + }; + + stack::render(ui, stack_rect, rc, state); + + if inspector_shown { + let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); inspector::render(ui, sheet_rect, region.height(), rc, state); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index ab46feb..8debad8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -224,6 +224,19 @@ fn nweights(weights: &[f32; 3], count: usize) -> Vec { w } +/// The bottom Y of the given stack slot's band within `region` (using the current window + +/// weights), or None if that slot isn't in the visible window. Used to decide whether the +/// inspector sheet would cover the selected pane. +pub fn pane_bottom_in(state: &MobileState, region: egui::Rect, slot: usize) -> Option { + let (top, count) = (state.window_top, state.window_count); + if slot < top || slot >= top + count { + return None; + } + let nw = nweights(&state.weights, count); + let cum: f32 = nw[..=(slot - top)].iter().sum(); + Some(region.top() + cum * region.height()) +} + /// Lay out `count` panes in `rect` using normalized weights `nw`. fn config_rects(top: usize, count: usize, rect: egui::Rect, nw: &[f32]) -> Vec<(usize, egui::Rect)> { let h = rect.height(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index 7d1acb8..d1f1fe4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -356,6 +356,8 @@ pub struct SharedPaneState<'a> { /// on the first frame; panes (e.g. infopanel) convert the pixel data to egui /// TextureHandles. Each entry is `(width, height, sRGB-premultiplied RGBA bytes)`. pub brush_preview_pixels: &'a std::sync::Arc)>>>, + /// True when rendering the phone/mobile shell (panes can render more compactly). + pub is_mobile: bool, } /// Trait for pane rendering diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index f233858..158ec7b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -17,6 +17,8 @@ use std::collections::HashMap; const RULER_HEIGHT: f32 = 30.0; const LAYER_HEIGHT: f32 = 60.0; const LAYER_HEADER_WIDTH: f32 = 200.0; +/// On mobile the header collapses to a minimal color swatch with a 1:2 width:height aspect. +const MOBILE_LAYER_HEADER_WIDTH: f32 = LAYER_HEIGHT * 0.5; const MIN_PIXELS_PER_SECOND: f32 = 1.0; // Allow zooming out to see 10+ minutes const MAX_PIXELS_PER_SECOND: f32 = 500.0; const EDGE_DETECTION_PIXELS: f32 = 8.0; // Distance from edge to detect trim handles @@ -1937,6 +1939,8 @@ impl TimelinePane { track_levels: &std::collections::HashMap, input_level: f32, playback_time: f64, + header_width: f32, + is_mobile: bool, ) { // Background for header column let header_style = theme.style(".timeline-header", ui.ctx()); @@ -1999,7 +2003,7 @@ impl TimelinePane { let header_rect = egui::Rect::from_min_size( egui::pos2(rect.min.x, y), - egui::vec2(LAYER_HEADER_WIDTH, LAYER_HEIGHT), + egui::vec2(header_width, LAYER_HEIGHT), ); // Determine the AnyLayer or GroupLayer for this row @@ -2033,6 +2037,27 @@ impl TimelinePane { ui.painter().rect_filled(header_rect, 0.0, bg_color); + // Mobile: collapse the header to a minimal color swatch (1:2 w:h). Selection is handled + // by handle_input via the (narrow) header rect, so we only draw here. + if is_mobile { + let sw = header_rect.shrink(2.0); + let col = if is_active || is_selected { + type_color + } else { + type_color.gamma_multiply(0.55) + }; + ui.painter().rect_filled(sw, 3.0, col); + if is_active || is_selected { + ui.painter().rect_stroke( + sw, + 3.0, + egui::Stroke::new(1.5, egui::Color32::WHITE), + egui::StrokeKind::Inside, + ); + } + continue; + } + // Gutter area (left of indicator) — solid group color, with collapse chevron if indent > 0.0 { let gutter_rect = egui::Rect::from_min_size( @@ -5456,15 +5481,21 @@ impl PaneRenderer for TimelinePane { } self.duration = max_endpoint; - // Split into layer header column (left) and timeline content (right) + // Split into layer header column (left) and timeline content (right). On mobile the header + // column collapses to a minimal color-swatch width. + let header_width = if shared.is_mobile { + MOBILE_LAYER_HEADER_WIDTH + } else { + LAYER_HEADER_WIDTH + }; let header_column_rect = egui::Rect::from_min_size( rect.min, - egui::vec2(LAYER_HEADER_WIDTH, rect.height()), + egui::vec2(header_width, rect.height()), ); let timeline_rect = egui::Rect::from_min_size( - rect.min + egui::vec2(LAYER_HEADER_WIDTH, 0.0), - egui::vec2(rect.width() - LAYER_HEADER_WIDTH, rect.height()), + rect.min + egui::vec2(header_width, 0.0), + egui::vec2(rect.width() - header_width, rect.height()), ); // Split timeline into ruler and content areas @@ -5481,12 +5512,12 @@ impl PaneRenderer for TimelinePane { // Split header column into ruler area (top) and layer headers (bottom) let header_ruler_spacer = egui::Rect::from_min_size( header_column_rect.min, - egui::vec2(LAYER_HEADER_WIDTH, RULER_HEIGHT), + egui::vec2(header_width, RULER_HEIGHT), ); let layer_headers_rect = egui::Rect::from_min_size( header_column_rect.min + egui::vec2(0.0, RULER_HEIGHT), - egui::vec2(LAYER_HEADER_WIDTH, header_column_rect.height() - RULER_HEIGHT), + egui::vec2(header_width, header_column_rect.height() - RULER_HEIGHT), ); // Save original clip rect to restore at the end @@ -5503,7 +5534,7 @@ impl PaneRenderer for TimelinePane { // Render layer header column with clipping ui.set_clip_rect(layer_headers_rect.intersect(original_clip_rect)); - self.render_layer_headers(ui, layer_headers_rect, shared.theme, shared.active_layer_id, shared.focus, &mut shared.pending_actions, document, &context_layers, shared.layer_to_track_map, shared.track_levels, shared.input_level, *shared.playback_time); + self.render_layer_headers(ui, layer_headers_rect, shared.theme, shared.active_layer_id, shared.focus, &mut shared.pending_actions, document, &context_layers, shared.layer_to_track_map, shared.track_levels, shared.input_level, *shared.playback_time, header_width, shared.is_mobile); // Render time ruler (clip to ruler rect) ui.set_clip_rect(ruler_rect.intersect(original_clip_rect)); From b8419778a5695f3ccc571dfc25477b2b7dc849fc Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 05:42:46 -0400 Subject: [PATCH 15/43] Add omnibutton FAB (P4); fix Stage playback freeze when timeline hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Omnibutton (mobile/omni.rs): a context FAB whose radial holds the active layer's drawing tools laid out in a staggered inner/outer arc, plus a "+New" petal (create Vector/Audio/MIDI/Raster/Video layer, Group, Import, and Split/Duplicate when a clip is selected — dispatched as MenuActions) and a "more" petal that opens a full-tools grid when the layer has extra tools. Contextual: the tool ring only appears when the Stage is in the visible window; off-Stage the FAB is a "+New" create button. Tools/creates use Lucide icons. Pane-internal adds (nodes/notes/instruments) deferred to P6. Playback fix: document.current_time (read by the renderer for animation eval and video-frame decode) was only synced from the audio playback position inside TimelinePane::render_content. In the mobile shell, panes render on demand, so with the timeline off-screen current_time went stale and the Stage froze during playback. Move the sync into the per-frame block in update() so it runs regardless of which panes are visible. --- .../lightningbeam-editor/src/main.rs | 5 + .../lightningbeam-editor/src/mobile/icons.rs | 25 ++ .../lightningbeam-editor/src/mobile/mod.rs | 13 + .../lightningbeam-editor/src/mobile/omni.rs | 347 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index e2f8e0d..f00990a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6582,6 +6582,11 @@ impl eframe::App for EditorApp { { let time = self.playback_time; let document = self.action_executor.document_mut(); + // Keep document.current_time synced from the audio playback position every frame. + // The renderer reads current_time for animation eval + video-frame decode, so this + // must not depend on any particular pane (e.g. the timeline) being rendered — + // otherwise the Stage freezes during playback when that pane is off-screen. + document.current_time = time; // Bake animation transforms for root layers for layer in document.root.children.iter_mut() { if let lightningbeam_core::layer::AnyLayer::Vector(vl) = layer { diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs index d13b6a8..725dd19 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs @@ -53,3 +53,28 @@ pub const MIC: &str = "\u{e118}"; pub const CLAPPERBOARD: &str = "\u{e29b}"; pub const SQUARE_DASHED: &str = "\u{e1cb}"; pub const FOLDER_OPEN: &str = "\u{e247}"; +// Tool icons (omnibutton). +pub const MOUSE_POINTER_2: &str = "\u{e1c3}"; +pub const PENCIL: &str = "\u{e1f9}"; +pub const MOVE: &str = "\u{e121}"; +pub const VECTOR_SQUARE: &str = "\u{e67c}"; +pub const SQUARE: &str = "\u{e167}"; +pub const CIRCLE: &str = "\u{e076}"; +pub const MINUS: &str = "\u{e11c}"; +pub const HEXAGON: &str = "\u{e0f3}"; +pub const PAINT_BUCKET: &str = "\u{e2e6}"; +pub const PIPETTE: &str = "\u{e13b}"; +pub const TYPE: &str = "\u{e198}"; +pub const ERASER: &str = "\u{e28f}"; +pub const PEN_TOOL: &str = "\u{e131}"; +pub const BLEND: &str = "\u{e59c}"; +pub const WAND_SPARKLES: &str = "\u{e357}"; +pub const LASSO_SELECT: &str = "\u{e1cf}"; +pub const SCISSORS: &str = "\u{e14e}"; +// Create-menu icons. +pub const AUDIO_WAVEFORM: &str = "\u{e55b}"; +pub const PIANO: &str = "\u{e561}"; +pub const FOLDER: &str = "\u{e0d7}"; +pub const FILE_PLUS: &str = "\u{e0c9}"; +pub const COPY: &str = "\u{e09e}"; +pub const LAYERS: &str = "\u{e529}"; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 0c12ca5..3e42b9f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -20,6 +20,7 @@ use crate::RenderContext; pub mod icons; mod inspector; pub mod intent; +mod omni; mod stack; mod surface; mod transport; @@ -148,6 +149,12 @@ pub struct MobileState { pub show_instruments: bool, /// Inspector sheet height as a fraction of the region above the transport. pub inspector_frac: f32, + /// Whether the omnibutton radial tool menu is open. + pub omni_open: bool, + /// Whether the omnibutton "more" grid (all tools) is open. + pub omni_grid_open: bool, + /// Whether the omnibutton "+New" create grid is open. + pub omni_create_open: bool, /// Active handle drag (transient). pub drag: Option, /// In-flight layout ease (transient). @@ -163,6 +170,9 @@ impl Default for MobileState { weights: [1.0, 1.0, 1.0], show_instruments: false, inspector_frac: 0.45, + omni_open: false, + omni_grid_open: false, + omni_create_open: false, drag: None, anim: None, } @@ -221,4 +231,7 @@ pub fn render_mobile_shell( // Transport floor: drawn last = always on top, the persistent spine. transport::render(ui, transport_rect, &mut rc.shared); + + // Omnibutton FAB (radial tool menu) — drawn above the stack region, on top of everything else. + omni::render(ui, region, rc, state); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs new file mode 100644 index 0000000..3d80d29 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs @@ -0,0 +1,347 @@ +//! Omnibutton — a floating action button whose radial holds the active layer's drawing tools plus +//! a "+New" petal (create layers / import / clip ops) and, when there are extra tools, a "⋯ more" +//! petal that opens a full-tools grid. Tools drive `shared.selected_tool`; create actions dispatch +//! `MenuAction`s. The radial fans up and to the left, away from the thumb. Pane-internal adds +//! (nodes/notes/instruments) are handled inside their panes (P6), not here. + +use eframe::egui; +use lightningbeam_core::layer::{AnyLayer, LayerType}; +use lightningbeam_core::selection::FocusSelection; +use lightningbeam_core::tool::Tool; + +use super::{icons, MobileState}; +use crate::menu::MenuAction; +use crate::RenderContext; + +const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); +const C_DARK: egui::Color32 = egui::Color32::from_rgb(0x1b, 0x13, 0x0a); +const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_PANEL2: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); + +const FAB_R: f32 = 26.0; +const IR: f32 = 20.0; +const PRIMARY: [Tool; 14] = [ + Tool::Select, + Tool::Draw, + Tool::Transform, + Tool::Rectangle, + Tool::Ellipse, + Tool::Line, + Tool::Polygon, + Tool::PaintBucket, + Tool::Gradient, + Tool::Eyedropper, + Tool::Text, + Tool::Erase, + Tool::BezierEdit, + Tool::Split, +]; +const RING_CAP: usize = 10; + +#[derive(Clone, Copy)] +enum Special { + More, + New, +} + +fn tool_icon(t: Tool) -> &'static str { + match t { + Tool::Select | Tool::RegionSelect => icons::MOUSE_POINTER_2, + Tool::SelectLasso => icons::LASSO_SELECT, + Tool::Draw | Tool::Pencil | Tool::Pen => icons::PENCIL, + Tool::Transform => icons::VECTOR_SQUARE, + Tool::Rectangle => icons::SQUARE, + Tool::Ellipse => icons::CIRCLE, + Tool::Line => icons::MINUS, + Tool::Polygon => icons::HEXAGON, + Tool::PaintBucket => icons::PAINT_BUCKET, + Tool::Gradient => icons::BLEND, + Tool::Eyedropper => icons::PIPETTE, + Tool::Text => icons::TYPE, + Tool::Erase => icons::ERASER, + Tool::BezierEdit => icons::PEN_TOOL, + Tool::MagicWand | Tool::QuickSelect => icons::WAND_SPARKLES, + Tool::Split => icons::SCISSORS, + _ => icons::BRUSH, + } +} + +fn active_layer_type(rc: &RenderContext) -> Option { + rc.shared + .active_layer_id + .and_then(|id| rc.shared.action_executor.document().get_layer(&id)) + .map(|layer| match layer { + AnyLayer::Vector(_) => LayerType::Vector, + AnyLayer::Audio(_) => LayerType::Audio, + AnyLayer::Video(_) => LayerType::Video, + AnyLayer::Effect(_) => LayerType::Effect, + AnyLayer::Group(_) => LayerType::Group, + AnyLayer::Raster(_) => LayerType::Raster, + AnyLayer::Text(_) => LayerType::Text, + }) +} + +/// The create-menu items: (label, icon, action, enabled). Split/Duplicate need a selected clip. +fn create_items(rc: &RenderContext) -> Vec<(&'static str, &'static str, MenuAction, bool)> { + let clip_sel = matches!(&*rc.shared.focus, FocusSelection::ClipInstances(ids) if !ids.is_empty()); + vec![ + ("Vector", icons::PEN_TOOL, MenuAction::AddLayer, true), + ("Audio", icons::AUDIO_WAVEFORM, MenuAction::AddAudioTrack, true), + ("MIDI", icons::PIANO, MenuAction::AddMidiTrack, true), + ("Raster", icons::BRUSH, MenuAction::AddRasterLayer, true), + ("Video", icons::CLAPPERBOARD, MenuAction::AddVideoLayer, true), + ("Group", icons::FOLDER, MenuAction::Group, true), + ("Import", icons::FILE_PLUS, MenuAction::Import, true), + ("Split", icons::SCISSORS, MenuAction::SplitClip, clip_sel), + ("Duplicate", icons::COPY, MenuAction::DuplicateClip, clip_sel), + ] +} + +fn close_all(state: &mut MobileState) { + state.omni_open = false; + state.omni_grid_open = false; + state.omni_create_open = false; +} + +pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { + let ctx_tools: Vec = Tool::for_layer_type(active_layer_type(rc)).to_vec(); + let primary: Vec = PRIMARY + .iter() + .copied() + .filter(|t| ctx_tools.contains(t)) + .take(RING_CAP) + .collect(); + let has_more = ctx_tools.len() > primary.len(); + + // Tools are only relevant when the Stage is on screen. Off-Stage the omnibutton is purely a + // "+New" create button. + let stage_slot = super::STACK + .iter() + .position(|p| *p == super::StackPane::Stage) + .unwrap_or(2); + let stage_visible = (state.window_top..state.window_top + state.window_count).contains(&stage_slot); + if !stage_visible { + state.omni_open = false; // no tool radial off-Stage + } + + let fab_center = egui::pos2(region.right() - 18.0 - FAB_R, region.bottom() - 18.0 - FAB_R); + + if state.omni_grid_open { + // Full-tools grid. + let cells: Vec = ctx_tools + .iter() + .map(|t| Cell { + icon: tool_icon(*t), + label: t.display_name(), + selected: *rc.shared.selected_tool == *t, + enabled: true, + }) + .collect(); + let (close, clicked) = draw_grid(ui, region, "Tools", &cells); + if let Some(i) = clicked { + *rc.shared.selected_tool = ctx_tools[i]; + close_all(state); + } else if close { + close_all(state); + } + } else if state.omni_create_open { + // Create grid. + let items = create_items(rc); + let cells: Vec = items + .iter() + .map(|it| Cell { + icon: it.1, + label: it.0, + selected: false, + enabled: it.3, + }) + .collect(); + let (close, clicked) = draw_grid(ui, region, "New", &cells); + if let Some(i) = clicked { + rc.shared.pending_menu_actions.push(items[i].2); + close_all(state); + } else if close { + close_all(state); + } + } else if state.omni_open { + // Radial: scrim, then tool petals + special petals (more / new). + let scrim = ui.interact(region, ui.id().with("mobile_omni_scrim"), egui::Sense::click()); + ui.painter() + .rect_filled(region, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 140)); + if scrim.clicked() { + state.omni_open = false; + } + + let mut specials: Vec = Vec::new(); + if has_more { + specials.push(Special::More); + } + specials.push(Special::New); + let n = primary.len() + specials.len(); + + for k in 0..n { + let frac = if n > 1 { k as f32 / (n as f32 - 1.0) } else { 0.5 }; + let deg = 74.0 + frac * 126.0; // 74°..200°, up and to the left + let a = deg.to_radians(); + let radius = if k % 2 == 0 { 114.0 } else { 170.0 }; + let c = fab_center + egui::vec2(radius * a.cos(), -radius * a.sin()); + let rect = egui::Rect::from_center_size(c, egui::vec2(IR * 2.0, IR * 2.0)); + + if k < primary.len() { + let t = primary[k]; + let resp = ui.interact(rect, ui.id().with(("mobile_omni_tool", k)), egui::Sense::click()); + let selected = *rc.shared.selected_tool == t; + petal(ui, c, if selected { C_AMBER } else { petal_bg(&resp) }, tool_icon(t), if selected { C_DARK } else { C_BRIGHT }); + if resp.clicked() { + *rc.shared.selected_tool = t; + state.omni_open = false; + } + } else { + match specials[k - primary.len()] { + Special::More => { + let resp = ui.interact(rect, ui.id().with("mobile_omni_more"), egui::Sense::click()); + petal(ui, c, petal_bg(&resp), icons::MENU, C_BRIGHT); + if resp.clicked() { + state.omni_grid_open = true; + } + } + Special::New => { + let resp = ui.interact(rect, ui.id().with("mobile_omni_new"), egui::Sense::click()); + petal(ui, c, C_AMBER, icons::PLUS, C_DARK); + if resp.hovered() { + ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.5, C_BRIGHT)); + } + if resp.clicked() { + state.omni_create_open = true; + } + } + } + } + } + } + + // The FAB itself (on top). Closed → current tool icon; open → ✕. + let fab_rect = egui::Rect::from_center_size(fab_center, egui::vec2(FAB_R * 2.0, FAB_R * 2.0)); + let fresp = ui.interact(fab_rect, ui.id().with("mobile_omni_fab"), egui::Sense::click()); + ui.painter().circle_filled(fab_center, FAB_R, C_AMBER); + let open = state.omni_open || state.omni_grid_open || state.omni_create_open; + let glyph = if open { + icons::X + } else if stage_visible { + tool_icon(*rc.shared.selected_tool) + } else { + icons::PLUS // off-Stage: a create button + }; + ui.painter() + .text(fab_center, egui::Align2::CENTER_CENTER, glyph, icons::font(22.0), C_DARK); + if fresp.clicked() { + if open { + close_all(state); + } else if stage_visible { + state.omni_open = true; + } else { + state.omni_create_open = true; + } + } +} + +fn petal_bg(resp: &egui::Response) -> egui::Color32 { + if resp.hovered() { + C_LINE + } else { + C_PANEL2 + } +} + +fn petal(ui: &egui::Ui, c: egui::Pos2, bg: egui::Color32, glyph: &str, fg: egui::Color32) { + ui.painter().circle_filled(c, IR, bg); + ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.0, C_LINE)); + ui.painter().text(c, egui::Align2::CENTER_CENTER, glyph, icons::font(18.0), fg); +} + +struct Cell { + icon: &'static str, + label: &'static str, + selected: bool, + enabled: bool, +} + +/// A modal grid sheet of icon+label cells. Returns (backdrop-tapped, Some(clicked-enabled-index)). +fn draw_grid(ui: &mut egui::Ui, region: egui::Rect, title: &str, cells: &[Cell]) -> (bool, Option) { + let scrim = ui.interact(region, ui.id().with(("mobile_omni_gridscrim", title)), egui::Sense::click()); + ui.painter() + .rect_filled(region, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 170)); + + let panel = region.shrink2(egui::vec2(16.0, 40.0)); + ui.painter().rect_filled(panel, 14.0, C_PANEL); + ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + ui.painter().text( + egui::pos2(panel.left() + 16.0, panel.top() + 18.0), + egui::Align2::LEFT_CENTER, + title, + egui::FontId::proportional(14.0), + C_BRIGHT, + ); + + let cols = 4usize; + let n = cells.len(); + let rows = n.div_ceil(cols).max(1); + let grid = egui::Rect::from_min_max( + egui::pos2(panel.left() + 10.0, panel.top() + 36.0), + egui::pos2(panel.right() - 10.0, panel.bottom() - 10.0), + ); + let cw = grid.width() / cols as f32; + let chh = (grid.height() / rows as f32).min(96.0); + let mut clicked = None; + for (i, cell) in cells.iter().enumerate() { + let col = (i % cols) as f32; + let row = (i / cols) as f32; + let r = egui::Rect::from_min_size( + egui::pos2(grid.left() + col * cw, grid.top() + row * chh), + egui::vec2(cw, chh), + ) + .shrink(5.0); + let resp = if cell.enabled { + ui.interact(r, ui.id().with(("mobile_omni_cell", title, i)), egui::Sense::click()) + } else { + ui.interact(r, ui.id().with(("mobile_omni_cell_off", title, i)), egui::Sense::hover()) + }; + let bg = if cell.selected { + C_AMBER + } else if cell.enabled && resp.hovered() { + C_PANEL2 + } else { + C_PANEL + }; + ui.painter().rect_filled(r, 10.0, bg); + ui.painter().rect_stroke(r, 10.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + let fg = if cell.selected { + C_DARK + } else if cell.enabled { + C_BRIGHT + } else { + C_LINE + }; + ui.painter().text( + egui::pos2(r.center().x, r.top() + r.height() * 0.38), + egui::Align2::CENTER_CENTER, + cell.icon, + icons::font(22.0), + fg, + ); + ui.painter().text( + egui::pos2(r.center().x, r.bottom() - 12.0), + egui::Align2::CENTER_CENTER, + cell.label, + egui::FontId::proportional(9.5), + if cell.selected { C_DARK } else if cell.enabled { C_DIM } else { C_LINE }, + ); + if cell.enabled && resp.clicked() { + clicked = Some(i); + } + } + (scrim.clicked() && clicked.is_none(), clicked) +} From a0ac4f2d3583f371fdbc585eb4e7f120511f75a7 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 06:09:09 -0400 Subject: [PATCH 16/43] =?UTF-8?q?Add=20mobile=20top=20bar:=20filename=20+?= =?UTF-8?q?=20command=20palette=20(=E2=8C=95)=20+=20overflow=20(=E2=8B=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reserve a slim top bar in the mobile shell showing the project filename (or "Lightningbeam" when unsaved) on the left, and ⌕/⋯ on the right. ⋯ opens a curated overflow sheet (Save/Save As/Open/New/Import/Export/Undo/Redo/ Preferences); ⌕ opens a command palette that searches the whole menu tree (flattened from MenuItemDef::menu_structure()). Both dispatch MenuActions. Completes the P4 omnibutton + global menu. --- .../lightningbeam-editor/src/mobile/icons.rs | 1 + .../lightningbeam-editor/src/mobile/mod.rs | 23 +- .../lightningbeam-editor/src/mobile/topbar.rs | 196 ++++++++++++++++++ 3 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs index 725dd19..22c9eaf 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs @@ -78,3 +78,4 @@ pub const FOLDER: &str = "\u{e0d7}"; pub const FILE_PLUS: &str = "\u{e0c9}"; pub const COPY: &str = "\u{e09e}"; pub const LAYERS: &str = "\u{e529}"; +pub const ELLIPSIS: &str = "\u{e0b6}"; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 3e42b9f..00d0806 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -23,6 +23,7 @@ pub mod intent; mod omni; mod stack; mod surface; +mod topbar; mod transport; /// Reserved sentinel namespace for mobile pane-instance paths. Desktop layout paths are built @@ -31,6 +32,7 @@ mod transport; pub const MOBILE_NS: usize = usize::MAX; const TRANSPORT_H: f32 = 60.0; +const TOPBAR_H: f32 = 40.0; /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Any non-empty value other than "0" enables it. @@ -155,6 +157,11 @@ pub struct MobileState { pub omni_grid_open: bool, /// Whether the omnibutton "+New" create grid is open. pub omni_create_open: bool, + /// Top-bar overflow (⋯ commands) sheet open. + pub overflow_open: bool, + /// Top-bar command palette (⌕) open, and its search query. + pub palette_open: bool, + pub palette_query: String, /// Active handle drag (transient). pub drag: Option, /// In-flight layout ease (transient). @@ -173,6 +180,9 @@ impl Default for MobileState { omni_open: false, omni_grid_open: false, omni_create_open: false, + overflow_open: false, + palette_open: false, + palette_query: String::new(), drag: None, anim: None, } @@ -190,14 +200,18 @@ pub fn render_mobile_shell( ui.painter() .rect_filled(available_rect, 0.0, egui::Color32::from_rgb(0x14, 0x16, 0x1b)); + let topbar_rect = egui::Rect::from_min_max( + available_rect.min, + egui::pos2(available_rect.right(), available_rect.top() + TOPBAR_H), + ); let transport_rect = egui::Rect::from_min_max( egui::pos2(available_rect.left(), available_rect.bottom() - TRANSPORT_H), available_rect.max, ); - // Region above the transport, shared between the stack and (when something is selected) the - // inspector sheet that rises above the transport. + // Region between the top bar and the transport, shared between the stack and (when something is + // selected) the inspector sheet that rises above the transport. let region = egui::Rect::from_min_max( - available_rect.min, + egui::pos2(available_rect.left(), topbar_rect.bottom()), egui::pos2(available_rect.right(), transport_rect.top()), ); // When the inspector is up, the sheet overlays the lower part of the stack. If the selected @@ -234,4 +248,7 @@ pub fn render_mobile_shell( // Omnibutton FAB (radial tool menu) — drawn above the stack region, on top of everything else. omni::render(ui, region, rc, state); + + // Top bar (filename + ⌕ palette + ⋯ commands). Its menus overlay the whole shell, so it's last. + topbar::render(ui, topbar_rect, available_rect, rc, state); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs new file mode 100644 index 0000000..9f4ac72 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs @@ -0,0 +1,196 @@ +//! The mobile top bar: the project filename on the left, and on the right ⌕ (command palette) and +//! ⋯ (overflow commands). Both open a modal sheet whose items dispatch `MenuAction`s (which +//! `main.rs::handle_menu_action` already implements). Per the wireframe these are the +//! "commands/destinations", as opposed to the omnibutton's "tools/objects". + +use eframe::egui; + +use super::{icons, MobileState}; +use crate::menu::{MenuAction, MenuDef, MenuItemDef}; +use crate::RenderContext; + +const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); +const C_PANEL2: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); +const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); +const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); +const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); +const BTN: f32 = 40.0; + +/// Curated overflow (⋯) commands. +fn overflow_items() -> [(&'static str, MenuAction); 9] { + [ + ("Save", MenuAction::Save), + ("Save As…", MenuAction::SaveAs), + ("Open File…", MenuAction::OpenFile), + ("New file…", MenuAction::NewFile), + ("Import…", MenuAction::Import), + ("Export…", MenuAction::Export), + ("Undo", MenuAction::Undo), + ("Redo", MenuAction::Redo), + ("Preferences", MenuAction::Preferences), + ] +} + +/// Flatten the whole menu tree into (path-label, action) for the command palette. +fn flatten(defs: &'static [MenuDef], prefix: &str, out: &mut Vec<(String, MenuAction)>) { + for d in defs { + match d { + MenuDef::Item(item) => out.push((format!("{prefix}{}", item.label), item.action)), + MenuDef::Submenu { label, children } => { + flatten(children, &format!("{prefix}{label} › "), out); + } + MenuDef::Separator => {} + } + } +} + +fn all_commands() -> Vec<(String, MenuAction)> { + let mut v = Vec::new(); + flatten(MenuItemDef::menu_structure(), "", &mut v); + v +} + +pub fn render( + ui: &mut egui::Ui, + bar: egui::Rect, + full: egui::Rect, + rc: &mut RenderContext, + state: &mut MobileState, +) { + ui.painter().rect_filled(bar, 0.0, C_PANEL); + ui.painter().hline(bar.x_range(), bar.bottom(), egui::Stroke::new(1.0, C_LINE)); + + // Filename (or app name when unsaved). + let name = rc + .shared + .container_path + .as_ref() + .and_then(|p| p.file_name()) + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "Lightningbeam".to_string()); + ui.painter().text( + egui::pos2(bar.left() + 14.0, bar.center().y), + egui::Align2::LEFT_CENTER, + name, + egui::FontId::proportional(14.0), + C_BRIGHT, + ); + + // Right cluster: ⌕ (palette) then ⋯ (overflow). + let overflow_rect = egui::Rect::from_min_size(egui::pos2(bar.right() - BTN, bar.top()), egui::vec2(BTN, bar.height())); + let palette_rect = egui::Rect::from_min_size(egui::pos2(bar.right() - 2.0 * BTN, bar.top()), egui::vec2(BTN, bar.height())); + + let sresp = ui.interact(palette_rect, ui.id().with("mobile_topbar_search"), egui::Sense::click()); + ui.painter().text(palette_rect.center(), egui::Align2::CENTER_CENTER, icons::SEARCH, icons::font(17.0), + if sresp.hovered() || state.palette_open { C_BRIGHT } else { C_DIM }); + if sresp.clicked() { + state.palette_open = !state.palette_open; + state.overflow_open = false; + state.palette_query.clear(); + } + + let oresp = ui.interact(overflow_rect, ui.id().with("mobile_topbar_overflow"), egui::Sense::click()); + ui.painter().text(overflow_rect.center(), egui::Align2::CENTER_CENTER, icons::ELLIPSIS, icons::font(18.0), + if oresp.hovered() || state.overflow_open { C_BRIGHT } else { C_DIM }); + if oresp.clicked() { + state.overflow_open = !state.overflow_open; + state.palette_open = false; + } + + if state.overflow_open { + render_overflow(ui, full, rc, state); + } else if state.palette_open { + render_palette(ui, full, rc, state); + } +} + +/// Common modal scrim + panel. Returns (backdrop-tapped, panel inner rect). +fn open_panel(ui: &mut egui::Ui, full: egui::Rect, id: &str) -> (bool, egui::Rect) { + let scrim = ui.interact(full, ui.id().with(("mobile_topbar_scrim", id)), egui::Sense::click()); + ui.painter().rect_filled(full, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 170)); + let panel = egui::Rect::from_min_max( + egui::pos2(full.left() + 16.0, full.top() + 44.0), + egui::pos2(full.right() - 16.0, full.bottom() - 60.0), + ); + ui.painter().rect_filled(panel, 14.0, C_PANEL); + ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + (scrim.clicked(), panel) +} + +fn command_button(ui: &mut egui::Ui, r: egui::Rect, label: &str, key: (&str, usize)) -> bool { + let resp = ui.interact(r, ui.id().with(("mobile_cmd", key.0, key.1)), egui::Sense::click()); + ui.painter().rect_filled(r, 8.0, if resp.hovered() { C_PANEL2 } else { C_PANEL }); + ui.painter().hline(r.x_range(), r.bottom(), egui::Stroke::new(1.0, C_LINE)); + ui.painter().text( + egui::pos2(r.left() + 14.0, r.center().y), + egui::Align2::LEFT_CENTER, + label, + egui::FontId::proportional(13.0), + C_BRIGHT, + ); + resp.clicked() +} + +fn render_overflow(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { + let (backdrop, panel) = open_panel(ui, full, "overflow"); + let mut close = backdrop; + let items = overflow_items(); + let row_h = 44.0; + let inner = panel.shrink(8.0); + for (i, (label, action)) in items.iter().enumerate() { + let r = egui::Rect::from_min_size( + egui::pos2(inner.left(), inner.top() + i as f32 * row_h), + egui::vec2(inner.width(), row_h), + ); + if r.bottom() > inner.bottom() { + break; + } + if command_button(ui, r, label, ("of", i)) { + rc.shared.pending_menu_actions.push(*action); + close = true; + } + } + if close { + state.overflow_open = false; + } +} + +fn render_palette(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { + let (backdrop, panel) = open_panel(ui, full, "palette"); + let mut close = backdrop; + let inner = panel.shrink(8.0); + + // Search field (real egui widget). + let field = egui::Rect::from_min_size(inner.min, egui::vec2(inner.width(), 30.0)); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(field).layout(egui::Layout::left_to_right(egui::Align::Center))); + let te = child.add( + egui::TextEdit::singleline(&mut state.palette_query) + .hint_text("Search commands…") + .desired_width(inner.width()), + ); + te.request_focus(); + + // Filtered list. + let q = state.palette_query.to_lowercase(); + let cmds = all_commands(); + let row_h = 38.0; + let list_top = inner.top() + 38.0; + let mut y = list_top; + for (i, (label, action)) in cmds.iter().enumerate() { + if !q.is_empty() && !label.to_lowercase().contains(&q) { + continue; + } + let r = egui::Rect::from_min_size(egui::pos2(inner.left(), y), egui::vec2(inner.width(), row_h)); + if r.bottom() > inner.bottom() { + break; + } + if command_button(ui, r, label, ("pal", i)) { + rc.shared.pending_menu_actions.push(*action); + close = true; + } + y += row_h; + } + if close { + state.palette_open = false; + } +} From f13a127c9da741673a5f197e5761bf25078a4056 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 06:27:59 -0400 Subject: [PATCH 17/43] Fit modal dialogs to the phone screen on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mobile::dialog_width(ctx, desired) — a min()-clamp that's a no-op on wide desktop screens. Clamp the Export and Export Progress modals and the Sample Import window to it. Render Preferences as an egui::Modal (dim backdrop, centered, no title bar) with a screen-fit scroll height when mobile_active(); desktop keeps its window. Extracted the preferences body into render_body() shared by both paths. --- .../lightningbeam-editor/src/export/dialog.rs | 4 +- .../lightningbeam-editor/src/main.rs | 3 +- .../lightningbeam-editor/src/mobile/mod.rs | 7 + .../src/preferences/dialog.rs | 149 +++++++++++------- .../src/sample_import_dialog.rs | 3 +- 5 files changed, 102 insertions(+), 64 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs index aa5dddb..7438836 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs @@ -208,7 +208,7 @@ impl ExportDialog { let modal_response = egui::Modal::new(egui::Id::new("export_dialog_modal")) .show(ctx, |ui| { - ui.set_width(500.0); + ui.set_width(crate::mobile::dialog_width(ctx, 500.0)); ui.heading(window_title); ui.add_space(8.0); @@ -801,7 +801,7 @@ impl ExportProgressDialog { egui::Modal::new(egui::Id::new("export_progress_modal")) .show(ctx, |ui| { - ui.set_width(400.0); + ui.set_width(crate::mobile::dialog_width(ctx, 400.0)); ui.heading("Exporting..."); ui.add_space(8.0); diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index f00990a..ec0f956 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6329,7 +6329,8 @@ impl eframe::App for EditorApp { } // Render preferences dialog - if let Some(result) = self.preferences_dialog.render(ctx, &mut self.config, &mut self.theme) { + let mobile = self.mobile_active(); + if let Some(result) = self.preferences_dialog.render(ctx, &mut self.config, &mut self.theme, mobile) { if result.buffer_size_changed { println!("⚠️ Audio buffer size will be applied on next app restart"); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 00d0806..9c722ca 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -34,6 +34,13 @@ pub const MOBILE_NS: usize = usize::MAX; const TRANSPORT_H: f32 = 60.0; const TOPBAR_H: f32 = 40.0; +/// Clamp a desktop dialog width to fit the current screen (with side margins). A no-op on wide +/// desktop screens (`min` keeps the desired width); on a phone-aspect window it shrinks to fit. +pub fn dialog_width(ctx: &egui::Context, desired: f32) -> f32 { + let avail = ctx.screen_rect().width() - 24.0; + desired.min(avail.max(200.0)) +} + /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Any non-empty value other than "0" enables it. pub fn is_mobile_env() -> bool { diff --git a/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs index e0f0989..16e379d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs @@ -151,6 +151,7 @@ impl PreferencesDialog { ctx: &egui::Context, config: &mut AppConfig, theme: &mut Theme, + mobile: bool, ) -> Option { if !self.open { return None; @@ -160,68 +161,32 @@ impl PreferencesDialog { let mut should_cancel = false; let mut open = self.open; - egui::Window::new("Preferences") - .open(&mut open) - .resizable(false) - .collapsible(false) - .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) - .show(ctx, |ui| { - ui.set_width(550.0); + // On mobile, render as a screen-fitting modal sheet (dim backdrop, centered) like the other + // mobile modals; on desktop, the familiar draggable window. + let width = crate::mobile::dialog_width(ctx, 550.0); + let scroll_h = if mobile { + (ctx.screen_rect().height() - 220.0).clamp(160.0, 400.0) + } else { + 400.0 + }; - // Error message - if let Some(error) = &self.error_message { - ui.colored_label(egui::Color32::from_rgb(255, 100, 100), error); - ui.add_space(8.0); - } - - // Tab bar - ui.horizontal(|ui| { - ui.selectable_value(&mut self.tab, PreferencesTab::General, "General"); - ui.selectable_value(&mut self.tab, PreferencesTab::Shortcuts, "Keyboard Shortcuts"); - }); - ui.separator(); - - // Tab content - match self.tab { - PreferencesTab::General => { - egui::ScrollArea::vertical() - .max_height(400.0) - .show(ui, |ui| { - self.render_general_section(ui); - ui.add_space(8.0); - self.render_audio_section(ui); - ui.add_space(8.0); - self.render_appearance_section(ui); - ui.add_space(8.0); - self.render_startup_section(ui); - ui.add_space(8.0); - self.render_advanced_section(ui); - }); - } - PreferencesTab::Shortcuts => { - self.render_shortcuts_tab(ui); - } - } - - ui.add_space(16.0); - - // Buttons - ui.horizontal(|ui| { - if ui.button("Cancel").clicked() { - should_cancel = true; - } - - if ui.button("Reset to Defaults").clicked() { - self.reset_to_defaults(); - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("Save").clicked() { - should_save = true; - } - }); - }); + if mobile { + let resp = egui::Modal::new(egui::Id::new("preferences_modal")).show(ctx, |ui| { + self.render_body(ui, width, scroll_h, &mut should_save, &mut should_cancel); }); + if resp.backdrop_response.clicked() { + should_cancel = true; + } + } else { + egui::Window::new("Preferences") + .open(&mut open) + .resizable(false) + .collapsible(false) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .show(ctx, |ui| { + self.render_body(ui, width, scroll_h, &mut should_save, &mut should_cancel); + }); + } // Update open state self.open = open; @@ -238,6 +203,70 @@ impl PreferencesDialog { None } + #[allow(clippy::too_many_arguments)] + fn render_body( + &mut self, + ui: &mut egui::Ui, + width: f32, + scroll_h: f32, + should_save: &mut bool, + should_cancel: &mut bool, + ) { + ui.set_width(width); + + // Error message + if let Some(error) = &self.error_message { + ui.colored_label(egui::Color32::from_rgb(255, 100, 100), error); + ui.add_space(8.0); + } + + // Tab bar + ui.horizontal(|ui| { + ui.selectable_value(&mut self.tab, PreferencesTab::General, "General"); + ui.selectable_value(&mut self.tab, PreferencesTab::Shortcuts, "Keyboard Shortcuts"); + }); + ui.separator(); + + // Tab content + match self.tab { + PreferencesTab::General => { + egui::ScrollArea::vertical().max_height(scroll_h).show(ui, |ui| { + self.render_general_section(ui); + ui.add_space(8.0); + self.render_audio_section(ui); + ui.add_space(8.0); + self.render_appearance_section(ui); + ui.add_space(8.0); + self.render_startup_section(ui); + ui.add_space(8.0); + self.render_advanced_section(ui); + }); + } + PreferencesTab::Shortcuts => { + self.render_shortcuts_tab(ui); + } + } + + ui.add_space(16.0); + + // Buttons + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + *should_cancel = true; + } + + if ui.button("Reset to Defaults").clicked() { + self.reset_to_defaults(); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Save").clicked() { + *should_save = true; + } + }); + }); + } + fn render_shortcuts_tab(&mut self, ui: &mut egui::Ui) { // Capture key events for rebinding BEFORE rendering the rest if let Some(rebind_action) = self.rebinding { diff --git a/lightningbeam-ui/lightningbeam-editor/src/sample_import_dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/sample_import_dialog.rs index 42b7150..10b5aac 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/sample_import_dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/sample_import_dialog.rs @@ -58,7 +58,8 @@ impl SampleImportDialog { .resizable(true) .collapsible(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) - .default_width(700.0) + .default_width(crate::mobile::dialog_width(ctx, 700.0)) + .max_width(crate::mobile::dialog_width(ctx, 700.0)) .default_height(500.0) .show(ctx, |ui| { // Folder info From 943ff7c15f66022715cd0a2c816db43e488b1bb0 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 07:03:07 -0400 Subject: [PATCH 18/43] Unify colors via theme CSS variables; theme egui visuals Add Theme::var(name, ctx) (CSS custom-property getter) and Theme::apply_to_egui(ctx) which maps the palette onto egui's global Visuals so standard widgets share the theme colors. Add a mobile::palette::Palette built from the theme each frame and replace the duplicated per-file C_* color constants across all mobile modules (omni, topbar, stack, inspector, transport, intent, shell) with it. Add --scrim and --accent-* category vars to styles.css. Apply the theme visuals every frame in update(). Mobile and the main UI now share one CSS-variable-driven palette that responds to light/dark/user themes. --- .../lightningbeam-editor/assets/styles.css | 8 +++ .../lightningbeam-editor/src/main.rs | 4 ++ .../src/mobile/inspector.rs | 27 +++----- .../lightningbeam-editor/src/mobile/intent.rs | 44 +++++------- .../lightningbeam-editor/src/mobile/mod.rs | 20 +++--- .../lightningbeam-editor/src/mobile/omni.rs | 68 ++++++++----------- .../src/mobile/palette.rs | 59 ++++++++++++++++ .../lightningbeam-editor/src/mobile/stack.rs | 36 ++++------ .../lightningbeam-editor/src/mobile/topbar.rs | 50 +++++++------- .../src/mobile/transport.rs | 26 +++---- .../lightningbeam-editor/src/theme.rs | 67 ++++++++++++++++++ 11 files changed, 255 insertions(+), 154 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/mobile/palette.rs diff --git a/lightningbeam-ui/lightningbeam-editor/assets/styles.css b/lightningbeam-ui/lightningbeam-editor/assets/styles.css index 7599b9b..f947592 100644 --- a/lightningbeam-ui/lightningbeam-editor/assets/styles.css +++ b/lightningbeam-ui/lightningbeam-editor/assets/styles.css @@ -99,6 +99,14 @@ --font-size-small: 11px; --font-size-default: 13px; --pane-border-width: 1px; + + /* Modal backdrop (translucent) + intent/category accents (shared by light & dark) */ + --scrim: #10141ab0; + --accent-coral: #e8826b; + --accent-cyan: #54c3e8; + --accent-amber: #f4a340; + --accent-pink: #c75b8a; + --accent-violet: #8a6ec0; } /* ============================================ diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index ec0f956..63e0c6a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -5343,6 +5343,10 @@ impl eframe::App for EditorApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { let _frame_start = std::time::Instant::now(); + // Apply the theme's palette to egui's global visuals so the standard widgets (dialogs, pane + // chrome, text) share the same colors as the mobile UI and respond to light/dark/user themes. + self.theme.apply_to_egui(ctx); + // === Raster fault-in (Phase 3 paging) === // The canvas records raster keyframe ids whose `raw_pixels` weren't resident // (it can't mutate the document while rendering). Drain that sink here, BEFORE diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index 2cbeb4f..e75ab69 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -6,16 +6,10 @@ use eframe::egui; use lightningbeam_core::pane::PaneType; use lightningbeam_core::selection::FocusSelection; -use super::{surface, MobileState, MOBILE_NS}; +use super::{surface, MobileState, Palette, MOBILE_NS}; use crate::panes::{NodePath, SharedPaneState}; use crate::RenderContext; -const C_SHEET: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_CYAN: egui::Color32 = egui::Color32::from_rgb(0x54, 0xc3, 0xe8); - const GRAB_H: f32 = 16.0; const HEAD_H: f32 = 30.0; const CHIP_H: f32 = 30.0; @@ -104,14 +98,15 @@ pub fn render( region_h: f32, rc: &mut RenderContext, state: &mut MobileState, + pal: &Palette, ) { // Sheet background with rounded top. ui.painter().rect_filled( rect, egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }, - C_SHEET, + pal.surface_alt, ); - ui.painter().hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, C_LINE)); + ui.painter().hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, pal.line)); // Grab handle — drag to resize the sheet. let grab = egui::Rect::from_min_max( @@ -120,7 +115,7 @@ pub fn render( ); let gresp = ui.interact(grab, ui.id().with("mobile_inspector_grab"), egui::Sense::drag()); let pill = egui::Rect::from_center_size(grab.center(), egui::vec2(34.0, 4.0)); - ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { C_CYAN } else { C_LINE }); + ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { pal.accent } else { pal.line }); if gresp.dragged() && region_h > 1.0 { state.inspector_frac = (state.inspector_frac - gresp.drag_delta().y / region_h).clamp(0.2, 0.85); } @@ -139,7 +134,7 @@ pub fn render( egui::Align2::LEFT_CENTER, title(&rc.shared), egui::FontId::proportional(13.0), - C_BRIGHT, + pal.text, ); let close = egui::Rect::from_min_size(egui::pos2(head.right() - 36.0, head.top()), egui::vec2(36.0, HEAD_H)); let cresp = ui.interact(close, ui.id().with("mobile_inspector_close"), egui::Sense::click()); @@ -148,7 +143,7 @@ pub fn render( egui::Align2::CENTER_CENTER, super::icons::X, super::icons::font(16.0), - if cresp.hovered() { C_BRIGHT } else { C_DIM }, + if cresp.hovered() { pal.text } else { pal.text_dim }, ); if cresp.clicked() { rc.shared.selection.clear(); @@ -162,14 +157,14 @@ pub fn render( let galley = ui.painter().layout_no_wrap( chip.label.to_string(), egui::FontId::proportional(11.0), - C_BRIGHT, + pal.text, ); let w = galley.size().x + 18.0; let chip_rect = egui::Rect::from_min_size(egui::pos2(cx, chip_y + 4.0), egui::vec2(w, CHIP_H - 8.0)); let resp = ui.interact(chip_rect, ui.id().with(("mobile_inspector_chip", i)), egui::Sense::click()); - ui.painter().rect_filled(chip_rect, 11.0, if resp.hovered() { C_LINE } else { C_SHEET }); - ui.painter().rect_stroke(chip_rect, 11.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); - ui.painter().galley(chip_rect.center() - galley.size() * 0.5, galley, C_BRIGHT); + ui.painter().rect_filled(chip_rect, 11.0, if resp.hovered() { pal.line } else { pal.surface_alt }); + ui.painter().rect_stroke(chip_rect, 11.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); + ui.painter().galley(chip_rect.center() - galley.size() * 0.5, galley, pal.text); if resp.clicked() { let (top, count) = chip.window; state.window_top = top; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs index 664a69c..911dd07 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs @@ -7,16 +7,9 @@ use eframe::egui; -use super::icons; +use super::{icons, Palette}; use crate::EditorApp; -const C_DEVICE: egui::Color32 = egui::Color32::from_rgb(0x14, 0x16, 0x1b); -const C_CARD: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_CARD_HOT: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); - /// One intent card. struct Intent { label: &'static str, @@ -28,12 +21,8 @@ struct Intent { window: (usize, usize), } -fn intents() -> [Intent; 6] { - let coral = egui::Color32::from_rgb(0xe8, 0x82, 0x6b); - let cyan = egui::Color32::from_rgb(0x54, 0xc3, 0xe8); - let amber = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); - let pink = egui::Color32::from_rgb(0xc7, 0x5b, 0x8a); - let violet = egui::Color32::from_rgb(0x6a, 0x46, 0x90); +fn intents(pal: &Palette) -> [Intent; 6] { + let [coral, cyan, amber, pink, violet] = pal.accents; [ // Stage indices (see super::STACK): Stage=2, Timeline=3, PianoRoll=4, VirtualPiano=5. Intent { label: "Draw", icon: icons::BRUSH, accent: coral, focus: 5, window: (2, 1) }, @@ -41,13 +30,14 @@ fn intents() -> [Intent; 6] { Intent { label: "Compose", icon: icons::MUSIC, accent: amber, focus: 2, window: (3, 3) }, Intent { label: "Record", icon: icons::MIC, accent: pink, focus: 2, window: (3, 3) }, Intent { label: "Edit video", icon: icons::CLAPPERBOARD, accent: violet, focus: 1, window: (2, 2) }, - Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: C_DIM, focus: 0, window: (2, 2) }, + Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: pal.text_dim, focus: 0, window: (2, 2) }, ] } pub fn render(app: &mut EditorApp, ctx: &egui::Context) { + let pal = Palette::from_theme(&app.theme, ctx); egui::CentralPanel::default() - .frame(egui::Frame::NONE.fill(C_DEVICE)) + .frame(egui::Frame::NONE.fill(pal.bg)) .show(ctx, |ui| { let rect = ui.available_rect_before_wrap(); let margin = 16.0; @@ -60,7 +50,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { egui::Align2::LEFT_TOP, "Start something", egui::FontId::proportional(22.0), - C_BRIGHT, + pal.text, ); // Vertical budget: ~2/3 for the intent grid, ~1/3 for the recent list. @@ -72,7 +62,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { // 2×3 grid of intent cards filling the grid budget. let col_w = (right - left - gap) / 2.0; let card_h = (grid_h - 2.0 * gap) / 3.0; - for (i, intent) in intents().iter().enumerate() { + for (i, intent) in intents(&pal).iter().enumerate() { let col = (i % 2) as f32; let row = (i / 2) as f32; let cx = left + col * (col_w + gap); @@ -81,8 +71,8 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { let resp = ui.interact(card, ui.id().with(("mobile_intent", i)), egui::Sense::click()); let p = ui.painter(); - p.rect_filled(card, 12.0, if resp.hovered() { C_CARD_HOT } else { C_CARD }); - p.rect_stroke(card, 12.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + p.rect_filled(card, 12.0, if resp.hovered() { pal.surface_alt } else { pal.surface }); + p.rect_stroke(card, 12.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); p.text( egui::pos2(card.center().x, card.top() + card.height() * 0.40), egui::Align2::CENTER_CENTER, @@ -95,7 +85,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { egui::Align2::CENTER_CENTER, intent.label, egui::FontId::proportional(15.0), - C_BRIGHT, + pal.text, ); if resp.clicked() { @@ -113,7 +103,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { egui::Align2::LEFT_TOP, "Recent", egui::FontId::proportional(13.0), - C_DIM, + pal.text_dim, ); let list_top = recent_top + 22.0; let recents = app.config.get_recent_files(); @@ -123,7 +113,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { egui::Align2::LEFT_TOP, "No recent projects", egui::FontId::proportional(12.0), - C_DIM, + pal.text_dim, ); } else { let row_h = 38.0; @@ -138,14 +128,14 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { let resp = ui.interact(row_rect, ui.id().with(("mobile_recent", j)), egui::Sense::click()); let p = ui.painter(); - p.rect_filled(row_rect, 8.0, if resp.hovered() { C_CARD_HOT } else { C_CARD }); - p.rect_stroke(row_rect, 8.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + p.rect_filled(row_rect, 8.0, if resp.hovered() { pal.surface_alt } else { pal.surface }); + p.rect_stroke(row_rect, 8.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); p.text( egui::pos2(row_rect.left() + 12.0, row_rect.center().y), egui::Align2::LEFT_CENTER, icons::FOLDER_OPEN, icons::font(15.0), - C_DIM, + pal.text_dim, ); let name = path .file_name() @@ -156,7 +146,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { egui::Align2::LEFT_CENTER, &name, egui::FontId::proportional(13.0), - C_BRIGHT, + pal.text, ); if resp.clicked() { chosen = Some(path.clone()); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 9c722ca..0baacd3 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -21,11 +21,14 @@ pub mod icons; mod inspector; pub mod intent; mod omni; +mod palette; mod stack; mod surface; mod topbar; mod transport; +use palette::Palette; + /// Reserved sentinel namespace for mobile pane-instance paths. Desktop layout paths are built /// from small child indices, so prefixing with `usize::MAX` guarantees mobile slots never alias a /// real layout path in the shared `pane_instances` map. @@ -203,9 +206,10 @@ pub fn render_mobile_shell( rc: &mut RenderContext, state: &mut MobileState, ) { - // Background (wireframe device color; bands paint over most of it). - ui.painter() - .rect_filled(available_rect, 0.0, egui::Color32::from_rgb(0x14, 0x16, 0x1b)); + let pal = Palette::from_theme(rc.shared.theme, ui.ctx()); + + // Background (device color; bands paint over most of it). + ui.painter().rect_filled(available_rect, 0.0, pal.bg); let topbar_rect = egui::Rect::from_min_max( available_rect.min, @@ -243,19 +247,19 @@ pub fn render_mobile_shell( region }; - stack::render(ui, stack_rect, rc, state); + stack::render(ui, stack_rect, rc, state, &pal); if inspector_shown { let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); - inspector::render(ui, sheet_rect, region.height(), rc, state); + inspector::render(ui, sheet_rect, region.height(), rc, state, &pal); } // Transport floor: drawn last = always on top, the persistent spine. - transport::render(ui, transport_rect, &mut rc.shared); + transport::render(ui, transport_rect, &mut rc.shared, &pal); // Omnibutton FAB (radial tool menu) — drawn above the stack region, on top of everything else. - omni::render(ui, region, rc, state); + omni::render(ui, region, rc, state, &pal); // Top bar (filename + ⌕ palette + ⋯ commands). Its menus overlay the whole shell, so it's last. - topbar::render(ui, topbar_rect, available_rect, rc, state); + topbar::render(ui, topbar_rect, available_rect, rc, state, &pal); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs index 3d80d29..7158071 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/omni.rs @@ -9,18 +9,10 @@ use lightningbeam_core::layer::{AnyLayer, LayerType}; use lightningbeam_core::selection::FocusSelection; use lightningbeam_core::tool::Tool; -use super::{icons, MobileState}; +use super::{icons, MobileState, Palette}; use crate::menu::MenuAction; use crate::RenderContext; -const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); -const C_DARK: egui::Color32 = egui::Color32::from_rgb(0x1b, 0x13, 0x0a); -const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_PANEL2: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); - const FAB_R: f32 = 26.0; const IR: f32 = 20.0; const PRIMARY: [Tool; 14] = [ @@ -106,7 +98,7 @@ fn close_all(state: &mut MobileState) { state.omni_create_open = false; } -pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { +pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette) { let ctx_tools: Vec = Tool::for_layer_type(active_layer_type(rc)).to_vec(); let primary: Vec = PRIMARY .iter() @@ -140,7 +132,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta enabled: true, }) .collect(); - let (close, clicked) = draw_grid(ui, region, "Tools", &cells); + let (close, clicked) = draw_grid(ui, region, "Tools", &cells, pal); if let Some(i) = clicked { *rc.shared.selected_tool = ctx_tools[i]; close_all(state); @@ -159,7 +151,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta enabled: it.3, }) .collect(); - let (close, clicked) = draw_grid(ui, region, "New", &cells); + let (close, clicked) = draw_grid(ui, region, "New", &cells, pal); if let Some(i) = clicked { rc.shared.pending_menu_actions.push(items[i].2); close_all(state); @@ -169,8 +161,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta } else if state.omni_open { // Radial: scrim, then tool petals + special petals (more / new). let scrim = ui.interact(region, ui.id().with("mobile_omni_scrim"), egui::Sense::click()); - ui.painter() - .rect_filled(region, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 140)); + ui.painter().rect_filled(region, 0.0, pal.scrim); if scrim.clicked() { state.omni_open = false; } @@ -194,7 +185,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta let t = primary[k]; let resp = ui.interact(rect, ui.id().with(("mobile_omni_tool", k)), egui::Sense::click()); let selected = *rc.shared.selected_tool == t; - petal(ui, c, if selected { C_AMBER } else { petal_bg(&resp) }, tool_icon(t), if selected { C_DARK } else { C_BRIGHT }); + petal(ui, c, if selected { pal.accent } else { petal_bg(&resp, pal) }, tool_icon(t), if selected { pal.on_accent } else { pal.text }, pal); if resp.clicked() { *rc.shared.selected_tool = t; state.omni_open = false; @@ -203,16 +194,16 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta match specials[k - primary.len()] { Special::More => { let resp = ui.interact(rect, ui.id().with("mobile_omni_more"), egui::Sense::click()); - petal(ui, c, petal_bg(&resp), icons::MENU, C_BRIGHT); + petal(ui, c, petal_bg(&resp, pal), icons::MENU, pal.text, pal); if resp.clicked() { state.omni_grid_open = true; } } Special::New => { let resp = ui.interact(rect, ui.id().with("mobile_omni_new"), egui::Sense::click()); - petal(ui, c, C_AMBER, icons::PLUS, C_DARK); + petal(ui, c, pal.accent, icons::PLUS, pal.on_accent, pal); if resp.hovered() { - ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.5, C_BRIGHT)); + ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.5, pal.text)); } if resp.clicked() { state.omni_create_open = true; @@ -226,7 +217,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta // The FAB itself (on top). Closed → current tool icon; open → ✕. let fab_rect = egui::Rect::from_center_size(fab_center, egui::vec2(FAB_R * 2.0, FAB_R * 2.0)); let fresp = ui.interact(fab_rect, ui.id().with("mobile_omni_fab"), egui::Sense::click()); - ui.painter().circle_filled(fab_center, FAB_R, C_AMBER); + ui.painter().circle_filled(fab_center, FAB_R, pal.accent); let open = state.omni_open || state.omni_grid_open || state.omni_create_open; let glyph = if open { icons::X @@ -236,7 +227,7 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta icons::PLUS // off-Stage: a create button }; ui.painter() - .text(fab_center, egui::Align2::CENTER_CENTER, glyph, icons::font(22.0), C_DARK); + .text(fab_center, egui::Align2::CENTER_CENTER, glyph, icons::font(22.0), pal.on_accent); if fresp.clicked() { if open { close_all(state); @@ -248,17 +239,17 @@ pub fn render(ui: &mut egui::Ui, region: egui::Rect, rc: &mut RenderContext, sta } } -fn petal_bg(resp: &egui::Response) -> egui::Color32 { +fn petal_bg(resp: &egui::Response, pal: &Palette) -> egui::Color32 { if resp.hovered() { - C_LINE + pal.line } else { - C_PANEL2 + pal.surface_alt } } -fn petal(ui: &egui::Ui, c: egui::Pos2, bg: egui::Color32, glyph: &str, fg: egui::Color32) { +fn petal(ui: &egui::Ui, c: egui::Pos2, bg: egui::Color32, glyph: &str, fg: egui::Color32, pal: &Palette) { ui.painter().circle_filled(c, IR, bg); - ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.0, C_LINE)); + ui.painter().circle_stroke(c, IR, egui::Stroke::new(1.0, pal.line)); ui.painter().text(c, egui::Align2::CENTER_CENTER, glyph, icons::font(18.0), fg); } @@ -270,20 +261,19 @@ struct Cell { } /// A modal grid sheet of icon+label cells. Returns (backdrop-tapped, Some(clicked-enabled-index)). -fn draw_grid(ui: &mut egui::Ui, region: egui::Rect, title: &str, cells: &[Cell]) -> (bool, Option) { +fn draw_grid(ui: &mut egui::Ui, region: egui::Rect, title: &str, cells: &[Cell], pal: &Palette) -> (bool, Option) { let scrim = ui.interact(region, ui.id().with(("mobile_omni_gridscrim", title)), egui::Sense::click()); - ui.painter() - .rect_filled(region, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 170)); + ui.painter().rect_filled(region, 0.0, pal.scrim); let panel = region.shrink2(egui::vec2(16.0, 40.0)); - ui.painter().rect_filled(panel, 14.0, C_PANEL); - ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + ui.painter().rect_filled(panel, 14.0, pal.surface); + ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); ui.painter().text( egui::pos2(panel.left() + 16.0, panel.top() + 18.0), egui::Align2::LEFT_CENTER, title, egui::FontId::proportional(14.0), - C_BRIGHT, + pal.text, ); let cols = 4usize; @@ -310,20 +300,20 @@ fn draw_grid(ui: &mut egui::Ui, region: egui::Rect, title: &str, cells: &[Cell]) ui.interact(r, ui.id().with(("mobile_omni_cell_off", title, i)), egui::Sense::hover()) }; let bg = if cell.selected { - C_AMBER + pal.accent } else if cell.enabled && resp.hovered() { - C_PANEL2 + pal.surface_alt } else { - C_PANEL + pal.surface }; ui.painter().rect_filled(r, 10.0, bg); - ui.painter().rect_stroke(r, 10.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + ui.painter().rect_stroke(r, 10.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); let fg = if cell.selected { - C_DARK + pal.on_accent } else if cell.enabled { - C_BRIGHT + pal.text } else { - C_LINE + pal.line }; ui.painter().text( egui::pos2(r.center().x, r.top() + r.height() * 0.38), @@ -337,7 +327,7 @@ fn draw_grid(ui: &mut egui::Ui, region: egui::Rect, title: &str, cells: &[Cell]) egui::Align2::CENTER_CENTER, cell.label, egui::FontId::proportional(9.5), - if cell.selected { C_DARK } else if cell.enabled { C_DIM } else { C_LINE }, + if cell.selected { pal.on_accent } else if cell.enabled { pal.text_dim } else { pal.line }, ); if cell.enabled && resp.clicked() { clicked = Some(i); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/palette.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/palette.rs new file mode 100644 index 0000000..cb479be --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/palette.rs @@ -0,0 +1,59 @@ +//! The mobile UI's semantic color palette, sourced from the shared theme CSS variables so it +//! matches the main UI and responds to light/dark/user themes. Built once per frame from the active +//! [`Theme`](crate::theme::Theme) and passed (by `Copy`) into the mobile render functions, replacing +//! the per-file hardcoded color constants. + +use eframe::egui::{Color32, Context}; + +use crate::theme::Theme; + +#[derive(Clone, Copy)] +pub struct Palette { + /// App/device background behind everything. + pub bg: Color32, + /// Panels, cards, bands, sheets. + pub surface: Color32, + /// Raised / hovered surface. + pub surface_alt: Color32, + /// Band headers, the top bar. + pub header: Color32, + /// Borders and dividers. + pub line: Color32, + /// Primary text / icons. + pub text: Color32, + /// Secondary (dim) text / icons. + pub text_dim: Color32, + /// Selection / active accent. + pub accent: Color32, + /// Text / icons drawn on top of `accent`. + pub on_accent: Color32, + /// Modal backdrop (translucent). + pub scrim: Color32, + /// Decorative category accents (intent picker, chips): coral, cyan, amber, pink, violet. + pub accents: [Color32; 5], +} + +impl Palette { + pub fn from_theme(theme: &Theme, ctx: &Context) -> Self { + let c = |name: &str, fb: Color32| theme.var(name, ctx).unwrap_or(fb); + Self { + bg: c("bg-app", Color32::from_rgb(0x2a, 0x2a, 0x2a)), + surface: c("bg-panel", Color32::from_rgb(0x22, 0x22, 0x22)), + surface_alt: c("bg-surface-raised", Color32::from_rgb(0x3f, 0x3f, 0x3f)), + header: c("bg-header", Color32::from_rgb(0x35, 0x35, 0x35)), + line: c("border-default", Color32::from_rgb(0x44, 0x44, 0x44)), + text: c("text-primary", Color32::from_rgb(0xf6, 0xf6, 0xf6)), + text_dim: c("text-secondary", Color32::from_rgb(0xaa, 0xaa, 0xaa)), + accent: c("accent", Color32::from_rgb(0x39, 0x6c, 0xd8)), + on_accent: c("text-on-accent", Color32::WHITE), + scrim: c("scrim", Color32::from_rgba_unmultiplied(0x10, 0x14, 0x1a, 0xb0)), + accents: [ + c("accent-coral", Color32::from_rgb(0xe8, 0x82, 0x6b)), + c("accent-cyan", Color32::from_rgb(0x54, 0xc3, 0xe8)), + c("accent-amber", Color32::from_rgb(0xf4, 0xa3, 0x40)), + c("accent-pink", Color32::from_rgb(0xc7, 0x5b, 0x8a)), + c("accent-violet", Color32::from_rgb(0x8a, 0x6e, 0xc0)), + ], + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 8debad8..5921527 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -18,7 +18,7 @@ use eframe::egui; -use super::{icons, slot_path, LayoutAnim, MobileState, StackDrag, StackPane, STACK}; +use super::{icons, slot_path, LayoutAnim, MobileState, Palette, StackDrag, StackPane, STACK}; use crate::RenderContext; const N: usize = STACK.len(); @@ -34,12 +34,6 @@ const BTN_W: f32 = 44.0; /// Max pixels of drag to complete a window transition (so you don't drag half the screen). const TRIGGER_MAX: f32 = 150.0; -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x7c, 0x86, 0x93); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_HEADER: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); - /// A draggable boundary of the stack. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Handle { @@ -345,7 +339,7 @@ fn interp_layout( /// The interaction (drag) target for a band's boundary is its full-width header bar. Bands are /// laid out in the area above a reserved bottom footer (the BottomEdge handle). Band 0's header is /// the TopEdge handle; band i's header (i≥1) is the divider above it; the footer is the BottomEdge. -pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { +pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette) { let top = state.window_top; let count = state.window_count; @@ -437,24 +431,24 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state brect.left_top(), egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), ); - draw_header(ui, hr, STACK[*slot], state.show_instruments, fullscreen); + draw_header(ui, hr, STACK[*slot], state.show_instruments, fullscreen, pal); } - draw_footer(ui, footer_rect, top + count >= N); + draw_footer(ui, footer_rect, top + count >= N, pal); // 3) Interactions on the resting header/footer rects (added last → they win the press). handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, now, state); } -fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool) { +fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool, pal: &Palette) { let p = ui.painter(); // Rounded top corners so the header reads as a tab atop the pane; square at the bottom where // it meets the pane content. p.rect_filled( hr, egui::CornerRadius { nw: HEADER_RADIUS, ne: HEADER_RADIUS, sw: 0, se: 0 }, - C_HEADER, + pal.header, ); - p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); + p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, pal.line)); let cy = hr.center().y; // Grip glyph on the left (Lucide). p.text( @@ -462,14 +456,14 @@ fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: b egui::Align2::CENTER_CENTER, icons::GRIP_HORIZONTAL, icons::font(16.0), - C_DIM, + pal.text_dim, ); p.text( egui::pos2(hr.left() + 32.0, cy), egui::Align2::LEFT_CENTER, sp.label(show_instruments), egui::FontId::proportional(15.0), - C_BRIGHT, + pal.text, ); // Right-side buttons: fullscreen / restore rightmost, Node/Instrument toggle just left of it. p.text( @@ -477,7 +471,7 @@ fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: b egui::Align2::CENTER_CENTER, if fullscreen { icons::MINIMIZE } else { icons::MAXIMIZE }, icons::font(17.0), - C_DIM, + pal.text_dim, ); if sp == StackPane::NodeInstrument { p.text( @@ -485,16 +479,16 @@ fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: b egui::Align2::CENTER_CENTER, icons::ARROW_LEFT_RIGHT, icons::font(17.0), - C_AMBER, + pal.accent, ); } } -fn draw_footer(ui: &egui::Ui, fr: egui::Rect, at_end: bool) { +fn draw_footer(ui: &egui::Ui, fr: egui::Rect, at_end: bool, pal: &Palette) { let p = ui.painter(); - p.rect_filled(fr, 0.0, C_HEADER); - p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, C_LINE)); - let col = if at_end { C_LINE } else { C_DIM }; + p.rect_filled(fr, 0.0, pal.header); + p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, pal.line)); + let col = if at_end { pal.line } else { pal.text_dim }; let cy = fr.center().y; let galley = p.layout_no_wrap( "pull up for more".to_string(), diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs index 9f4ac72..982382e 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs @@ -5,15 +5,10 @@ use eframe::egui; -use super::{icons, MobileState}; +use super::{icons, MobileState, Palette}; use crate::menu::{MenuAction, MenuDef, MenuItemDef}; use crate::RenderContext; -const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_PANEL2: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1); const BTN: f32 = 40.0; /// Curated overflow (⋯) commands. @@ -56,9 +51,10 @@ pub fn render( full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, + pal: &Palette, ) { - ui.painter().rect_filled(bar, 0.0, C_PANEL); - ui.painter().hline(bar.x_range(), bar.bottom(), egui::Stroke::new(1.0, C_LINE)); + ui.painter().rect_filled(bar, 0.0, pal.header); + ui.painter().hline(bar.x_range(), bar.bottom(), egui::Stroke::new(1.0, pal.line)); // Filename (or app name when unsaved). let name = rc @@ -73,7 +69,7 @@ pub fn render( egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(14.0), - C_BRIGHT, + pal.text, ); // Right cluster: ⌕ (palette) then ⋯ (overflow). @@ -82,7 +78,7 @@ pub fn render( let sresp = ui.interact(palette_rect, ui.id().with("mobile_topbar_search"), egui::Sense::click()); ui.painter().text(palette_rect.center(), egui::Align2::CENTER_CENTER, icons::SEARCH, icons::font(17.0), - if sresp.hovered() || state.palette_open { C_BRIGHT } else { C_DIM }); + if sresp.hovered() || state.palette_open { pal.text } else { pal.text_dim }); if sresp.clicked() { state.palette_open = !state.palette_open; state.overflow_open = false; @@ -91,48 +87,48 @@ pub fn render( let oresp = ui.interact(overflow_rect, ui.id().with("mobile_topbar_overflow"), egui::Sense::click()); ui.painter().text(overflow_rect.center(), egui::Align2::CENTER_CENTER, icons::ELLIPSIS, icons::font(18.0), - if oresp.hovered() || state.overflow_open { C_BRIGHT } else { C_DIM }); + if oresp.hovered() || state.overflow_open { pal.text } else { pal.text_dim }); if oresp.clicked() { state.overflow_open = !state.overflow_open; state.palette_open = false; } if state.overflow_open { - render_overflow(ui, full, rc, state); + render_overflow(ui, full, rc, state, pal); } else if state.palette_open { - render_palette(ui, full, rc, state); + render_palette(ui, full, rc, state, pal); } } /// Common modal scrim + panel. Returns (backdrop-tapped, panel inner rect). -fn open_panel(ui: &mut egui::Ui, full: egui::Rect, id: &str) -> (bool, egui::Rect) { +fn open_panel(ui: &mut egui::Ui, full: egui::Rect, id: &str, pal: &Palette) -> (bool, egui::Rect) { let scrim = ui.interact(full, ui.id().with(("mobile_topbar_scrim", id)), egui::Sense::click()); - ui.painter().rect_filled(full, 0.0, egui::Color32::from_rgba_premultiplied(8, 10, 14, 170)); + ui.painter().rect_filled(full, 0.0, pal.scrim); let panel = egui::Rect::from_min_max( egui::pos2(full.left() + 16.0, full.top() + 44.0), egui::pos2(full.right() - 16.0, full.bottom() - 60.0), ); - ui.painter().rect_filled(panel, 14.0, C_PANEL); - ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside); + ui.painter().rect_filled(panel, 14.0, pal.surface); + ui.painter().rect_stroke(panel, 14.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); (scrim.clicked(), panel) } -fn command_button(ui: &mut egui::Ui, r: egui::Rect, label: &str, key: (&str, usize)) -> bool { +fn command_button(ui: &mut egui::Ui, r: egui::Rect, label: &str, key: (&str, usize), pal: &Palette) -> bool { let resp = ui.interact(r, ui.id().with(("mobile_cmd", key.0, key.1)), egui::Sense::click()); - ui.painter().rect_filled(r, 8.0, if resp.hovered() { C_PANEL2 } else { C_PANEL }); - ui.painter().hline(r.x_range(), r.bottom(), egui::Stroke::new(1.0, C_LINE)); + ui.painter().rect_filled(r, 8.0, if resp.hovered() { pal.surface_alt } else { pal.surface }); + ui.painter().hline(r.x_range(), r.bottom(), egui::Stroke::new(1.0, pal.line)); ui.painter().text( egui::pos2(r.left() + 14.0, r.center().y), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(13.0), - C_BRIGHT, + pal.text, ); resp.clicked() } -fn render_overflow(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { - let (backdrop, panel) = open_panel(ui, full, "overflow"); +fn render_overflow(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette) { + let (backdrop, panel) = open_panel(ui, full, "overflow", pal); let mut close = backdrop; let items = overflow_items(); let row_h = 44.0; @@ -145,7 +141,7 @@ fn render_overflow(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, if r.bottom() > inner.bottom() { break; } - if command_button(ui, r, label, ("of", i)) { + if command_button(ui, r, label, ("of", i), pal) { rc.shared.pending_menu_actions.push(*action); close = true; } @@ -155,8 +151,8 @@ fn render_overflow(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, } } -fn render_palette(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState) { - let (backdrop, panel) = open_panel(ui, full, "palette"); +fn render_palette(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette) { + let (backdrop, panel) = open_panel(ui, full, "palette", pal); let mut close = backdrop; let inner = panel.shrink(8.0); @@ -184,7 +180,7 @@ fn render_palette(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, s if r.bottom() > inner.bottom() { break; } - if command_button(ui, r, label, ("pal", i)) { + if command_button(ui, r, label, ("pal", i), pal) { rc.shared.pending_menu_actions.push(*action); close = true; } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs index 90b2a50..87f0e5a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs @@ -3,19 +3,13 @@ use eframe::egui; -use super::icons; +use super::{icons, Palette}; use crate::panes::SharedPaneState; -const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); -const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49); -const C_AMBER: egui::Color32 = egui::Color32::from_rgb(0xf4, 0xa3, 0x40); -const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3); -const C_DARK: egui::Color32 = egui::Color32::from_rgb(0x1b, 0x13, 0x0a); - -pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) { +pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState, pal: &Palette) { let painter = ui.painter_at(rect); - painter.rect_filled(rect, 0.0, C_PANEL); - painter.hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, C_LINE)); + painter.rect_filled(rect, 0.0, pal.surface); + painter.hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, pal.line)); let cy = rect.center().y; @@ -24,14 +18,14 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) let btn_center = egui::pos2(rect.left() + 20.0 + btn_r, cy); let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0)); let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click()); - painter.circle_filled(btn_center, btn_r, C_AMBER); + painter.circle_filled(btn_center, btn_r, pal.accent); let glyph = if *shared.is_playing { icons::PAUSE } else { icons::PLAY }; painter.text( btn_center, egui::Align2::CENTER_CENTER, glyph, icons::font(16.0), - C_DARK, + pal.on_accent, ); if btn_resp.clicked() { *shared.is_playing = !*shared.is_playing; @@ -55,7 +49,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) egui::Align2::LEFT_CENTER, &tc, egui::FontId::monospace(13.0), - C_BRIGHT, + pal.text, ); let tc_width = 78.0; @@ -71,18 +65,18 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) ui.id().with("mobile_transport_scrub"), egui::Sense::click_and_drag(), ); - painter.rect_filled(scrub_rect, 3.0, C_LINE); + painter.rect_filled(scrub_rect, 3.0, pal.line); let frac = (*shared.playback_time / duration).clamp(0.0, 1.0) as f32; let filled = egui::Rect::from_min_max( scrub_rect.min, egui::pos2(scrub_rect.left() + scrub_rect.width() * frac, scrub_rect.bottom()), ); - painter.rect_filled(filled, 3.0, C_AMBER.gamma_multiply(0.5)); + painter.rect_filled(filled, 3.0, pal.accent.gamma_multiply(0.5)); let head_x = scrub_rect.left() + scrub_rect.width() * frac; painter.vline( head_x, (scrub_rect.top() - 4.0)..=(scrub_rect.bottom() + 4.0), - egui::Stroke::new(2.0, C_AMBER), + egui::Stroke::new(2.0, pal.accent), ); if (scrub_resp.dragged() || scrub_resp.clicked()) && scrub_rect.width() > 0.0 { diff --git a/lightningbeam-ui/lightningbeam-editor/src/theme.rs b/lightningbeam-ui/lightningbeam-editor/src/theme.rs index 1b6f847..6624b7d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/theme.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/theme.rs @@ -519,6 +519,73 @@ impl Theme { self.resolve(context, ctx).border_color.unwrap_or(fallback) } + /// Look up a CSS custom property (variable) as a color, respecting the active light/dark mode. + /// `name` is given without the leading `--` (e.g. `"bg-surface"`). Dark overrides light. + pub fn var(&self, name: &str, ctx: &egui::Context) -> Option { + let mut vars = self.light_variables.clone(); + if self.is_dark(ctx) { + vars.extend(self.dark_variables.clone()); + } + let raw = vars.get(name)?.clone(); + parse_color_value(&raw, &vars) + } + + /// Apply the theme's core palette variables to egui's global `Visuals`, so the standard egui + /// widgets (buttons, text fields, dialogs, pane chrome) share the same colors as the mobile UI. + /// Cheap enough to call every frame; respects the active light/dark mode. + pub fn apply_to_egui(&self, ctx: &egui::Context) { + let is_dark = self.is_dark(ctx); + let v = |name: &str, fb: egui::Color32| self.var(name, ctx).unwrap_or(fb); + let g = |n: u8| egui::Color32::from_gray(n); + + let text = v("text-primary", if is_dark { g(230) } else { g(20) }); + let bg_app = v("bg-app", if is_dark { g(42) } else { g(224) }); + let panel = v("bg-panel", bg_app); + let surface = v("bg-surface", panel); + let raised = v("bg-surface-raised", surface); + let sunken = v("bg-surface-sunken", bg_app); + let border = v("border-default", if is_dark { g(68) } else { g(153) }); + let accent = v("accent", egui::Color32::from_rgb(0x39, 0x6c, 0xd8)); + let on_accent = v("text-on-accent", egui::Color32::WHITE); + let stroke = |c: egui::Color32| egui::Stroke::new(1.0, c); + + let mut visuals = if is_dark { egui::Visuals::dark() } else { egui::Visuals::light() }; + visuals.panel_fill = panel; + visuals.window_fill = panel; + visuals.window_stroke = stroke(border); + visuals.extreme_bg_color = sunken; + visuals.faint_bg_color = surface; + visuals.override_text_color = Some(text); + visuals.hyperlink_color = accent; + + let w = &mut visuals.widgets; + w.noninteractive.bg_fill = panel; + w.noninteractive.weak_bg_fill = panel; + w.noninteractive.bg_stroke = stroke(border); + w.noninteractive.fg_stroke = stroke(text); + w.inactive.bg_fill = surface; + w.inactive.weak_bg_fill = surface; + w.inactive.bg_stroke = stroke(border); + w.inactive.fg_stroke = stroke(text); + w.hovered.bg_fill = raised; + w.hovered.weak_bg_fill = raised; + w.hovered.bg_stroke = stroke(border); + w.hovered.fg_stroke = stroke(text); + w.active.bg_fill = accent; + w.active.weak_bg_fill = accent; + w.active.bg_stroke = stroke(accent); + w.active.fg_stroke = stroke(on_accent); + w.open.bg_fill = surface; + w.open.weak_bg_fill = surface; + w.open.bg_stroke = stroke(border); + w.open.fg_stroke = stroke(text); + + visuals.selection.bg_fill = accent.linear_multiply(0.4); + visuals.selection.stroke = stroke(on_accent); + + ctx.set_visuals(visuals); + } + /// Convenience: resolve and extract a dimension with fallback #[allow(dead_code)] pub fn dimension(&self, context: &[&str], ctx: &egui::Context, property: &str, fallback: f32) -> f32 { From 4b2802076b03f54c14a68120696bc573f4d0387e Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 1 Jul 2026 07:28:08 -0400 Subject: [PATCH 19/43] Touch-friendly egui widgets on mobile; egui-based inspector header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mobile::apply_touch_style(ctx) — applied every frame when mobile is active — enlarging egui's global spacing/sizing (interact_size, button padding, slider width/rail, combo width/height, icon sizes, scrollbar, body/button text) so the standard widgets in panes and dialogs are finger-sized. Desktop is untouched. Rework the mobile inspector header into a single row using real egui widgets: title (left, truncating) + Timeline/Nodes jump buttons + ✕ close, laid out with egui layouts so they inherit the touch sizing and theme visuals instead of being hand-painted. --- .../lightningbeam-editor/src/main.rs | 4 + .../src/mobile/inspector.rs | 80 +++++++++---------- .../lightningbeam-editor/src/mobile/mod.rs | 30 +++++++ 3 files changed, 70 insertions(+), 44 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 63e0c6a..00772b2 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -5346,6 +5346,10 @@ impl eframe::App for EditorApp { // Apply the theme's palette to egui's global visuals so the standard widgets (dialogs, pane // chrome, text) share the same colors as the mobile UI and respond to light/dark/user themes. self.theme.apply_to_egui(ctx); + // On mobile, enlarge egui spacing/sizing so widgets in panes/dialogs are touch-friendly. + if self.mobile_active() { + mobile::apply_touch_style(ctx); + } // === Raster fault-in (Phase 3 paging) === // The canvas records raster keyframe ids whose `raw_pixels` weren't resident diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index e75ab69..5c23319 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -11,8 +11,7 @@ use crate::panes::{NodePath, SharedPaneState}; use crate::RenderContext; const GRAB_H: f32 = 16.0; -const HEAD_H: f32 = 30.0; -const CHIP_H: f32 = 30.0; +const HEAD_H: f32 = 44.0; fn inspector_path() -> NodePath { vec![MOBILE_NS, 200] @@ -123,61 +122,54 @@ pub fn render( ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); } - // Header row: title + close. + // Single header row (egui widgets — they inherit the mobile touch sizing + theme visuals): + // title on the left, [Timeline] [Nodes] jump buttons + ✕ close on the right. let head_y = rect.top() + GRAB_H; let head = egui::Rect::from_min_max( egui::pos2(rect.left(), head_y), egui::pos2(rect.right(), head_y + HEAD_H), ); - ui.painter().text( - egui::pos2(head.left() + 14.0, head.center().y), - egui::Align2::LEFT_CENTER, - title(&rc.shared), - egui::FontId::proportional(13.0), - pal.text, + let title_text = title(&rc.shared); + let mut jump: Option<(usize, usize)> = None; + let mut do_close = false; + let mut hui = ui.new_child( + egui::UiBuilder::new() + .max_rect(head.shrink2(egui::vec2(12.0, 5.0))) + .layout(egui::Layout::left_to_right(egui::Align::Center)), ); - let close = egui::Rect::from_min_size(egui::pos2(head.right() - 36.0, head.top()), egui::vec2(36.0, HEAD_H)); - let cresp = ui.interact(close, ui.id().with("mobile_inspector_close"), egui::Sense::click()); - ui.painter().text( - close.center(), - egui::Align2::CENTER_CENTER, - super::icons::X, - super::icons::font(16.0), - if cresp.hovered() { pal.text } else { pal.text_dim }, - ); - if cresp.clicked() { + hui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui + .add(egui::Button::new(egui::RichText::new(super::icons::X).font(super::icons::font(18.0))).frame(false)) + .clicked() + { + do_close = true; + } + ui.add_space(4.0); + // Reversed so the visual order stays Timeline, then Nodes. + for chip in CHIPS.iter().rev() { + if ui.button(chip.label).clicked() { + jump = Some(chip.window); + } + } + // Title fills the remaining space on the left. + ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { + ui.add(egui::Label::new(egui::RichText::new(title_text).color(pal.text)).truncate()); + }); + }); + if do_close { rc.shared.selection.clear(); *rc.shared.focus = FocusSelection::None; } - - // Jump-to chips (reframe to a related surface, keeping the selection). - let chip_y = head.bottom(); - let mut cx = rect.left() + 12.0; - for (i, chip) in CHIPS.iter().enumerate() { - let galley = ui.painter().layout_no_wrap( - chip.label.to_string(), - egui::FontId::proportional(11.0), - pal.text, - ); - let w = galley.size().x + 18.0; - let chip_rect = egui::Rect::from_min_size(egui::pos2(cx, chip_y + 4.0), egui::vec2(w, CHIP_H - 8.0)); - let resp = ui.interact(chip_rect, ui.id().with(("mobile_inspector_chip", i)), egui::Sense::click()); - ui.painter().rect_filled(chip_rect, 11.0, if resp.hovered() { pal.line } else { pal.surface_alt }); - ui.painter().rect_stroke(chip_rect, 11.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); - ui.painter().galley(chip_rect.center() - galley.size() * 0.5, galley, pal.text); - if resp.clicked() { - let (top, count) = chip.window; - state.window_top = top; - state.window_count = count; - state.weights = [1.0, 1.0, 1.0]; - state.anim = None; - } - cx += w + 6.0; + if let Some((top, count)) = jump { + state.window_top = top; + state.window_count = count; + state.weights = [1.0, 1.0, 1.0]; + state.anim = None; } // Properties content — reuse the Infopanel full-bleed. let content = egui::Rect::from_min_max( - egui::pos2(rect.left(), chip_y + CHIP_H), + egui::pos2(rect.left(), head.bottom()), rect.max, ); if content.height() > 1.0 { diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 0baacd3..b49863c 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -44,6 +44,36 @@ pub fn dialog_width(ctx: &egui::Context, desired: f32) -> f32 { desired.min(avail.max(200.0)) } +/// Enlarge egui's spacing/sizing so the standard widgets (buttons, dropdowns, sliders, text fields) +/// in panes and dialogs are touch-friendly. Applied every frame after the theme visuals when the +/// mobile shell is active. The mobile chrome (transport, omnibutton, headers) is hand-sized already; +/// this targets the egui-widget content inside panes. +pub fn apply_touch_style(ctx: &egui::Context) { + use egui::{FontId, TextStyle}; + ctx.style_mut(|s| { + let sp = &mut s.spacing; + // Minimum touch target height for buttons/sliders/checkboxes. + sp.interact_size = egui::vec2(sp.interact_size.x.max(44.0), 38.0); + sp.button_padding = egui::vec2(12.0, 9.0); + sp.item_spacing = egui::vec2(10.0, 10.0); + sp.slider_width = 200.0; + sp.slider_rail_height = 10.0; + sp.combo_width = 200.0; + sp.combo_height = 360.0; + sp.text_edit_width = 220.0; + sp.icon_width = 24.0; + sp.icon_width_inner = 14.0; + sp.icon_spacing = 8.0; + sp.scroll.bar_width = 16.0; + sp.menu_margin = egui::Margin::same(8); + // Larger text for touch legibility. + s.text_styles.insert(TextStyle::Body, FontId::proportional(15.0)); + s.text_styles.insert(TextStyle::Button, FontId::proportional(15.0)); + s.text_styles.insert(TextStyle::Monospace, FontId::monospace(13.0)); + s.text_styles.insert(TextStyle::Small, FontId::proportional(12.0)); + }); +} + /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Any non-empty value other than "0" enables it. pub fn is_mobile_env() -> bool { From 01b20165cc7b20e6f330eb30a6419b2ecddda90a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 00:11:43 -0400 Subject: [PATCH 20/43] P5 mobile gestures + long-press context menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gestures (Stage + Timeline): - Pinch-zoom via zoom_delta() (touch pinch + Ctrl+wheel), unified with the raw-wheel path so Ctrl+wheel zooms exactly once; plain wheel zoom + trackpad pan preserved. - Double-tap empty → zoom-to-fit (Stage: artboard via zoom_to_fit; Timeline: new fit_to_project over the full duration). - Double-tap-drag on empty → transient marquee regardless of the active tool. Long-press context menu: a shared SharedPaneState.mobile_context_menu that panes populate with (label, MenuAction) items on secondary_clicked(); the mobile shell renders one persistent popup (styled like the timeline menu) and dispatches via pending_menu_actions. Stage offers Cut/Copy/Duplicate/Delete for clip instances and Cut/Copy/Convert-to-movie-clip/Delete for geometry. Fixes surfaced along the way: - Geometry delete now frees selected fills (free_fill) and GCs isolated vertices (new VectorGraph::gc_isolated_vertices) — no more orphaned fills / phantom snap points; applies to the Delete key too. - clipboard_delete_selection clears focus so deleting dismisses the mobile inspector. - Mobile inspector: appears on pointer release (not press) so drags aren't interrupted; reflows only when the tapped point is actually behind the sheet; taps outside the sheet dismiss it; rounded-corner border no longer detaches. --- .../src/vector_graph/mod.rs | 23 ++ .../lightningbeam-editor/src/main.rs | 57 ++-- .../src/mobile/inspector.rs | 13 +- .../lightningbeam-editor/src/mobile/mod.rs | 127 +++++++- .../lightningbeam-editor/src/mobile/stack.rs | 1 + .../lightningbeam-editor/src/panes/mod.rs | 12 + .../lightningbeam-editor/src/panes/stage.rs | 307 ++++++++++++++---- .../src/panes/timeline.rs | 56 +++- 8 files changed, 474 insertions(+), 122 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/vector_graph/mod.rs b/lightningbeam-ui/lightningbeam-core/src/vector_graph/mod.rs index 646fe31..16cc15f 100644 --- a/lightningbeam-ui/lightningbeam-core/src/vector_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-core/src/vector_graph/mod.rs @@ -351,6 +351,29 @@ impl VectorGraph { } } + /// Free any vertex no longer referenced by a non-deleted edge (e.g. after deleting a shape's + /// edges), so stale vertices don't linger as snap targets. + pub fn gc_isolated_vertices(&mut self) { + let mut referenced = vec![false; self.vertices.len()]; + for e in &self.edges { + if e.deleted { + continue; + } + for v in e.vertices.iter() { + if !v.is_none() { + referenced[v.idx()] = true; + } + } + } + let to_free: Vec = (0..self.vertices.len()) + .filter(|&i| !self.vertices[i].deleted && !referenced[i]) + .map(|i| VertexId(i as u32)) + .collect(); + for vid in to_free { + self.free_vertex(vid); + } + } + // ------------------------------------------------------------------- // Fill / hit-test queries // ------------------------------------------------------------------- diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 00772b2..94621dc 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -959,6 +959,8 @@ struct EditorApp { mobile_ui_override: Option, /// Persistent state for the mobile shell (active surface, ribbon tier). mobile_state: mobile::MobileState, + /// Active mobile long-press context menu (set by a pane, rendered by the shell). + mobile_context_menu: Option, icon_cache: IconCache, tool_icon_cache: ToolIconCache, focus_icon_cache: FocusIconCache, // Focus card icons (start screen) @@ -1320,6 +1322,7 @@ impl EditorApp { mobile_ui: mobile::is_mobile_env(), mobile_ui_override: None, mobile_state: mobile::MobileState::default(), + mobile_context_menu: None, icon_cache: IconCache::new(), tool_icon_cache: ToolIconCache::new(), focus_icon_cache: FocusIconCache::new(), @@ -2681,45 +2684,56 @@ impl EditorApp { } self.selection.clear_clip_instances(); + self.focus = lightningbeam_core::selection::FocusSelection::None; } else if self.selection.has_geometry_selection() { let active_layer_id = match self.active_layer_id { Some(id) => id, None => return, }; - // Delete the selected edges (region/marquee/click all populate the same sets). + // Delete the selected geometry (region/marquee/click all populate the same sets). + // Selecting a fill also selects its boundary edges, so removing edges alone would leave + // the fill's face orphaned — free the fills too so the whole shape is removed. let edge_ids: Vec = self.selection.selected_edges().iter().copied().collect(); + let fill_ids: Vec = + self.selection.selected_fills().iter().copied().collect(); - if !edge_ids.is_empty() { + if !edge_ids.is_empty() || !fill_ids.is_empty() { let document = self.action_executor.document(); - if let Some(layer) = document.get_layer(&active_layer_id) { - if let lightningbeam_core::layer::AnyLayer::Vector(vector_layer) = layer { - if let Some(graph_before) = vector_layer.graph_at_time(self.playback_time) { - let mut graph_after = graph_before.clone(); - for edge_id in &edge_ids { - if !graph_after.edge(*edge_id).deleted { - graph_after.remove_edge(*edge_id); - } + if let Some(lightningbeam_core::layer::AnyLayer::Vector(vector_layer)) = document.get_layer(&active_layer_id) { + if let Some(graph_before) = vector_layer.graph_at_time(self.playback_time) { + let mut graph_after = graph_before.clone(); + // Free fills first so their boundary edges become unreferenced and are GC'd. + for fill_id in &fill_ids { + if !graph_after.fill(*fill_id).deleted { + graph_after.free_fill(*fill_id); } - - let action = lightningbeam_core::actions::ModifyGraphAction::new( - active_layer_id, - self.playback_time, - graph_before.clone(), - graph_after, - "Delete selected edges", - ); - - if let Err(e) = self.action_executor.execute(Box::new(action)) { - eprintln!("Delete DCEL edges failed: {}", e); + } + for edge_id in &edge_ids { + if !graph_after.edge(*edge_id).deleted { + graph_after.remove_edge(*edge_id); } } + graph_after.gc_isolated_vertices(); + + let action = lightningbeam_core::actions::ModifyGraphAction::new( + active_layer_id, + self.playback_time, + graph_before.clone(), + graph_after, + "Delete", + ); + + if let Err(e) = self.action_executor.execute(Box::new(action)) { + eprintln!("Delete geometry failed: {}", e); + } } } } self.selection.clear_geometry_selection(); + self.focus = lightningbeam_core::selection::FocusSelection::None; } } @@ -7182,6 +7196,7 @@ impl EditorApp { rc: RenderContext { shared: panes::SharedPaneState { is_mobile, + mobile_context_menu: &mut self.mobile_context_menu, container_path: self.current_file_path.clone(), onion: { // Onion skinning is disabled during playback. diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index 5c23319..cfec83d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -24,6 +24,7 @@ pub fn is_active(shared: &SharedPaneState) -> bool { /// The stack slot (see `super::STACK`) where the current selection lives, so we can tell if the /// sheet would cover it. Geometry/selection lives on the Stage; clips/layers on the Timeline; etc. +#[allow(dead_code)] // selection→pane mapping; kept for reflow/jump heuristics pub fn target_slot(shared: &SharedPaneState) -> usize { match &*shared.focus { FocusSelection::Notes { .. } => 4, // PianoRoll @@ -99,13 +100,11 @@ pub fn render( state: &mut MobileState, pal: &Palette, ) { - // Sheet background with rounded top. - ui.painter().rect_filled( - rect, - egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }, - pal.surface_alt, - ); - ui.painter().hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, pal.line)); + // Sheet background with rounded top + a border stroke that follows the rounded corners (a plain + // hline would overrun the rounded corners and detach from them). + let radius = egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }; + ui.painter().rect_filled(rect, radius, pal.surface_alt); + ui.painter().rect_stroke(rect, radius, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); // Grab handle — drag to resize the sheet. let grab = egui::Rect::from_min_max( diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index b49863c..9601894 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -191,6 +191,12 @@ pub struct MobileState { pub show_instruments: bool, /// Inspector sheet height as a fraction of the region above the transport. pub inspector_frac: f32, + /// Whether the inspector sheet is currently shown. Gated to appear on pointer *release* (a tap), + /// not on press, so press+drag interactions aren't interrupted by the sheet popping up. + pub inspector_visible: bool, + /// Screen-y of the tap that opened the inspector — used to decide whether the tapped thing would + /// be hidden by the sheet (only then do we reflow the stack). + pub inspector_anchor_y: f32, /// Whether the omnibutton radial tool menu is open. pub omni_open: bool, /// Whether the omnibutton "more" grid (all tools) is open. @@ -217,6 +223,8 @@ impl Default for MobileState { weights: [1.0, 1.0, 1.0], show_instruments: false, inspector_frac: 0.45, + inspector_visible: false, + inspector_anchor_y: 0.0, omni_open: false, omni_grid_open: false, omni_create_open: false, @@ -255,22 +263,55 @@ pub fn render_mobile_shell( egui::pos2(available_rect.left(), topbar_rect.bottom()), egui::pos2(available_rect.right(), transport_rect.top()), ); - // When the inspector is up, the sheet overlays the lower part of the stack. If the selected - // pane would be *covered* by the sheet, reflow (shrink the stack above the sheet) so it stays - // visible; otherwise leave the stack full-height and just overlay. Restoring on dismiss is - // automatic — reflow only changes the render rect, not the window state. - let inspector_shown = inspector::is_active(&rc.shared); + // The inspector sheet appears on pointer *release* (a tap), not on press — so press+drag on the + // canvas isn't interrupted by the sheet popping up. Track visibility: hide when nothing's + // selected; show once the pointer is up with a selection; keep it shown while it's being + // interacted with (pointer down but still selected). + let active = inspector::is_active(&rc.shared); + let any_down = ui.input(|i| i.pointer.any_down()); + if !active { + state.inspector_visible = false; + } else if !any_down { + if !state.inspector_visible { + // Just opened (tap released): anchor to the release position for the reflow test below. + state.inspector_anchor_y = ui + .input(|i| i.pointer.interact_pos()) + .map(|p| p.y) + .unwrap_or(region.bottom()); + } + state.inspector_visible = true; + } + let mut inspector_shown = state.inspector_visible; let sheet_h = if inspector_shown { (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0) } else { 0.0 }; - let sheet_top = region.bottom() - sheet_h; + let mut sheet_top = region.bottom() - sheet_h; - let covered = inspector_shown - && stack::pane_bottom_in(state, region, inspector::target_slot(&rc.shared)) - .map(|bottom| bottom > sheet_top + 1.0) - .unwrap_or(false); + // Tapping outside the sheet (but not on the transport) dismisses the inspector. The press still + // reaches the panes below via stack::render, so tapping another object reselects and reopens it. + if inspector_shown { + let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); + let dismiss = ui.input(|i| { + i.pointer.primary_pressed() + && i.pointer.press_origin().map_or(false, |p| { + !sheet_rect.contains(p) && !transport_rect.contains(p) + }) + }); + if dismiss { + rc.shared.selection.clear(); + *rc.shared.focus = lightningbeam_core::selection::FocusSelection::None; + state.inspector_visible = false; + inspector_shown = false; + sheet_top = region.bottom(); + } + } + + // Reflow (shrink the stack above the sheet) only when the thing that was tapped would actually be + // hidden by the sheet — i.e. the tap landed below where the sheet now sits. Otherwise just + // overlay. Restoring on dismiss is automatic — reflow only changes the render rect. + let covered = inspector_shown && state.inspector_anchor_y > sheet_top; let stack_rect = if covered { egui::Rect::from_min_max(region.min, egui::pos2(region.right(), sheet_top)) } else { @@ -292,4 +333,70 @@ pub fn render_mobile_shell( // Top bar (filename + ⌕ palette + ⋯ commands). Its menus overlay the whole shell, so it's last. topbar::render(ui, topbar_rect, available_rect, rc, state, &pal); + + // Long-press context menu (populated by whichever pane was long-pressed). Persistent popup, + // dispatched via pending_menu_actions. + render_context_menu(ui, available_rect, rc); +} + +/// Render the pane-populated long-press context menu (`shared.mobile_context_menu`) as a persistent +/// popup: it stays open until an item is chosen or the user taps outside it. Styled to match the +/// timeline's context menu (default themed popup frame + full-width, touch-height items). +fn render_context_menu(ui: &mut egui::Ui, available: egui::Rect, rc: &mut RenderContext) { + let Some(menu) = rc.shared.mobile_context_menu.clone() else { + return; + }; + // Keep the popup on screen (rough clamp against its estimated size). + let est = egui::vec2(200.0, menu.items.len() as f32 * ui.spacing().interact_size.y + 16.0); + let pos = egui::pos2( + menu.pos.x.min(available.right() - est.x - 8.0).max(available.left() + 8.0), + menu.pos.y.min(available.bottom() - est.y - 8.0).max(available.top() + 8.0), + ); + + let mut chosen: Option = None; + let area = egui::Area::new(ui.id().with("mobile_ctx_menu")) + .order(egui::Order::Foreground) + .fixed_pos(pos) + .interactable(true) + .show(ui.ctx(), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.set_min_width(180.0); + for (label, action) in &menu.items { + let w = ui.available_width(); + let (rect, resp) = + ui.allocate_exact_size(egui::vec2(w, ui.spacing().interact_size.y), egui::Sense::click()); + if ui.is_rect_visible(rect) { + if resp.hovered() { + ui.painter().rect_filled(rect, 2.0, ui.visuals().widgets.hovered.bg_fill); + } + let tc = if resp.hovered() { + ui.visuals().widgets.hovered.text_color() + } else { + ui.visuals().widgets.inactive.text_color() + }; + ui.painter().text( + rect.min + egui::vec2(10.0, (rect.height() - 14.0) / 2.0), + egui::Align2::LEFT_TOP, + label, + egui::FontId::proportional(14.0), + tc, + ); + } + if resp.clicked() { + chosen = Some(*action); + } + } + }); + }); + + // Dismiss on item choice or a primary click outside the popup. (The secondary click that opened + // it won't dismiss, so there's no just-opened race.) + let primary_click = ui.input(|i| i.pointer.button_clicked(egui::PointerButton::Primary)); + let dismiss = chosen.is_some() || (primary_click && !area.response.contains_pointer()); + if let Some(action) = chosen { + rc.shared.pending_menu_actions.push(action); + } + if dismiss { + *rc.shared.mobile_context_menu = None; + } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 5921527..ad47a8e 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -221,6 +221,7 @@ fn nweights(weights: &[f32; 3], count: usize) -> Vec { /// The bottom Y of the given stack slot's band within `region` (using the current window + /// weights), or None if that slot isn't in the visible window. Used to decide whether the /// inspector sheet would cover the selected pane. +#[allow(dead_code)] // kept for pane-coverage heuristics pub fn pane_bottom_in(state: &MobileState, region: egui::Rect, slot: usize) -> Option { let (top, count) = (state.window_top, state.window_count); if slot < top || slot >= top + count { diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index d1f1fe4..ef40988 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -358,6 +358,18 @@ pub struct SharedPaneState<'a> { pub brush_preview_pixels: &'a std::sync::Arc)>>>, /// True when rendering the phone/mobile shell (panes can render more compactly). pub is_mobile: bool, + /// Mobile long-press context menu request. A pane sets this on `response.secondary_clicked()` + /// (which fires on long-press) with the items relevant to what was pressed; the mobile shell + /// renders one persistent popup and dispatches the chosen `MenuAction`. `None` = no menu. + pub mobile_context_menu: &'a mut Option, +} + +/// A mobile long-press context menu: a screen position and a list of `(label, action)` items. +/// Rendered by the mobile shell; each item dispatches its `MenuAction` via `pending_menu_actions`. +#[derive(Clone)] +pub struct MobileContextMenu { + pub pos: egui::Pos2, + pub items: Vec<(String, crate::menu::MenuAction)>, } /// Trait for pane rendering diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index a34094f..bb4078d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -3255,6 +3255,14 @@ pub struct StagePane { // Interaction state is_panning: bool, last_pan_pos: Option, + // Gesture state (P5): double-tap-drag marquee. `last_tap` = (time, canvas pos) of the last + // primary press for double-tap detection; `marquee_gesture_armed` = a double-tap landed on empty + // space and a drag will start a transient marquee regardless of the active tool. + last_tap: Option<(f64, egui::Pos2)>, + marquee_gesture_armed: bool, + /// True once an armed double-tap has actually begun dragging (so we suppress the active tool for + /// the duration of the marquee but NOT for a plain double-tap, which still zooms-to-fit). + marquee_gesture_active: bool, // Unique ID for this stage instance (for Vello resources) instance_id: u64, // Eyedropper state @@ -3743,6 +3751,9 @@ impl StagePane { needs_initial_center: true, is_panning: false, last_pan_pos: None, + last_tap: None, + marquee_gesture_armed: false, + marquee_gesture_active: false, instance_id, pending_eyedropper_sample: None, last_viewport_rect: None, @@ -4030,6 +4041,18 @@ impl StagePane { // Double-click on empty space while inside a clip: exit *shared.pending_exit_clip = true; return; + } else { + // Double-tap / double-click on truly empty space (no clip and no shape under the + // pointer, at the top level) → zoom to fit the artboard. (Gesture P5-2.) + let graph_hit = if inbetween { + None + } else { + hit_test::hit_test_layer(vector_layer, *shared.playback_time, point, 5.0, Affine::IDENTITY) + }; + if graph_hit.is_none() { + self.zoom_to_fit(shared); + return; + } } } @@ -11048,6 +11071,75 @@ impl StagePane { } } + /// True if a clip instance or vector geometry is under `point` (clip-local) on the active layer. + /// Used by the double-tap-drag marquee gesture to decide "empty vs object". + fn point_hits_object(&self, point: vello::kurbo::Point, shared: &SharedPaneState) -> bool { + use lightningbeam_core::hit_test; + use lightningbeam_core::layer::AnyLayer; + use vello::kurbo::Affine; + let Some(active_layer_id) = *shared.active_layer_id else { + return false; + }; + let document = shared.action_executor.document(); + let Some(AnyLayer::Vector(vl)) = document.get_layer(&active_layer_id) else { + return false; + }; + if hit_test::hit_test_clip_instances(&vl.clip_instances, document, point, Affine::IDENTITY, *shared.playback_time).is_some() { + return true; + } + hit_test::hit_test_layer(vl, *shared.playback_time, point, 5.0, Affine::IDENTITY).is_some() + } + + /// Delete the currently-selected DCEL edges on the active vector layer (snapshot-based undo). + /// Shared by the Delete/Backspace key and the long-press context menu. + fn delete_selected_geometry(&self, shared: &mut SharedPaneState) { + let Some(active_layer_id) = *shared.active_layer_id else { + return; + }; + let time = *shared.playback_time; + let selected_edges: Vec = + shared.selection.selected_edges().iter().copied().collect(); + let selected_fills: Vec = + shared.selection.selected_fills().iter().copied().collect(); + if selected_edges.is_empty() && selected_fills.is_empty() { + return; + } + let graph_before = match shared.action_executor.document().get_layer(&active_layer_id) { + Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => vl.graph_at_time(time).cloned(), + _ => None, + }; + let Some(graph_before) = graph_before else { + return; + }; + { + let document = shared.action_executor.document_mut(); + if let Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) = document.get_layer_mut(&active_layer_id) { + if let Some(graph) = vl.graph_at_time_mut(time) { + // Free fills first so their boundary edges become unreferenced and are GC'd. + for fid in &selected_fills { + if !graph.fill(*fid).deleted { + graph.free_fill(*fid); + } + } + for eid in &selected_edges { + graph.remove_edge(*eid); + } + graph.gc_isolated_vertices(); + } + } + } + let graph_after = match shared.action_executor.document().get_layer(&active_layer_id) { + Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => vl.graph_at_time(time).cloned(), + _ => None, + }; + if let Some(graph_after) = graph_after { + use lightningbeam_core::actions::ModifyGraphAction; + let action = ModifyGraphAction::new(active_layer_id, time, graph_before, graph_after, "Delete"); + shared.pending_actions.push(Box::new(action)); + } + shared.selection.clear_geometry_selection(); + } + fn handle_input(&mut self, ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) { let response = ui.allocate_rect(rect, egui::Sense::click_and_drag()); @@ -11314,8 +11406,126 @@ impl StagePane { } } - // Handle tool input (only if not using Alt modifier for panning) - if !alt_held { + // P5 gesture: double-tap-drag on empty space → transient marquee, regardless of the active + // tool. The second tap arms it; a drag then drives ToolState::MarqueeSelecting (resolved by + // the release handler above). While armed we suppress the normal tool dispatch so a + // brush/etc. doesn't paint during the gesture. A double-tap without a drag falls through + // (Select tool handles zoom-to-fit). + if !is_replaying && !alt_held { + use vello::kurbo::Point; + let point = Point::new(world_pos.x as f64, world_pos.y as f64); + let press_pos = mouse_canvas_pos.to_pos2(); + if self.rsp_primary_pressed(ui) { + let now = ui.input(|i| i.time); + let is_double = self + .last_tap + .map_or(false, |(t, p)| now - t < 0.3 && p.distance(press_pos) < 12.0); + let hits = self.point_hits_object(point, shared); + self.marquee_gesture_armed = is_double && !hits; + self.last_tap = Some((now, press_pos)); + } + if self.marquee_gesture_armed && self.rsp_dragged(&response) { + self.marquee_gesture_active = true; + match shared.tool_state { + ToolState::MarqueeSelecting { start, .. } => { + *shared.tool_state = ToolState::MarqueeSelecting { start: *start, current: point }; + } + _ => { + if !shift_held { + shared.selection.clear(); + *shared.focus = lightningbeam_core::selection::FocusSelection::None; + } + *shared.tool_state = ToolState::MarqueeSelecting { start: point, current: point }; + } + } + } + if ui.input(|i| i.pointer.any_released()) { + self.marquee_gesture_armed = false; + self.marquee_gesture_active = false; + } + } + + // P5 gesture (mobile only): long-press on an object opens its actions menu. In the egui fork + // `secondary_clicked()` fires on long-press (and right-click) and `context_menu()` opens on + // the same — so the menu must be attached UNCONDITIONALLY for egui to register it. Desktop + // (flag off) has no Stage context menu. + if shared.is_mobile { + use lightningbeam_core::layer::AnyLayer; + use lightningbeam_core::selection::FocusSelection; + use vello::kurbo::{Affine, Point}; + // Long-press selects the object under the press (clip instance first, else geometry). + if response.secondary_clicked() { + if let Some(layer_id) = *shared.active_layer_id { + let point = Point::new(world_pos.x as f64, world_pos.y as f64); + let time = *shared.playback_time; + let clip_hit = { + let document = shared.action_executor.document(); + if let Some(AnyLayer::Vector(vl)) = document.get_layer(&layer_id) { + lightningbeam_core::hit_test::hit_test_clip_instances(&vl.clip_instances, document, point, Affine::IDENTITY, time) + } else { + None + } + }; + if let Some(instance_id) = clip_hit { + if !shared.selection.contains_clip_instance(&instance_id) { + shared.selection.select_only_clip_instance(instance_id); + *shared.focus = FocusSelection::ClipInstances(shared.selection.clip_instances().to_vec()); + } + } else { + // No clip — try vector geometry (edge/fill). + let graph_hit = { + let document = shared.action_executor.document(); + if let Some(AnyLayer::Vector(vl)) = document.get_layer(&layer_id) { + lightningbeam_core::hit_test::hit_test_layer(vl, time, point, 5.0, Affine::IDENTITY) + } else { + None + } + }; + if let Some(gh) = graph_hit { + if let Some(AnyLayer::Vector(vl)) = shared.action_executor.document().get_layer(&layer_id) { + if let Some(graph) = vl.graph_at_time(time) { + shared.selection.clear_geometry_selection(); + match gh { + lightningbeam_core::hit_test::GraphHitResult::Edge(eid) => shared.selection.select_edge(eid, graph), + lightningbeam_core::hit_test::GraphHitResult::Fill(fid) => shared.selection.select_fill(fid, graph), + } + } + } + *shared.focus = FocusSelection::Geometry { layer_id, time }; + } + } + } + } + + // Build the menu items for the current selection and hand them to the shell to render + // as a persistent popup (stays open until an item or the backdrop is tapped). + if response.secondary_clicked() { + use crate::menu::MenuAction; + let mut items: Vec<(String, MenuAction)> = Vec::new(); + if !shared.selection.clip_instances().is_empty() { + // A clip instance is already a clip — no "convert" here. + items.push(("Cut".into(), MenuAction::Cut)); + items.push(("Copy".into(), MenuAction::Copy)); + items.push(("Duplicate".into(), MenuAction::DuplicateClip)); + items.push(("Delete".into(), MenuAction::Delete)); + } else if shared.selection.has_geometry_selection() { + // Geometry can be gathered up into a movie clip. + items.push(("Cut".into(), MenuAction::Cut)); + items.push(("Copy".into(), MenuAction::Copy)); + items.push(("Convert to movie clip".into(), MenuAction::ConvertToMovieClip)); + items.push(("Delete".into(), MenuAction::Delete)); + } + if !items.is_empty() { + if let Some(pos) = ui.input(|i| i.pointer.interact_pos()) { + *shared.mobile_context_menu = Some(crate::panes::MobileContextMenu { pos, items }); + } + } + } + } + + // Handle tool input (only if not using Alt modifier for panning, and not while a + // double-tap-drag marquee is actively dragging so the tool doesn't act during the gesture). + if !alt_held && !self.marquee_gesture_active { use lightningbeam_core::tool::Tool; // On a shape-tween in-between frame the active vector layer's geometry is an @@ -11425,70 +11635,26 @@ impl StagePane { // Delete/Backspace: remove selected DCEL elements if ui.input(|i| shared.keymap.action_pressed_with_backspace(crate::keymap::AppAction::StageDelete, i)) { if shared.selection.has_geometry_selection() { - if let Some(active_layer_id) = *shared.active_layer_id { - let time = *shared.playback_time; - - // Collect selected edge IDs before mutating - let selected_edges: Vec = - shared.selection.selected_edges().iter().copied().collect(); - - if !selected_edges.is_empty() { - // Snapshot before - let graph_before = { - let document = shared.action_executor.document(); - match document.get_layer(&active_layer_id) { - Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => { - vl.graph_at_time(time).cloned() - } - _ => None, - } - }; - - if let Some(graph_before) = graph_before { - // Remove selected edges - { - let document = shared.action_executor.document_mut(); - if let Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) = document.get_layer_mut(&active_layer_id) { - if let Some(graph) = vl.graph_at_time_mut(time) { - for eid in &selected_edges { - graph.remove_edge(*eid); - } - } - } - } - - // Snapshot after - let graph_after = { - let document = shared.action_executor.document(); - match document.get_layer(&active_layer_id) { - Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => { - vl.graph_at_time(time).cloned() - } - _ => None, - } - }; - - if let Some(graph_after) = graph_after { - use lightningbeam_core::actions::ModifyGraphAction; - let action = ModifyGraphAction::new( - active_layer_id, - time, - graph_before, - graph_after, - "Delete", - ); - shared.pending_actions.push(Box::new(action)); - } - - shared.selection.clear_geometry_selection(); - } - } - } + self.delete_selected_geometry(shared); } } // Skip real scroll/zoom/pan input during replay if !is_replaying { + // Unified pinch / Ctrl+scroll zoom: `zoom_delta()` folds in touch-pinch (on device) AND + // Ctrl+wheel/trackpad (on desktop). It's a multiplicative factor (1.0 = no change); + // `apply_zoom_at_point` wants an additive delta (new = old * (1 + delta)). This is the + // single path for factor-zoom — the raw MouseWheel branch below only handles plain + // (non-Ctrl) mouse-wheel zoom and non-Ctrl trackpad pan. + let zoom_factor = ui.input(|i| i.zoom_delta()); + if (zoom_factor - 1.0).abs() > f32::EPSILON { + let center = ui + .input(|i| i.multi_touch().map(|m| m.center_pos)) + .map(|p| p - rect.min) + .unwrap_or(mouse_canvas_pos); + self.apply_zoom_at_point(zoom_factor - 1.0, center); + } + // Distinguish between mouse wheel (discrete) and trackpad (smooth) let mut handled = false; ui.input(|i| { @@ -11496,23 +11662,20 @@ impl StagePane { if let egui::Event::MouseWheel { unit, delta, modifiers, .. } = event { match unit { egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => { - // Real mouse wheel (discrete clicks) -> always zoom - let zoom_delta = if ctrl_held || modifiers.ctrl { - delta.y * 0.01 // Ctrl+wheel: faster zoom - } else { - delta.y * 0.005 // Normal zoom - }; - self.apply_zoom_at_point(zoom_delta, mouse_canvas_pos); + // Plain mouse wheel zooms; Ctrl+wheel was already handled above via + // zoom_delta(). Consume the event either way so the pan fallback + // below doesn't also fire. + if !(ctrl_held || modifiers.ctrl) { + self.apply_zoom_at_point(delta.y * 0.005, mouse_canvas_pos); + } handled = true; } egui::MouseWheelUnit::Point => { - // Trackpad (smooth scrolling) -> only zoom if Ctrl held + // Trackpad: Ctrl-zoom handled above (consume it); non-Ctrl falls + // through to scroll_delta panning. if ctrl_held || modifiers.ctrl { - let zoom_delta = delta.y * 0.005; - self.apply_zoom_at_point(zoom_delta, mouse_canvas_pos); handled = true; } - // Otherwise let scroll_delta handle panning } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index 158ec7b..d04028a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -1481,6 +1481,15 @@ impl TimelinePane { self.pixels_per_second = 100.0; } + /// Fit the whole project duration into the given content width, scrolled to the start. + /// (Gesture P5-2: double-tap empty → zoom-to-fit.) + pub fn fit_to_project(&mut self, viewport_width: f32) { + let dur = self.duration.max(0.01); + self.pixels_per_second = + (viewport_width / dur as f32).clamp(MIN_PIXELS_PER_SECOND, MAX_PIXELS_PER_SECOND); + self.viewport_start_time = 0.0; + } + /// Apply zoom while keeping the time under the cursor stationary fn apply_zoom_at_point(&mut self, zoom_delta: f32, mouse_x: f32) { let old_zoom = self.pixels_per_second; @@ -4964,29 +4973,40 @@ impl TimelinePane { // Only handle scroll when mouse is over the timeline area let mut handled = false; let pointer_over_timeline = response.hovered() || ui.rect_contains_pointer(header_rect); + + // Unified pinch / Ctrl+scroll time-zoom: `zoom_delta()` folds in touch-pinch (on device) AND + // Ctrl+wheel/trackpad (on desktop). Multiplicative factor (1.0 = no change); apply_zoom_at_point + // wants an additive delta. This is the single path for factor-zoom — the raw MouseWheel branch + // below only handles plain (non-Ctrl) mouse-wheel zoom and non-Ctrl trackpad pan. + if pointer_over_timeline { + let zoom_factor = ui.input(|i| i.zoom_delta()); + if (zoom_factor - 1.0).abs() > f32::EPSILON { + let center_x = ui + .input(|i| i.multi_touch().map(|m| m.center_pos.x)) + .map(|x| x - content_rect.min.x) + .unwrap_or(mouse_x); + self.apply_zoom_at_point(zoom_factor - 1.0, center_x); + } + } + if pointer_over_timeline { ui.input(|i| { for event in &i.raw.events { if let egui::Event::MouseWheel { unit, delta, modifiers, .. } = event { match unit { egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => { - // Real mouse wheel (discrete clicks) -> always zoom horizontally - let zoom_delta = if ctrl_held || modifiers.ctrl { - delta.y * 0.01 // Ctrl+wheel: faster zoom - } else { - delta.y * 0.005 // Normal zoom - }; - self.apply_zoom_at_point(zoom_delta, mouse_x); + // Plain mouse wheel zooms; Ctrl+wheel handled above via zoom_delta(). + // Consume the event either way so the pan fallback doesn't also fire. + if !(ctrl_held || modifiers.ctrl) { + self.apply_zoom_at_point(delta.y * 0.005, mouse_x); + } handled = true; } egui::MouseWheelUnit::Point => { - // Trackpad (smooth scrolling) + // Trackpad: Ctrl-zoom handled above (consume it); non-Ctrl falls through + // to scroll_delta panning below. if ctrl_held || modifiers.ctrl { - // Ctrl held: zoom - let zoom_delta = delta.y * 0.005; - self.apply_zoom_at_point(zoom_delta, mouse_x); handled = true; } - // Otherwise let scroll_delta handle panning (below) } } } @@ -5029,6 +5049,18 @@ impl TimelinePane { } } + // Double-tap / double-click on empty space → fit the whole project into view. (Gesture P5-2.) + if response.double_clicked() { + if let Some(pos) = response.interact_pointer_pos() { + let over_clip = self + .detect_clip_at_pointer(pos, document, content_rect, header_rect, editing_clip_id, &audio_cache) + .is_some(); + if !over_clip { + self.fit_to_project(content_rect.width()); + } + } + } + // Update cursor based on hover position (only if not scrubbing or panning) if !self.is_scrubbing && !self.is_panning { // If dragging a clip with trim/loop, keep the appropriate cursor From 4f58edf436c54b6da410e9d9a728d820c79bed7b Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 01:14:34 -0400 Subject: [PATCH 21/43] P5 follow-ups: double-tap priority, inspector dismiss, breadcrumb path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mobile double-tap is now a single tool-independent priority chain: object → enter (movie clip or group); empty inside a clip → exit one level; empty at root → zoom-to-fit. Desktop keeps its own select-tool double-click (gated). - Inspector "tap outside to dismiss" now only HIDES the sheet (with a dismissed flag reset on selection change) instead of clearing the selection — the earlier clear clobbered the selection before menu/omnibutton actions ran, breaking Group/Convert/Cut/Copy. Delete also clears focus so it dismisses the inspector. - Breadcrumb shows the full editing path (Scene 1 › … › current) via a new SharedPaneState.editing_clip_path / EditingContext::clip_path(); each ancestor crumb jumps to that level (pending_exit_to_depth → EditingContext::exit_to_depth). - Remove the "Vello Stage" debug overlay; move the breadcrumb up to the top-left. --- .../lightningbeam-editor/src/main.rs | 27 +++ .../src/mobile/inspector.rs | 16 ++ .../lightningbeam-editor/src/mobile/mod.rs | 24 ++- .../lightningbeam-editor/src/panes/mod.rs | 5 + .../lightningbeam-editor/src/panes/stage.rs | 161 +++++++++++------- 5 files changed, 163 insertions(+), 70 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 94621dc..ee2ca62 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -942,6 +942,21 @@ impl EditingContext { self.stack.pop() } + /// The clip_id path from outermost to the current level (for breadcrumbs). + fn clip_path(&self) -> Vec { + self.stack.iter().map(|e| e.clip_id).collect() + } + + /// Pop down to `depth` entries, returning the entry at `depth` (whose saved state should be + /// restored). Returns None if already at or above that depth. + fn exit_to_depth(&mut self, depth: usize) -> Option { + if depth >= self.stack.len() { + return None; + } + let restore = self.stack[depth].clone(); + self.stack.truncate(depth); + Some(restore) + } } struct EditorApp { @@ -6926,6 +6941,15 @@ impl eframe::App for EditorApp { } } + // Breadcrumb jump: exit up to a specific depth (restoring that level's saved context). + if let Some(depth) = scratch.pending_exit_to_depth.take() { + if let Some(entry) = self.editing_context.exit_to_depth(depth) { + self.selection.clear(); + self.active_layer_id = entry.saved_active_layer_id; + self.playback_time = entry.saved_playback_time; + } + } + // Set cursor based on hover state if let Some((_, is_horizontal)) = self.hovered_divider { if is_horizontal { @@ -7157,6 +7181,7 @@ struct FrameScratch { pending_handlers: Vec, pending_enter_clip: Option<(Uuid, Uuid, Uuid)>, pending_exit_clip: bool, + pending_exit_to_depth: Option, pending_actions: Vec>, pending_menu_actions: Vec, effect_thumbnail_requests: Vec, @@ -7221,8 +7246,10 @@ impl EditorApp { editing_clip_id: self.editing_context.current_clip_id(), editing_instance_id: self.editing_context.current_instance_id(), editing_parent_layer_id: self.editing_context.current_parent_layer_id(), + editing_clip_path: self.editing_context.clip_path(), pending_enter_clip: &mut scratch.pending_enter_clip, pending_exit_clip: &mut scratch.pending_exit_clip, + pending_exit_to_depth: &mut scratch.pending_exit_to_depth, active_layer_id: &mut self.active_layer_id, tool_state: &mut self.tool_state, pending_actions: &mut scratch.pending_actions, diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index cfec83d..9edbfb1 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -22,6 +22,22 @@ pub fn is_active(shared: &SharedPaneState) -> bool { !shared.focus.is_none() || !shared.selection.is_empty() } +/// A cheap content-sensitive signature of the current selection, so the shell can detect when the +/// selection *changes* (to re-show a manually-dismissed inspector for the new thing). +pub fn selection_sig(shared: &SharedPaneState) -> u64 { + let mut h: u64 = 0; + for id in shared.selection.clip_instances() { + h ^= (id.as_u128() as u64).wrapping_mul(0x9E3779B97F4A7C15); + } + for f in shared.selection.selected_fills() { + h ^= (f.idx() as u64).wrapping_mul(0xD1B54A32D192ED03); + } + for e in shared.selection.selected_edges() { + h ^= (e.idx() as u64).wrapping_mul(0xA24BAED4963EE407); + } + h +} + /// The stack slot (see `super::STACK`) where the current selection lives, so we can tell if the /// sheet would cover it. Geometry/selection lives on the Stage; clips/layers on the Timeline; etc. #[allow(dead_code)] // selection→pane mapping; kept for reflow/jump heuristics diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 9601894..69c2d3a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -197,6 +197,11 @@ pub struct MobileState { /// Screen-y of the tap that opened the inspector — used to decide whether the tapped thing would /// be hidden by the sheet (only then do we reflow the stack). pub inspector_anchor_y: f32, + /// Set when the user taps outside the sheet to dismiss it; suppresses re-showing until the + /// selection changes. Reset when the selection signature below changes. + pub inspector_dismissed: bool, + /// Cheap signature of the current selection, to detect when it changes (re-show the inspector). + pub inspector_sel_sig: u64, /// Whether the omnibutton radial tool menu is open. pub omni_open: bool, /// Whether the omnibutton "more" grid (all tools) is open. @@ -225,6 +230,8 @@ impl Default for MobileState { inspector_frac: 0.45, inspector_visible: false, inspector_anchor_y: 0.0, + inspector_dismissed: false, + inspector_sel_sig: 0, omni_open: false, omni_grid_open: false, omni_create_open: false, @@ -269,9 +276,16 @@ pub fn render_mobile_shell( // interacted with (pointer down but still selected). let active = inspector::is_active(&rc.shared); let any_down = ui.input(|i| i.pointer.any_down()); + let sel_sig = inspector::selection_sig(&rc.shared); + if sel_sig != state.inspector_sel_sig { + // Selection changed → a fresh thing to inspect; clear any prior manual dismissal. + state.inspector_dismissed = false; + state.inspector_sel_sig = sel_sig; + } if !active { state.inspector_visible = false; - } else if !any_down { + state.inspector_dismissed = false; + } else if !state.inspector_dismissed && !any_down { if !state.inspector_visible { // Just opened (tap released): anchor to the release position for the reflow test below. state.inspector_anchor_y = ui @@ -289,8 +303,9 @@ pub fn render_mobile_shell( }; let mut sheet_top = region.bottom() - sheet_h; - // Tapping outside the sheet (but not on the transport) dismisses the inspector. The press still - // reaches the panes below via stack::render, so tapping another object reselects and reopens it. + // Tapping outside the sheet (but not on the transport) dismisses (hides) the inspector. We only + // hide it — NOT clear the selection — so actions dispatched from overlays (context menu, "+New" + // grid) still see the selection. It stays dismissed until the selection changes (sig above). if inspector_shown { let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); let dismiss = ui.input(|i| { @@ -300,9 +315,8 @@ pub fn render_mobile_shell( }) }); if dismiss { - rc.shared.selection.clear(); - *rc.shared.focus = lightningbeam_core::selection::FocusSelection::None; state.inspector_visible = false; + state.inspector_dismissed = true; inspector_shown = false; sheet_top = region.bottom(); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index ef40988..a345def 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -212,10 +212,15 @@ pub struct SharedPaneState<'a> { pub editing_instance_id: Option, /// The parent layer ID containing the clip instance being edited pub editing_parent_layer_id: Option, + /// The full clip_id path being edited, outermost → current (for breadcrumbs). Empty at root. + pub editing_clip_path: Vec, /// Request to enter a movie clip for editing: (clip_id, instance_id, parent_layer_id) pub pending_enter_clip: &'a mut Option<(uuid::Uuid, uuid::Uuid, uuid::Uuid)>, /// Request to exit the current movie clip pub pending_exit_clip: &'a mut bool, + /// Request to exit up to a specific editing depth (number of clips to keep); e.g. a breadcrumb + /// click. `Some(0)` exits all the way to the document root. + pub pending_exit_to_depth: &'a mut Option, /// Currently active layer ID pub active_layer_id: &'a mut Option, /// Current tool interaction state (mutable for tools to modify) diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index bb4078d..01adcd7 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -4009,8 +4009,9 @@ impl StagePane { // geometry selection/editing there; clip-instance interaction stays available. let inbetween = vector_layer.is_tween_inbetween(*shared.playback_time); - // Double-click: enter/exit movie clip editing - if response.double_clicked() { + // Double-click: enter/exit movie clip editing (desktop; mobile handles double-tap as a + // single tool-independent gesture in handle_input). + if !shared.is_mobile && response.double_clicked() { // Hit test clip instances at the click position let document = shared.action_executor.document(); let clip_hit = hit_test::hit_test_clip_instances( @@ -11445,6 +11446,36 @@ impl StagePane { } } + // P5 gesture (mobile): double-tap semantics, in priority order and regardless of the active + // tool. (double_clicked() fires on double-tap and, on desktop, double-click — so testable.) + // 1. object under the tap → enter it (movie clip or group — go a level deeper) + // 2. empty, inside a clip → exit one level + // 3. empty, at the top level → zoom to fit + // The desktop equivalent lives in handle_select_tool (gated to !is_mobile below). + if shared.is_mobile && response.double_clicked() { + use lightningbeam_core::layer::AnyLayer; + use vello::kurbo::{Affine, Point}; + let point = Point::new(world_pos.x as f64, world_pos.y as f64); + let hit = (*shared.active_layer_id).and_then(|layer_id| { + let document = shared.action_executor.document(); + if let Some(AnyLayer::Vector(vl)) = document.get_layer(&layer_id) { + lightningbeam_core::hit_test::hit_test_clip_instances( + &vl.clip_instances, document, point, Affine::IDENTITY, *shared.playback_time, + ) + .and_then(|iid| vl.clip_instances.iter().find(|c| c.id == iid).map(|c| (c.clip_id, iid, layer_id))) + } else { + None + } + }); + if let Some((clip_id, instance_id, layer_id)) = hit { + *shared.pending_enter_clip = Some((clip_id, instance_id, layer_id)); + } else if shared.editing_clip_id.is_some() { + *shared.pending_exit_clip = true; + } else { + self.zoom_to_fit(shared); + } + } + // P5 gesture (mobile only): long-press on an object opens its actions menu. In the egui fork // `secondary_clicked()` fires on long-press (and right-click) and `context_menu()` opens on // the same — so the menu must be attached UNCONDITIONALLY for egui to register it. Desktop @@ -12865,72 +12896,72 @@ impl PaneRenderer for StagePane { } } - // Show camera info overlay - let info_color = shared.theme.text_color(&["#stage", ".text-secondary"], ui.ctx(), egui::Color32::from_gray(200)); - ui.painter().text( - rect.min + egui::vec2(10.0, 10.0), - egui::Align2::LEFT_TOP, - format!("Vello Stage (zoom: {:.2}, pan: {:.0},{:.0})", - self.zoom, self.pan_offset.x, self.pan_offset.y), - egui::FontId::proportional(14.0), - info_color, - ); - - // Render breadcrumb navigation when inside a movie clip - if shared.editing_clip_id.is_some() { - let document = shared.action_executor.document(); - // Build breadcrumb names from the editing context - // We only have the current clip_id, so show "Scene 1 > ClipName" - let clip_name = shared.editing_clip_id - .and_then(|id| document.get_vector_clip(&id)) - .map(|c| c.name.clone()) - .unwrap_or_else(|| "Unknown".to_string()); - - let breadcrumb_y = rect.min.y + 30.0; - let breadcrumb_x = rect.min.x + 10.0; - - // Background pill - let scene_text = "Scene 1"; - let separator = " > "; - let full_text = format!("{}{}{}", scene_text, separator, clip_name); + // Render breadcrumb navigation showing the full editing path (root → … → current clip). + if !shared.editing_clip_path.is_empty() { let font = egui::FontId::proportional(13.0); - let galley = ui.painter().layout_no_wrap(full_text.clone(), font.clone(), egui::Color32::WHITE); - let text_rect = egui::Rect::from_min_size( - egui::pos2(breadcrumb_x, breadcrumb_y), - galley.size() + egui::vec2(16.0, 8.0), - ); - ui.painter().rect_filled( - text_rect, - 4.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180), - ); + let sep = " > "; + // Segments: "Scene 1" (root) + each clip name. `target` = depth to exit to when clicked + // (root → 0; clip at path index k → k+1); the current (last) level isn't clickable. + let segments: Vec<(String, Option)> = { + let document = shared.action_executor.document(); + let n = shared.editing_clip_path.len(); + let mut segs: Vec<(String, Option)> = vec![("Scene 1".to_string(), Some(0))]; + for (k, cid) in shared.editing_clip_path.iter().enumerate() { + let name = document + .get_vector_clip(cid) + .map(|c| c.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + segs.push((name, if k + 1 == n { None } else { Some(k + 1) })); + } + segs + }; - // "Scene 1" as clickable (exit clip) - let scene_galley = ui.painter().layout_no_wrap( - scene_text.to_string(), font.clone(), egui::Color32::from_rgb(120, 180, 255), - ); - let scene_rect = egui::Rect::from_min_size( - egui::pos2(breadcrumb_x + 8.0, breadcrumb_y + 4.0), - scene_galley.size(), - ); - let scene_response = ui.allocate_rect(scene_rect, egui::Sense::click()); - ui.painter().galley(scene_rect.min, scene_galley, egui::Color32::WHITE); - if scene_response.clicked() { - *shared.pending_exit_clip = true; - } - if scene_response.hovered() { - ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); - } + let bx = rect.min.x + 10.0; + let by = rect.min.y + 10.0; - // Separator + clip name (not clickable, it's the current level) - let rest_text = format!("{}{}", separator, clip_name); - ui.painter().text( - egui::pos2(scene_rect.max.x, breadcrumb_y + 4.0), - egui::Align2::LEFT_TOP, - rest_text, - font, - egui::Color32::WHITE, - ); + // Background pill sized to the whole path. + let full: String = segments + .iter() + .enumerate() + .map(|(i, (s, _))| if i == 0 { s.clone() } else { format!("{sep}{s}") }) + .collect(); + let full_galley = ui.painter().layout_no_wrap(full, font.clone(), egui::Color32::WHITE); + let pill = egui::Rect::from_min_size(egui::pos2(bx, by), full_galley.size() + egui::vec2(16.0, 8.0)); + ui.painter().rect_filled(pill, 4.0, egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180)); + + // Draw segments left → right; clickable ancestors in blue. + let mut x = bx + 8.0; + let y = by + 4.0; + let mut clicked_target: Option = None; + for (i, (name, target)) in segments.iter().enumerate() { + if i > 0 { + let sg = ui.painter().layout_no_wrap(sep.to_string(), font.clone(), egui::Color32::from_gray(160)); + let sw = sg.size().x; + ui.painter().galley(egui::pos2(x, y), sg, egui::Color32::from_gray(160)); + x += sw; + } + let color = if target.is_some() { + egui::Color32::from_rgb(120, 180, 255) + } else { + egui::Color32::WHITE + }; + let g = ui.painter().layout_no_wrap(name.clone(), font.clone(), color); + let seg_rect = egui::Rect::from_min_size(egui::pos2(x, y), g.size()); + if let Some(depth) = target { + let resp = ui.allocate_rect(seg_rect, egui::Sense::click()); + if resp.clicked() { + clicked_target = Some(*depth); + } + if resp.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + } + } + ui.painter().galley(seg_rect.min, g, color); + x += seg_rect.width(); + } + if let Some(depth) = clicked_target { + *shared.pending_exit_to_depth = Some(depth); + } } // Render vector editing overlays (vertices, control points, etc.) From 77eb2c2d33ee2097eb54da7a08067f8b8dd2e934 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 01:15:41 -0400 Subject: [PATCH 22/43] Fix oscillator/synth phase wraparound drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace conditional-subtraction phase wrapping with a single stable wrap: - OscillatorNode: `(phase + freq_mod/sr).rem_euclid(1.0)` — also wraps correctly when FM drives freq_mod negative (the old `if >= 1.0` wouldn't). - SynthVoice: `(phase + frequency/sr).fract()`. Repeated `if phase >= 1.0 { phase -= 1.0 }` accumulates f32 rounding error over long-held notes, drifting the timbre. --- daw-backend/src/audio/node_graph/nodes/oscillator.rs | 10 +++++----- daw-backend/src/effects/synth.rs | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/daw-backend/src/audio/node_graph/nodes/oscillator.rs b/daw-backend/src/audio/node_graph/nodes/oscillator.rs index 243a7bf..769a2b9 100644 --- a/daw-backend/src/audio/node_graph/nodes/oscillator.rs +++ b/daw-backend/src/audio/node_graph/nodes/oscillator.rs @@ -164,11 +164,11 @@ impl AudioNode for OscillatorNode { output[frame * 2] = sample; // Left output[frame * 2 + 1] = sample; // Right - // Update phase once per frame - self.phase += freq_mod / sample_rate_f32; - if self.phase >= 1.0 { - self.phase -= 1.0; - } + // Update phase once per frame. `rem_euclid(1.0)` gives a numerically stable + // wraparound into [0, 1): repeated conditional subtraction accumulates f32 + // rounding error over long-held notes (timbre drift), and FM can drive + // `freq_mod` negative, which a one-sided `if >= 1.0` wouldn't wrap at all. + self.phase = (self.phase + freq_mod / sample_rate_f32).rem_euclid(1.0); } } diff --git a/daw-backend/src/effects/synth.rs b/daw-backend/src/effects/synth.rs index 06e6cea..82a77b9 100644 --- a/daw-backend/src/effects/synth.rs +++ b/daw-backend/src/effects/synth.rs @@ -113,11 +113,10 @@ impl SynthVoice { // Simple sine wave let sample = (self.phase * 2.0 * PI).sin() * (self.velocity as f32 / 127.0) * 0.3; - // Update phase - self.phase += self.frequency / sample_rate; - if self.phase >= 1.0 { - self.phase -= 1.0; - } + // Update phase. Use `.fract()` for a numerically stable wraparound: repeated + // conditional subtraction accumulates f32 rounding error over long-held notes, + // which drifts the timbre. + self.phase = (self.phase + self.frequency / sample_rate).fract(); self.age += 1; From b14972f65799cfda62a6a42a9ebd24b09a59f942 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 05:18:21 -0400 Subject: [PATCH 23/43] P6a: mobile music surface (keyboard-primary instrument pane) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile Piano Roll becomes a unified, keyboard-primary instrument surface (wireframe Plate 04/08): a playable keyboard as the base, revealing a Synthesia-style falling-notes roll above it when the pane is tall enough. - Shared keyboard geometry (panes/keyboard_layout.rs): width-driven, pan-aware pitch->x, so the roll's note columns stay aligned with the Virtual Piano keys. Shared keyboard_octave + keyboard_pan_x in SharedPaneState. - Piano Roll vertical mode (is_mobile): notes as columns falling toward an amber now-line by the keys, tempo-map converted (beats->seconds) so onsets cross the line exactly when they sound. Vertical drag scrubs the timeline; horizontal drag smoothly scrolls the keys (snaps to nearest key on release). Long-press creates a note (drag to size) or resizes an existing one; pan suppresses it. The keyboard is embedded (reuses Virtual Piano render + MIDI); the standalone VirtualPiano stack slot is removed. show_roll is driven by the snapped pane size-class so the keyboard<->roll reveal lands on a stack snap point. - Virtual Piano: renders via the shared layout on mobile, colors playback_notes like pressed keys, gates note-on/glissando to presses that start on the keys (never gating release), and hides QWERTY hints on mobile. - Transport formats by document.timeline_mode (Measures->bar.beat.tick, Frames->MM:SS:FF, Seconds->MM:SS.mmm) — per project type, like desktop. - In-pane instrument header (name + Presets + REC). Recording is driven from the app each frame (not the Timeline pane's render) so REC works regardless of visible panes, and stopping playback stops recording. - Compose/Record intent opens Timeline + instrument pane; mobile central panel is full-bleed (no inner margin) so panes sit flush. - phone-ui-sketches.html: inst-bar moved to the top of the music surface. --- .../lightningbeam-editor/src/main.rs | 49 +- .../lightningbeam-editor/src/mobile/intent.rs | 20 +- .../lightningbeam-editor/src/mobile/mod.rs | 34 +- .../src/mobile/transport.rs | 21 +- .../src/panes/keyboard_layout.rs | 108 +++ .../lightningbeam-editor/src/panes/mod.rs | 14 + .../src/panes/piano_roll.rs | 354 ++++++++ .../src/panes/timeline.rs | 6 +- .../src/panes/virtual_piano.rs | 69 +- phone-ui-sketches.html | 842 ++++++++++++++++++ 10 files changed, 1482 insertions(+), 35 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/keyboard_layout.rs create mode 100644 phone-ui-sketches.html diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index ee2ca62..ca21996 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -976,6 +976,14 @@ struct EditorApp { mobile_state: mobile::MobileState, /// Active mobile long-press context menu (set by a pane, rendered by the shell). mobile_context_menu: Option, + /// Shared keyboard octave offset (C4-relative) for the mobile Virtual Piano + Piano Roll. + keyboard_octave: i8, + /// Shared horizontal keyboard pan (px) for the mobile keyboard + roll. + keyboard_pan_x: f32, + /// Mobile: request to open the instrument Preset Browser (set by the music pane header). + open_instrument_browser: bool, + /// Mobile: request to toggle recording (set by the music pane REC button). + pending_record_toggle: bool, icon_cache: IconCache, tool_icon_cache: ToolIconCache, focus_icon_cache: FocusIconCache, // Focus card icons (start screen) @@ -1338,6 +1346,10 @@ impl EditorApp { mobile_ui_override: None, mobile_state: mobile::MobileState::default(), mobile_context_menu: None, + keyboard_octave: 0, + keyboard_pan_x: 0.0, + open_instrument_browser: false, + pending_record_toggle: false, icon_cache: IconCache::new(), tool_icon_cache: ToolIconCache::new(), focus_icon_cache: FocusIconCache::new(), @@ -6608,7 +6620,16 @@ impl eframe::App for EditorApp { { scratch.synthetic_input = test_mode_replay.synthetic_input; } - egui::CentralPanel::default().show(ctx, |ui| { + // On mobile the shell is full-bleed to the window edges (it paints its own background), so + // drop the central panel's default inner margin — otherwise every pane is inset by it while + // the unclipped keyboard overdraws into it, leaving an inconsistent gutter. + let central = egui::CentralPanel::default(); + let central = if self.mobile_active() { + central.frame(egui::Frame::default()) + } else { + central + }; + central.show(ctx, |ui| { let available_rect = ui.available_rect_before_wrap(); // Reset hovered divider each frame @@ -6668,6 +6689,27 @@ impl eframe::App for EditorApp { &mut bundle.rc, bundle.mobile_state, ); + + // Drive recording from the application, not the Timeline pane's render pass, so a + // REC request (from the music pane header) and count-in progression work regardless + // of whether the Timeline is currently visible. The Timeline pane still owns the + // recording *state*; we just tick it here each frame. + let rc = &mut bundle.rc; + for pane in rc.pane_instances.values_mut() { + if let PaneInstance::Timeline(t) = pane { + t.check_pending_recording_start(&mut rc.shared); + if *rc.shared.pending_record_toggle { + *rc.shared.pending_record_toggle = false; + t.toggle_recording(&mut rc.shared); + } + // Stopping playback stops recording (stop_recording clears is_recording, + // so this fires once). + if *rc.shared.is_recording && !*rc.shared.is_playing { + t.stop_recording(&mut rc.shared); + } + break; + } + } } else { render_layout_node( ui, @@ -7221,6 +7263,11 @@ impl EditorApp { rc: RenderContext { shared: panes::SharedPaneState { is_mobile, + keyboard_octave: &mut self.keyboard_octave, + keyboard_pan_x: &mut self.keyboard_pan_x, + instrument_show_roll: false, + open_instrument_browser: &mut self.open_instrument_browser, + pending_record_toggle: &mut self.pending_record_toggle, mobile_context_menu: &mut self.mobile_context_menu, container_path: self.current_file_path.clone(), onion: { diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs index 911dd07..590bdf4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs @@ -19,18 +19,24 @@ struct Intent { focus: usize, /// Initial mobile stack window: (window_top, window_count). window: (usize, usize), + /// Initial pane weights (relative heights) for the window. + weights: [f32; 3], } fn intents(pal: &Palette) -> [Intent; 6] { let [coral, cyan, amber, pink, violet] = pal.accents; + let even = [1.0, 1.0, 1.0]; + // Compose/Record: compressed Timeline ribbon on top; the tall Piano Roll (which now hosts the + // instrument header + keyboard as one surface) fills the rest. + let music = [0.4, 1.6, 1.0]; [ // Stage indices (see super::STACK): Stage=2, Timeline=3, PianoRoll=4, VirtualPiano=5. - Intent { label: "Draw", icon: icons::BRUSH, accent: coral, focus: 5, window: (2, 1) }, - Intent { label: "Animate", icon: icons::FILM, accent: cyan, focus: 0, window: (2, 2) }, - Intent { label: "Compose", icon: icons::MUSIC, accent: amber, focus: 2, window: (3, 3) }, - Intent { label: "Record", icon: icons::MIC, accent: pink, focus: 2, window: (3, 3) }, - Intent { label: "Edit video", icon: icons::CLAPPERBOARD, accent: violet, focus: 1, window: (2, 2) }, - Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: pal.text_dim, focus: 0, window: (2, 2) }, + Intent { label: "Draw", icon: icons::BRUSH, accent: coral, focus: 5, window: (2, 1), weights: even }, + Intent { label: "Animate", icon: icons::FILM, accent: cyan, focus: 0, window: (2, 2), weights: even }, + Intent { label: "Compose", icon: icons::MUSIC, accent: amber, focus: 2, window: (3, 2), weights: music }, + Intent { label: "Record", icon: icons::MIC, accent: pink, focus: 2, window: (3, 2), weights: music }, + Intent { label: "Edit video", icon: icons::CLAPPERBOARD, accent: violet, focus: 1, window: (2, 2), weights: even }, + Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: pal.text_dim, focus: 0, window: (2, 2), weights: even }, ] } @@ -92,7 +98,7 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { app.create_new_project_with_focus(intent.focus); app.mobile_state.window_top = intent.window.0; app.mobile_state.window_count = intent.window.1; - app.mobile_state.weights = [1.0, 1.0, 1.0]; + app.mobile_state.weights = intent.weights; } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 69c2d3a..56cc7e7 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -89,21 +89,21 @@ pub enum StackPane { AssetLibrary, Stage, Timeline, + /// The instrument surface: keyboard-primary on mobile, hosting the falling-notes roll above the + /// keys (the standalone Virtual Piano is embedded here, so it isn't a separate stack slot). PianoRoll, - VirtualPiano, /// Node editor, or (toggled) the instrument/preset browser. NodeInstrument, ScriptEditor, } /// The stack order. Index into this is a pane's stable slot id. -pub const STACK: [StackPane; 8] = [ +pub const STACK: [StackPane; 7] = [ StackPane::Outliner, StackPane::AssetLibrary, StackPane::Stage, StackPane::Timeline, StackPane::PianoRoll, - StackPane::VirtualPiano, StackPane::NodeInstrument, StackPane::ScriptEditor, ]; @@ -118,7 +118,6 @@ impl StackPane { StackPane::Stage => PaneType::Stage, StackPane::Timeline => PaneType::Timeline, StackPane::PianoRoll => PaneType::PianoRoll, - StackPane::VirtualPiano => PaneType::VirtualPiano, StackPane::NodeInstrument => { if show_instruments { PaneType::PresetBrowser @@ -136,8 +135,7 @@ impl StackPane { StackPane::AssetLibrary => "Assets", StackPane::Stage => "Stage", StackPane::Timeline => "Timeline", - StackPane::PianoRoll => "Piano Roll", - StackPane::VirtualPiano => "Keys", + StackPane::PianoRoll => "Instrument", StackPane::NodeInstrument => { if show_instruments { "Instruments" @@ -332,6 +330,20 @@ pub fn render_mobile_shell( region }; + // Decide whether the instrument pane (PianoRoll = STACK index 4) reveals the roll, from the + // *committed* (snapped) window weights — so the keyboard↔roll transition lands on a stack snap + // rather than at an arbitrary mid-drag height. Roll shows once the pane is past the smallest snap. + rc.shared.instrument_show_roll = { + let idx = 4i32 - state.window_top as i32; + if idx >= 0 && (idx as usize) < state.window_count { + let sum: f32 = state.weights[..state.window_count].iter().map(|w| w.max(0.0)).sum(); + let w = if sum > 0.0 { state.weights[idx as usize].max(0.0) / sum } else { 0.0 }; + w > 0.30 // 0.25 preset ⇒ keyboard only; 0.33/0.5/0.75 ⇒ keyboard + roll + } else { + true + } + }; + stack::render(ui, stack_rect, rc, state, &pal); if inspector_shown { @@ -345,6 +357,16 @@ pub fn render_mobile_shell( // Omnibutton FAB (radial tool menu) — drawn above the stack region, on top of everything else. omni::render(ui, region, rc, state, &pal); + // Instrument-browser request from the music pane's header → show the Preset Browser fullscreen. + if *rc.shared.open_instrument_browser { + *rc.shared.open_instrument_browser = false; + state.show_instruments = true; + state.window_top = 5; // Node/Instrument band (PresetBrowser when show_instruments) + state.window_count = 1; + state.weights = [1.0, 1.0, 1.0]; + state.anim = None; + } + // Top bar (filename + ⌕ palette + ⋯ commands). Its menus overlay the whole shell, so it's last. topbar::render(ui, topbar_rect, available_rect, rc, state, &pal); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs index 87f0e5a..8b4bc1b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/transport.rs @@ -40,9 +40,24 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState, } } - // --- Timecode (MM:SS:FF) --- - let fps = shared.action_executor.document().framerate.max(1.0); - let tc = format_timecode(*shared.playback_time, fps); + // --- Playhead readout: format follows the project's TimelineMode (set at creation by type). --- + let t = *shared.playback_time; + let tc = { + use lightningbeam_core::document::TimelineMode; + let doc = shared.action_executor.document(); + match doc.timeline_mode { + TimelineMode::Measures => { + let pos = lightningbeam_core::beat_time::time_to_measure(t, doc.tempo_map(), &doc.time_signature); + format!("{}.{}.{:02}", pos.measure, pos.beat, pos.tick / 10) + } + TimelineMode::Frames => format_timecode(t, doc.framerate.max(1.0)), + TimelineMode::Seconds => { + let total = t.max(0.0); + let m = (total / 60.0).floor() as u32; + format!("{:02}:{:06.3}", m, total % 60.0) + } + } + }; let tc_left = btn_rect.right() + 12.0; painter.text( egui::pos2(tc_left, cy), diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/keyboard_layout.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/keyboard_layout.rs new file mode 100644 index 0000000..038665d --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/keyboard_layout.rs @@ -0,0 +1,108 @@ +//! Shared horizontal keyboard geometry (pitch → x), used by both the Virtual Piano and the mobile +//! portrait "Synthesia" Piano Roll so their columns line up with the keys. It is **width-driven** +//! (key width from the pane width, not its height) and supports a smooth horizontal **pan** (pixels) +//! so the roll and keyboard scroll together and stay aligned. + +/// Number of white keys strictly before each pitch-class within its octave. +const WHITES_BEFORE: [i32; 12] = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6]; +/// White pitch-classes, indexed by white-key-within-octave. +const WHITE_PC: [i32; 7] = [0, 2, 4, 5, 7, 9, 11]; +/// Target on-screen white-key width (px); the visible white-key count approximates it. +const TARGET_WHITE_KEY_W: f32 = 40.0; + +/// Absolute white-key index of a note, counting white keys from MIDI 0. +fn awi(note: u8) -> i32 { + (note as i32 / 12) * 7 + WHITES_BEFORE[(note % 12) as usize] +} + +/// The white note at a given absolute white-key index. +fn white_note_from_awi(a: i32) -> u8 { + let oct = a.div_euclid(7); + let rem = a.rem_euclid(7) as usize; + (oct * 12 + WHITE_PC[rem]).clamp(0, 127) as u8 +} + +/// A horizontal piano key layout for a pane width, octave center, and horizontal pan. +#[derive(Clone, Copy)] +pub struct KeyboardLayout { + pub white_key_width: f32, + pub black_key_width: f32, + /// `note_x(white n) == base_x + awi(n) * white_key_width`. + base_x: f32, + rect_left: f32, + rect_width: f32, +} + +impl KeyboardLayout { + pub fn is_black_key(note: u8) -> bool { + matches!(note % 12, 1 | 3 | 6 | 8 | 10) + } + + /// Build a layout. `pan_x` shifts everything right (drag-right reveals lower keys); at `pan_x == 0` + /// the octave-center key sits in the middle of the pane. + pub fn from_width(origin_x: f32, width: f32, octave: i8, pan_x: f32) -> Self { + let vis = ((width / TARGET_WHITE_KEY_W).round() as u32).clamp(7, 24) as f32; + let white_key_width = width / vis; + let black_key_width = white_key_width * 0.6; + let center = (60 + octave as i32 * 12).clamp(0, 127) as u8; + let base_x = origin_x + width / 2.0 - (awi(center) as f32 + 0.5) * white_key_width + pan_x; + Self { white_key_width, black_key_width, base_x, rect_left: origin_x, rect_width: width } + } + + /// Snap a pan offset to the nearest whole key (for release-snap). + pub fn snap_pan(&self, pan_x: f32) -> f32 { + (pan_x / self.white_key_width).round() * self.white_key_width + } + + /// Left x of a key's rect (black keys straddle the white boundary). + pub fn note_x(&self, note: u8) -> f32 { + let x = self.base_x + awi(note) as f32 * self.white_key_width; + if Self::is_black_key(note) { + x - self.black_key_width / 2.0 + } else { + x + } + } + + pub fn note_width(&self, note: u8) -> f32 { + if Self::is_black_key(note) { + self.black_key_width + } else { + self.white_key_width + } + } + + pub fn note_center_x(&self, note: u8) -> f32 { + self.note_x(note) + self.note_width(note) / 2.0 + } + + /// Leftmost visible white key (with one key of margin). + pub fn first_visible_white(&self) -> u8 { + let a = ((self.rect_left - self.base_x) / self.white_key_width).floor() as i32 - 1; + white_note_from_awi(a.max(0)) + } + + /// Rightmost visible note (with one key of margin). + pub fn last_visible_note(&self) -> u8 { + let a = ((self.rect_left + self.rect_width - self.base_x) / self.white_key_width).ceil() as i32 + 1; + white_note_from_awi(a).min(127) + } + + pub fn visible_notes(&self) -> std::ops::RangeInclusive { + self.first_visible_white()..=self.last_visible_note() + } + + /// Which key is at screen x (black keys, drawn on top, take precedence). + pub fn x_to_note(&self, x: f32) -> u8 { + for note in self.visible_notes() { + if Self::is_black_key(note) { + let c = self.note_center_x(note); + if (x - c).abs() <= self.black_key_width / 2.0 { + return note; + } + } + } + let a = ((x - self.base_x) / self.white_key_width).floor() as i32; + white_note_from_awi(a.max(0)).min(127) + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index a345def..fdca5ce 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -72,6 +72,7 @@ pub mod gradient_editor; pub mod timeline; pub mod infopanel; pub mod outliner; +pub mod keyboard_layout; pub mod piano_roll; pub mod virtual_piano; pub mod node_editor; @@ -363,6 +364,19 @@ pub struct SharedPaneState<'a> { pub brush_preview_pixels: &'a std::sync::Arc)>>>, /// True when rendering the phone/mobile shell (panes can render more compactly). pub is_mobile: bool, + /// Shared keyboard octave offset (C4-relative), so the mobile Virtual Piano and the portrait + /// Piano Roll agree on which keys are visible and stay column-aligned. + pub keyboard_octave: &'a mut i8, + /// Shared horizontal keyboard pan (px) for smooth left/right scroll of the mobile keyboard+roll. + pub keyboard_pan_x: &'a mut f32, + /// Whether the mobile instrument pane should show the falling-notes roll above the keys. Driven + /// by the shell from the *snapped* pane size-class so the reveal happens at a stack snap point. + pub instrument_show_roll: bool, + /// Set by the mobile instrument header's "Presets" button; the shell opens the Preset Browser. + pub open_instrument_browser: &'a mut bool, + /// Set by the mobile instrument header's REC button; the Timeline pane picks it up and toggles + /// recording (reusing its full count-in / clip-creation flow). + pub pending_record_toggle: &'a mut bool, /// Mobile long-press context menu request. A pane sets this on `response.secondary_clicked()` /// (which fires on long-press) with the items relevant to what was pressed; the mobile shell /// renders one persistent popup and dispatches the chosen `MenuAction`. `None` = no menu. diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs index 64e5390..19ddd0b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs @@ -169,6 +169,18 @@ pub struct PianoRollPane { snap_value: SnapValue, last_snap_selection: HashSet, snap_user_changed: bool, // set in render_header, consumed before handle_input + + // Mobile portrait "Synthesia" mode: the playable keyboard is drawn as a strip at the bottom of + // this pane (one unified surface), reusing the Virtual Piano's rendering + MIDI logic. + keyboard: super::virtual_piano::VirtualPianoPane, + // Manual long-press detection for the vertical roll (works with mouse-hold and touch): time the + // press started, and where. `None` = disarmed (no fresh press, or the press became a pan). + lp_press_time: Option, + lp_press_pos: egui::Pos2, + // When the long-press landed on an existing note, we resize it instead of creating: its index in + // the resolved-note list and its original duration (to compute the resize delta on commit). + editing_index: Option, + editing_orig_dur: f64, } impl PianoRollPane { @@ -205,6 +217,11 @@ impl PianoRollPane { snap_value: SnapValue::None, last_snap_selection: HashSet::new(), snap_user_changed: false, + keyboard: super::virtual_piano::VirtualPianoPane::new(), + lp_press_time: None, + lp_press_pos: egui::Pos2::ZERO, + editing_index: None, + editing_orig_dur: 0.0, } } @@ -444,6 +461,13 @@ impl PianoRollPane { } } + // Mobile: keyboard-primary "Synthesia" instrument surface (keys + falling-notes roll). Always + // used on mobile (device is portrait; the landscape/conventional flip is P7). + if shared.is_mobile { + self.render_vertical_mode(ui, rect, shared, &clip_data); + return; + } + // Apply quantize if the user changed the snap dropdown (must happen before handle_input // which may clear the selection when the ComboBox click propagates to the grid). if self.snap_user_changed { @@ -556,6 +580,336 @@ impl PianoRollPane { self.render_keyboard(&painter, keyboard_rect); } + /// Portrait mobile "Synthesia" view — ONE unified instrument surface: an instrument header on + /// top, then falling notes as vertical columns (pitch on X, aligned to the keys via the shared + /// `KeyboardLayout`, time falling toward the "now" line), then the playable keyboard strip at the + /// bottom (reusing the Virtual Piano). Tapping a column adds a note. + fn render_vertical_mode( + &mut self, + ui: &mut egui::Ui, + rect: Rect, + shared: &mut SharedPaneState, + clip_data: &[(u32, f64, f64, f64, Uuid)], + ) { + use super::keyboard_layout::KeyboardLayout; + + // Keyboard-primary layout (portrait, per wireframe Plate 08): the playable keyboard is the + // base surface, capped at an ergonomic height. Whether the falling-notes roll appears above + // is decided by the shell from the *snapped* pane size-class, so the reveal lands on a stack + // snap point (not mid-drag). Short/smallest snap ⇒ keyboard only. + const KEY_CAP: f32 = 170.0; + let header_h = 32.0; + let header_rect = Rect::from_min_max(rect.min, pos2(rect.right(), rect.top() + header_h)); + let content_top = header_rect.bottom(); + let content_h = (rect.bottom() - content_top).max(0.0); + let show_roll = shared.instrument_show_roll && content_h > KEY_CAP + 40.0; + let kb_h = if show_roll { content_h.min(KEY_CAP) } else { content_h }; + let kb_rect = Rect::from_min_max(pos2(rect.left(), rect.bottom() - kb_h), rect.max); + let roll_rect = Rect::from_min_max(pos2(rect.left(), content_top), pos2(rect.right(), kb_rect.top())); + + let pps = self.pixels_per_second; // pixels per second (vertical waterfall) + let now_y = roll_rect.bottom(); // a note reaches the keys at its onset time + let now = ui.input(|i| i.time); + + // Auto-release a long-press-added preview note once its short audition elapses (the normal + // release check lives in handle_input, which the vertical mode bypasses). + if let Some(dur) = self.preview_duration { + if self.preview_note_sounding && now - self.preview_start_time >= dur { + self.preview_note_off(shared); + } + } + + // Handle roll gestures BEFORE building the layout / drawing, so the roll and the keyboard both + // use the updated pan + playhead this same frame (no 1-frame follow lag between them). + let resp = if show_roll { + let r = ui.interact(roll_rect, ui.id().with("pr_vertical"), egui::Sense::click_and_drag()); + if self.creating_note.is_none() && r.dragged() { + let d = r.drag_delta(); + if d.y.abs() > 0.0 { + // Drag down ⇒ advance time (content follows the finger). + let nt = (*shared.playback_time + (d.y / pps) as f64).max(0.0); + *shared.playback_time = nt; + if let Some(ctrl) = shared.audio_controller.as_ref() { + if let Ok(mut c) = ctrl.lock() { + c.seek(nt); + } + } + } + *shared.keyboard_pan_x += d.x; + } + Some(r) + } else { + None + }; + + let layout = KeyboardLayout::from_width(rect.min.x, rect.width(), *shared.keyboard_octave, *shared.keyboard_pan_x); + // Note times are in beats; the playhead is in seconds — convert via the tempo map so the + // waterfall lines up with real playback (onset crosses the now line exactly when it sounds). + let tempo_map = shared.action_executor.document().tempo_map().clone(); + let playhead = *shared.playback_time; // seconds + + if let Some(resp) = resp { + let painter = ui.painter_at(roll_rect); + let bg = shared.theme.bg_color(&["#piano-roll", ".pane-content"], ui.ctx(), Color32::from_rgb(30, 30, 35)); + painter.rect_filled(roll_rect, 0.0, bg); + + // Column guides aligned to the keys: black-key columns tinted, white-key boundaries lined. + for note in layout.visible_notes() { + let x = layout.note_x(note); + if KeyboardLayout::is_black_key(note) { + let w = layout.note_width(note); + painter.rect_filled( + Rect::from_min_size(pos2(x, roll_rect.min.y), vec2(w, roll_rect.height())), + 0.0, + Color32::from_rgba_unmultiplied(0, 0, 0, 40), + ); + } else { + painter.line_segment([pos2(x, roll_rect.min.y), pos2(x, roll_rect.max.y)], Stroke::new(1.0, Color32::from_gray(55))); + } + } + + // Notes of the selected clip, falling toward the now line. + if let Some(clip_id) = self.selected_clip_id { + if let Some(&(_, timeline_start, trim_start, duration, _)) = + clip_data.iter().find(|c| c.0 == clip_id) + { + if let Some(events) = shared.midi_event_cache.get(&clip_id) { + let resolved = Self::resolve_notes(events); + for (ni, note) in resolved.iter().enumerate() { + // The note being resized is drawn separately (live) as the temp note. + if Some(ni) == self.editing_index { + continue; + } + if note.start_time + note.duration <= trim_start + || note.start_time >= trim_start + duration + { + continue; + } + let global = timeline_start + (note.start_time - trim_start); + let y_on = now_y - ((tempo_map.transform(global) - playhead) as f32) * pps; + let y_off = now_y - ((tempo_map.transform(global + note.duration) - playhead) as f32) * pps; + let (top, bot) = (y_on.min(y_off), y_on.max(y_off)); + if bot < roll_rect.min.y || top > roll_rect.max.y { + continue; + } + let x = layout.note_x(note.note); + let w = layout.note_width(note.note).max(3.0); + let bright = 0.35 + (note.velocity as f32 / 127.0) * 0.65; + let col = Color32::from_rgb( + (111.0 * bright) as u8, + (220.0 * bright) as u8, + (111.0 * bright) as u8, + ); + painter.rect_filled( + Rect::from_min_max( + pos2(x + 1.0, top.max(roll_rect.min.y)), + pos2(x + w - 1.0, bot.min(roll_rect.max.y)), + ), + 2.0, + col, + ); + } + } + } + } + + // The note currently being created (long-press to start, drag along Y to size). + if let Some(temp) = self.creating_note.as_ref() { + if let Some(&(_, ts, trim, _d, _)) = + clip_data.iter().find(|c| Some(c.0) == self.selected_clip_id) + { + let g = ts + (temp.start_time - trim); + let y_on = now_y - ((tempo_map.transform(g) - playhead) as f32) * pps; + let y_off = now_y - ((tempo_map.transform(g + temp.duration) - playhead) as f32) * pps; + let x = layout.note_x(temp.note); + let w = layout.note_width(temp.note).max(3.0); + painter.rect_filled( + Rect::from_min_max(pos2(x + 1.0, y_on.min(y_off)), pos2(x + w - 1.0, y_on.max(y_off))), + 2.0, + Color32::from_rgb(150, 255, 150), + ); + } + } + + // The amber "now" line at the boundary with the keyboard. + painter.line_segment( + [pos2(roll_rect.min.x, now_y), pos2(roll_rect.max.x, now_y)], + Stroke::new(2.0, Color32::from_rgb(0xf4, 0xa3, 0x40)), + ); + + const LONG_PRESS_SECS: f64 = 0.4; + let pressed = ui.input(|i| i.pointer.primary_pressed()); + let down = ui.input(|i| i.pointer.any_down()); + let ptr = ui.input(|i| i.pointer.latest_pos()); + + if self.creating_note.is_some() { + // Sizing the note (new or existing): drag along Y sets its duration; release commits. + if down { + if let (Some(p), Some(&(_, ts, trim, _d, _))) = ( + ptr, + clip_data.iter().find(|c| Some(c.0) == self.selected_clip_id), + ) { + let end_sec = playhead + ((now_y - p.y) / pps) as f64; + let end_beats = tempo_map.inverse_transform(end_sec).max(0.0); + let end_local = trim + (end_beats - ts); + if let Some(t) = self.creating_note.as_mut() { + t.duration = (end_local - t.start_time).max(0.25); + } + } + } else if let (Some(temp), Some(clip_id)) = + (self.creating_note.take(), self.selected_clip_id) + { + if let Some(idx) = self.editing_index.take() { + let dt = temp.duration - self.editing_orig_dur; + self.commit_resize_note(clip_id, idx, dt, shared, clip_data); + } else { + self.commit_create_note(clip_id, temp, shared, clip_data); + } + } + } else { + // Snap the pan to the nearest key on release. + if resp.drag_stopped() { + *shared.keyboard_pan_x = layout.snap_pan(*shared.keyboard_pan_x); + } + // Manual long-press (mouse-hold or touch): armed only on a *fresh* press, and cancelled + // for the rest of that press once it moves (→ pan), so pausing mid-pan won't fire. + if pressed { + match ptr { + Some(p) if roll_rect.contains(p) => { + self.lp_press_time = Some(now); + self.lp_press_pos = p; + } + _ => self.lp_press_time = None, + } + } + if !down { + self.lp_press_time = None; + } + if let Some(t0) = self.lp_press_time { + let moved = ptr.map_or(0.0, |p| (p - self.lp_press_pos).length()); + if moved > 8.0 { + self.lp_press_time = None; // became a pan → suppress for this press + } else if now - t0 >= LONG_PRESS_SECS { + self.lp_press_time = None; + let pos = self.lp_press_pos; + if let Some(clip_id) = self.selected_clip_id { + if let Some(&(_, timeline_start, trim_start, clip_dur, _)) = + clip_data.iter().find(|c| c.0 == clip_id) + { + let pitch = layout.x_to_note(pos.x); + // Hit-test an existing note in this column → resize it; else create. + let mut hit = None; + if let Some(events) = shared.midi_event_cache.get(&clip_id) { + for (i, n) in Self::resolve_notes(events).iter().enumerate() { + if n.note != pitch + || n.start_time < trim_start + || n.start_time >= trim_start + clip_dur + { + continue; + } + let g = timeline_start + (n.start_time - trim_start); + let y_on = now_y - ((tempo_map.transform(g) - playhead) as f32) * pps; + let y_off = now_y - ((tempo_map.transform(g + n.duration) - playhead) as f32) * pps; + if pos.y >= y_on.min(y_off) && pos.y <= y_on.max(y_off) { + hit = Some((i, n.start_time, n.duration, n.velocity)); + break; + } + } + } + if let Some((idx, onset, dur, vel)) = hit { + self.editing_index = Some(idx); + self.editing_orig_dur = dur; + self.creating_note = Some(TempNote { note: pitch, start_time: onset, duration: dur, velocity: vel }); + } else { + let onset_sec = playhead + ((now_y - pos.y) / pps) as f64; + let onset_beats = tempo_map.inverse_transform(onset_sec).max(0.0); + let onset_local = snap_to_value((trim_start + (onset_beats - timeline_start)).max(0.0), self.snap_value, &tempo_map); + self.editing_index = None; + self.creating_note = Some(TempNote { note: pitch, start_time: onset_local, duration: 0.5, velocity: DEFAULT_VELOCITY }); + } + self.preview_note_on(pitch, DEFAULT_VELOCITY, Some(0.4), now, shared); + } + } + } + } + } + } + + // Notes sounding at the playhead → drive the keyboard's highlights so it colors them exactly + // like pressed keys (full key, correct white/black layering) rather than an after-overlay. + let mut sounding = std::collections::HashSet::new(); + if let Some(clip_id) = self.selected_clip_id { + if let Some(&(_, timeline_start, trim_start, duration, _)) = + clip_data.iter().find(|c| c.0 == clip_id) + { + if let Some(events) = shared.midi_event_cache.get(&clip_id) { + for note in Self::resolve_notes(events) { + if note.start_time < trim_start || note.start_time >= trim_start + duration { + continue; + } + let global = timeline_start + (note.start_time - trim_start); + let s = tempo_map.transform(global); + let e = tempo_map.transform(global + note.duration); + if playhead >= s && playhead < e { + sounding.insert(note.note); + } + } + } + } + } + self.keyboard.playback_notes = sounding; + + // Instrument header (in this pane's own header) + the always-present playable keyboard strip. + self.render_instrument_header(ui, header_rect, shared); + let kb_path: NodePath = Vec::new(); + self.keyboard.render_content(ui, kb_rect, &kb_path, shared); + } + + /// The in-pane instrument header: active track name + Presets + REC. + fn render_instrument_header(&mut self, ui: &mut egui::Ui, rect: Rect, shared: &mut SharedPaneState) { + let painter = ui.painter_at(rect); + let hdr = shared.theme.bg_color(&[".pane-header"], ui.ctx(), Color32::from_rgb(24, 24, 28)); + painter.rect_filled(rect, 0.0, hdr); + painter.line_segment( + [pos2(rect.left(), rect.bottom()), pos2(rect.right(), rect.bottom())], + Stroke::new(1.0, Color32::from_gray(60)), + ); + + let name = shared + .active_layer_id + .and_then(|id| shared.action_executor.document().get_layer(&id)) + .map(|l| l.name().to_string()) + .unwrap_or_else(|| "No instrument".to_string()); + painter.text( + pos2(rect.left() + 12.0, rect.center().y), + egui::Align2::LEFT_CENTER, + name, + egui::FontId::proportional(14.0), + Color32::from_gray(220), + ); + + // REC (rightmost). + let rec = Rect::from_min_max(pos2(rect.right() - 64.0, rect.top() + 4.0), pos2(rect.right() - 8.0, rect.bottom() - 4.0)); + let rec_resp = ui.interact(rec, ui.id().with("pr_rec"), egui::Sense::click()); + let recording = *shared.is_recording; + let rec_bg = if recording { Color32::from_rgb(0xe0, 0x3a, 0x3a) } else { Color32::from_gray(66) }; + painter.rect_filled(rec, 6.0, rec_bg); + painter.circle_filled(pos2(rec.left() + 13.0, rec.center().y), 4.0, Color32::from_rgb(255, 96, 96)); + painter.text(pos2(rec.left() + 23.0, rec.center().y), egui::Align2::LEFT_CENTER, "REC", egui::FontId::proportional(12.0), Color32::WHITE); + if rec_resp.clicked() { + *shared.pending_record_toggle = true; + } + + // Presets (left of REC). + let pre = Rect::from_min_max(pos2(rec.left() - 84.0, rect.top() + 4.0), pos2(rec.left() - 8.0, rect.bottom() - 4.0)); + let pre_resp = ui.interact(pre, ui.id().with("pr_presets"), egui::Sense::click()); + painter.rect_filled(pre, 6.0, if pre_resp.hovered() { Color32::from_gray(56) } else { Color32::from_gray(42) }); + painter.text(pre.center(), egui::Align2::CENTER_CENTER, "Presets", egui::FontId::proportional(12.0), Color32::from_gray(220)); + if pre_resp.clicked() { + *shared.open_instrument_browser = true; + } + } + fn render_keyboard(&self, painter: &egui::Painter, rect: Rect) { // Background painter.rect_filled(rect, 0.0, Color32::from_rgb(40, 40, 45)); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index d04028a..663678b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -951,7 +951,7 @@ impl TimelinePane { /// Toggle recording on/off /// In Auto mode, records to the active layer (audio or video with camera) - fn toggle_recording(&mut self, shared: &mut SharedPaneState) { + pub(crate) fn toggle_recording(&mut self, shared: &mut SharedPaneState) { if *shared.is_recording { // Stop recording self.stop_recording(shared); @@ -1212,7 +1212,7 @@ impl TimelinePane { /// Stop all active recordings /// Called every frame; fires deferred recording commands once the count-in pre-roll ends. - fn check_pending_recording_start(&mut self, shared: &mut SharedPaneState) { + pub(crate) fn check_pending_recording_start(&mut self, shared: &mut SharedPaneState) { let Some(ref pending) = self.pending_recording_start else { return }; if !*shared.is_playing || *shared.playback_time < pending.trigger_time { return; @@ -1232,7 +1232,7 @@ impl TimelinePane { *shared.count_in_enabled = saved_count_in; } - fn stop_recording(&mut self, shared: &mut SharedPaneState) { + pub(crate) fn stop_recording(&mut self, shared: &mut SharedPaneState) { // Cancel any in-progress count-in self.pending_recording_start = None; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/virtual_piano.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/virtual_piano.rs index 3ab982c..1bcba15 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/virtual_piano.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/virtual_piano.rs @@ -33,6 +33,8 @@ pub struct VirtualPianoPane { sustain_active: bool, /// Notes being held by sustain pedal (not by active key/mouse press) sustained_notes: HashSet, + /// Externally-driven highlights (e.g. notes sounding during playback), colored like pressed keys. + pub playback_notes: HashSet, } impl Default for VirtualPianoPane { @@ -83,6 +85,7 @@ impl VirtualPianoPane { note_to_key_map, sustain_active: false, sustained_notes: HashSet::new(), + playback_notes: HashSet::new(), } } @@ -216,9 +219,17 @@ impl VirtualPianoPane { /// Render the piano keyboard fn render_keyboard(&mut self, ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState) { - // Calculate visible range and key dimensions based on pane size - let (visible_start, visible_end, white_key_width, offset_x) = - self.calculate_visible_range(rect.width(), rect.height()); + // Calculate visible range and key dimensions based on pane size. On mobile use the shared + // width-driven layout (so the portrait Piano Roll's columns line up with these keys). + let (visible_start, visible_end, white_key_width, offset_x) = if shared.is_mobile { + let layout = super::keyboard_layout::KeyboardLayout::from_width( + rect.min.x, rect.width(), self.octave_offset, *shared.keyboard_pan_x, + ); + let vs = layout.first_visible_white(); + (vs, layout.last_visible_note(), layout.white_key_width, layout.note_x(vs) - rect.min.x) + } else { + self.calculate_visible_range(rect.width(), rect.height()) + }; let white_key_height = rect.height(); let black_key_width = white_key_width * self.black_key_width_ratio; @@ -238,6 +249,11 @@ impl VirtualPianoPane { return; } + // Only play notes when the press STARTED on the keyboard, so dragging in from the roll above + // (mobile) doesn't trigger notes. On desktop the whole pane is the keyboard, so a click here + // always qualifies. + let started_here = ui.input(|i| i.pointer.press_origin().map_or(false, |p| rect.contains(p))); + // Count white keys before each note for positioning let mut white_key_positions: std::collections::HashMap = std::collections::HashMap::new(); let mut white_count = 0; @@ -303,7 +319,8 @@ impl VirtualPianoPane { }); let pointer_down = ui.input(|i| i.pointer.primary_down()); let is_pressed = self.pressed_notes.contains(¬e) || - (!black_key_interacted && pointer_over_key && pointer_down); + self.playback_notes.contains(¬e) || + (started_here && !black_key_interacted && pointer_over_key && pointer_down); let color = if is_pressed { shared.theme.bg_color(&[".piano-white-key", ".pressed"], ui.ctx(), egui::Color32::from_rgb(100, 150, 255)) } else { @@ -320,8 +337,9 @@ impl VirtualPianoPane { ); if !black_key_interacted { - // Mouse down starts note (detect primary button pressed on this key) - if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) { + // Mouse down starts note (detect primary button pressed on this key). Gated on + // `started_here` so a drag that began on the roll above doesn't start notes. + if started_here && pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) { // Calculate velocity based on mouse Y position let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y; let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect); @@ -330,7 +348,8 @@ impl VirtualPianoPane { self.dragging_note = Some(note); } - // Mouse up stops note (detect primary button released) + // Mouse up stops note (detect primary button released). NEVER gated — a held note + // must always release. if ui.input(|i| i.pointer.primary_released()) { if self.dragging_note == Some(note) { self.send_note_off(note, shared); @@ -339,7 +358,7 @@ impl VirtualPianoPane { } // Dragging over a new key (pointer is down and over a different key) - if pointer_over_key && pointer_down { + if started_here && pointer_over_key && pointer_down { if self.dragging_note != Some(note) { // Stop previous note if let Some(prev_note) = self.dragging_note { @@ -387,7 +406,8 @@ impl VirtualPianoPane { }); let pointer_down = ui.input(|i| i.pointer.primary_down()); let is_pressed = self.pressed_notes.contains(¬e) || - (pointer_over_key && pointer_down); + self.playback_notes.contains(¬e) || + (started_here && pointer_over_key && pointer_down); let color = if is_pressed { shared.theme.bg_color(&[".piano-black-key", ".pressed"], ui.ctx(), egui::Color32::from_rgb(50, 100, 200)) } else { @@ -397,7 +417,7 @@ impl VirtualPianoPane { ui.painter().rect_filled(key_rect, 2.0, color); // Mouse down starts note - if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) { + if started_here && pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) { // Calculate velocity based on mouse Y position let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y; let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect); @@ -415,7 +435,7 @@ impl VirtualPianoPane { } // Dragging over a new key - if pointer_over_key && pointer_down { + if started_here && pointer_over_key && pointer_down { if self.dragging_note != Some(note) { if let Some(prev_note) = self.dragging_note { self.send_note_off(prev_note, shared); @@ -824,6 +844,11 @@ impl PaneRenderer for VirtualPianoPane { _path: &NodePath, shared: &mut SharedPaneState, ) { + // On mobile, the octave is shared with the Piano Roll so they stay aligned. + if shared.is_mobile { + self.octave_offset = *shared.keyboard_octave; + } + // Check if there's an active MIDI layer let has_active_midi_layer = if let Some(active_layer_id) = *shared.active_layer_id { shared.layer_to_track_map.contains_key(&active_layer_id) @@ -869,14 +894,28 @@ impl PaneRenderer for VirtualPianoPane { } // Calculate visible range (needed for both rendering and labels) - let (visible_start, visible_end, white_key_width, offset_x) = - self.calculate_visible_range(rect.width(), rect.height()); + let (visible_start, visible_end, white_key_width, offset_x) = if shared.is_mobile { + let layout = super::keyboard_layout::KeyboardLayout::from_width( + rect.min.x, rect.width(), self.octave_offset, *shared.keyboard_pan_x, + ); + let vs = layout.first_visible_white(); + (vs, layout.last_visible_note(), layout.white_key_width, layout.note_x(vs) - rect.min.x) + } else { + self.calculate_visible_range(rect.width(), rect.height()) + }; // Render the keyboard self.render_keyboard(ui, rect, shared); - // Render keyboard labels on top - self.render_key_labels(ui, rect, shared, visible_start, visible_end, white_key_width, offset_x); + // Render keyboard labels on top — but not on mobile (no physical keyboard to hint at). + if !shared.is_mobile { + self.render_key_labels(ui, rect, shared, visible_start, visible_end, white_key_width, offset_x); + } + + // Publish the octave so the portrait Piano Roll aligns to these keys. + if shared.is_mobile { + *shared.keyboard_octave = self.octave_offset; + } } fn name(&self) -> &str { diff --git a/phone-ui-sketches.html b/phone-ui-sketches.html new file mode 100644 index 0000000..313a8fc --- /dev/null +++ b/phone-ui-sketches.html @@ -0,0 +1,842 @@ + + + + + +Phone UI — Wireframe Plates + + + + +

+ +
+ Time / playhead / transport + Selection + Annotation +
+ + +
+
+ PLATE 01 +

The spine & the resizable ribbon

+ Tabs top · time bottom · 3 snaps +
+

Surface tabs sit along the top. At the bottom, the transport bar is fixed — the irreducible spine — and the timeline ribbon expands upward above it through three snaps, trading stage for tracks by degrees. It never hits zero on its own; the transport is always the floor.

+ +
+
+
+
9:41●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
+
+
V1
+
00:12:04
+ 1 + 2 + 3 +
+
Peek — ribbon collapsed to a sliver; transport still anchors the bottom.
+
+ +
+
+
9:41●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
+
+
+
+
RASTER
+
VEC
+
AUD
+
AUD
+
+
00:12:04
+
+
Half — arrange & trim across tracks while the stage stays in view.
+
+ +
+
1
+

Tabs own the top

+

Stage / Time / Nodes / Mixer / Tree. The active surface is home; the rest are one tap away. Moving them up clears the bottom for the controls your thumb actually lives on.

+
+
2
+

Ribbon expands upward

+

Drag the grabber up for more tracks, down for more stage. A continuous reveal, not a modal toggle — there's always a sliver left.

+
+
3
+

Transport = the fixed floor

+

Play, playhead time and a zoomed-out project scrub never move and never disappear. It's the irreducible spine; the ribbon is detail layered on top of it.

+
+
+
+
+ + +
+
+ PLATE 02 +

Tweak mode — the pull model

+ Selection → inspector +
+

You arrive with a full project and a vague target, so controls come to what you point at. Tap anything and its properties rise in a sheet above the transport. The header carries jump-to chips that reframe to another surface with the same object still selected.

+ +
+
+
+
9:41⌕ ●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
+
+
+
+
+
+
+
+
Ellipse · Vector layer
+
PropertiesTimelineNodesAutomation
+
Fill
#9A6BC0
+
Opacity
+
Position
x 108
y 54
+
Z-order
Front ▾
+
+
00:12:04
+ 1 + 2 + 3 +
+
Half tier shown — drag up for every advanced parameter, down to a 3-control peek. Transport stays pinned below.
+
+ +
+
1
+

Selection-driven

+

Tap selects and summons the sheet. Once selected, dragging the object moves it; dragging empty space pans. One chain: tap → inspect → move → long-press.

+
+
2
+

Jump-to chips = parity move

+

Timeline · Nodes · Automation reframe to that surface with the object still held — replacing the desktop trick of seeing panes side-by-side.

+
+
3
+

Sheet floats over the floor

+

The inspector rises above the transport, never under it — so even mid-tweak you can scrub and see the playhead. Object actions like send to back live on the .

+
+
+
+

Two ways to find the thing

+

For "I don't know what I want to tweak": the command palette and an outliner surface (the Tree tab) — a browsable map of the whole project.

+
+
+
+
+ + +
+
+ PLATE 03 +

Sketch mode — the push model

+ New file · intent seeds +
+

An empty project with a clearish intent, so a tool defines the surface and gets out of the way. "New" offers intents — the same non-binding menu as desktop. Picking one sets the hero surface and a minimal toolset; switching intent mid-session just swaps the hero while a real project quietly accretes underneath.

+ +
+
+
+
9:41●●●
+
+
Start something
+
Sets your first surface — nothing is locked.
+
+
DrawFull canvas · stylus · radial brushes
+
AnimateStage + keyframe scrub strip
+
ComposeInstrument + arrangement ribbon
+
RecordBig button · waveform fills screen
+
Edit videoClip bin + trim timeline
+
BlankEmpty unified timeline
+
Open recent…
+
+
+ 1 +
+
Intent picker — mirrors the desktop new-file flow; the seed steers nothing once content exists.
+
+ +
+
1
+

Intent seeds, state steers

+

At creation there's nothing else to go on, so intent is the right call. The moment content exists, the original type stops driving anything.

+
+
2
+

Home-on-open ≠ birth type

+

Re-opening lands on the file's last-focused surface + last selection — not the type it was born as. A music file that grew an animation layer shouldn't open music-shaped.

+
+
3
+

Lossless underneath

+

Every intent writes to the same document model. Whatever you rough out on the train reconstitutes as a full pane layout back on desktop.

+
+
+
+
+ + +
+
+ PLATE 04 +

Music surface — the GarageBand fix

+ Expression + context, together +
+

GarageBand denies you the when while you play. Here the instrument lives in the thumb zone with a compressed arrangement ribbon pinned above it — loop boundaries, the playhead sweeping toward the wrap, and squashed lanes of what you're playing against. The transport anchors the bottom as everywhere else.

+ +
+
+
+
9:41●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
DRUMS
+
BASS
+
KEYS ●
+
+
Grand Piano ▾Loop ⟳ 2 bars● REC
+
+ LIVE TAKE · KEYS +
+
+
+
+
+
+
+
+
+
+
+
2.1.03
120bpm
+ 1 + 2 + 3 +
+
Record-against-context — see the loop wrap and the other tracks while your thumbs are on the keys.
+
+ +
+
1
+

The loop boundary is visible

+

Green markers show where the 2-bar loop wraps; the amber playhead sweeps toward it — the exact when GarageBand's modal switch hides.

+
+
2
+

Playing against the mix

+

Squashed drum/bass lanes stay on screen so you hear and see what your take lands over. The live KEYS lane fills as you record.

+
+
3
+

Same spine, same floor

+

Transport with bars-beats + tempo anchors the bottom. Drag the ribbon down for full arrangement; the Mixer is a portrait-native surface one tab away.

+
+
+
+
+ + +
+
+ PLATE 05 +

Orientation is a per-surface reflow

+ Not an app-global mode +
+

No surface forces a rotation — that's the user's wrist, not the app's call. But the same timeline data emphasises a different axis per orientation: landscape spends pixels on time resolution, portrait spends them on track count. Rotation preserves selection, playhead and zoom-center; it only reflows.

+ +
+
+
+
9:41●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
V1
+
V2
+
RAS
+
A1
+
A2
+
A3
+
A4
+
FX
+
+
00:12:04
+
+
Portrait — vertical room buys more tracks. Best for arranging, reordering, seeing structure.
+
+ +
+
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
V1
+
A1
+
A2
+
+
00:12:04
+
+
Landscape — wide axis buys time resolution. Best for scrubbing, precise trims.
+
+ +
+
+

Reflow, don't reset

+

Same selection, playhead and zoom-center — just re-emphasised. A rotate that throws away where you were is its own GarageBand-tier annoyance.

+
+
+

Per-surface character

+

Stage follows the project's aspect; instrument prefers landscape for key width; mixer is portrait-native. The ribbon absorbs whatever the hero leaves over.

+
+
🔒
+

Manual lock

+

People work lying down and propped at angles. The device suggests; the user can pin. Auto-rotate never fights a drawing gesture.

+
+
+
+
+ + +
+
+ PLATE 06 +

Omnibutton & the gesture map

+ Tools vs commands · 7 + 1 meanings +
+

The omnibutton produces tools/objects; the global menu holds commands/destinations — a clean tool-vs-command line. Below, the resolved stage gestures: seven meanings, no collisions, each reinforcing a desktop idiom rather than inventing a phone-only convention.

+ +
+
+
+
9:41⌕ ⋯ ●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
+
+
+
+
+
Shape
+
+
+
+
V1
+
00:12:04
+ 1 + 2 +
+
Radial fans away from the finger to dodge occlusion. Global commands live in the top .
+
+ +
+
1
+

Omnibutton = tools/objects

+

Brushes, shapes, instruments, nodes — things you place or use. Long-press on empty space can summon this same create menu at the point you pressed.

+
+
2
+

Global menu = commands

+

Export, render, project settings, and the command palette itself live in the top /. If an item feels like either, it's usually a selection action in disguise → inspector.

+
+
+

Palette is the safety net

+

Every menu-bar verb has one contextual home plus the palette. That's what lets contextual homes hide rare commands without breaking parity.

+
+
+
+ +
+
+
tap object
select  + summon inspector
+
tap empty
deselect
+
drag selected
move the object / clip
+
drag empty
pan the view (matches timeline scroll)
+
two-finger drag
pan — always, everywhere (escape valve)
+
pinch
zoom — time-zoom on timeline, spatial on stage
+
long-press object
actions menu for that object
+
long-press empty
create-here radial (proposed)
+
double-tap object
enter / edit — go a level deeper
+
double-tap empty
zoom-to-fit
+
double-tap-drag empty
marquee select — fast transient path
+
+
+
+

Two tiers for marquee

+

The double-tap-drag gesture is the fast, transient path. A sustained marquee mode covers heavy vector multi-select — and with the gesture in hand, it may not even need a dedicated button.

+
+
+

Origin disambiguates

+

Marquee is strictly empty-space-initiated. Double-tap-drag starting on an object never marquees — same spatial-zones logic as the ruler vs lanes.

+
+
+

Crisp double-tap window

+

Empty space is where both zoom-to-fit and marquee branch off the same tap-tap, so tighten the detection window there specifically.

+
+
+
+
+ + +
+
+ PLATE 07 +

Node editor — focus & patch

+ Audio synthesis · port-to-port +
+

For synthesis the graph is patch-heavy: several cables of one type running between modules. So ports are identified by name — gate, velocity, pitch — not just type; type only decides what may connect, the name decides which is which. Connections are wire-level, parallel cables between a single pair are normal, and one output can fan out. Focus mode tunes a module's parameters; patch mode runs the cables.

+ +
+
+
+
9:41⌕ ⋯ ●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
+ + + + + + +
+
+
+
+
◂ gateMIDI→CV.gate
+
◂ velocityMIDI→CV.vel
+
+
+
ADSRENV · CV
+
+
Attack
+
Decay
120 ms
+
Sustain
+
+
+
env ▸VCA.in
+
+
+ Add node  ·  ⌕ Search
+
+
+
00:12:04
+ 1 + 2 +
+
Focus — one module's parameters; its named ports are travel chips. Input chips name the remote port, so two cables from the same node never collide.
+
+ +
+
+
9:41⌕ ⋯ ●●●
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
PatchMIDI→CV ▸ ADSRCV
+
pitch
→ Osc 1
+
velocity
velocity
+
gate
gate
+
tap a source port, then a target — both are CV, so the name decides the match
+
+
+
00:12:04
+ 3 +
+
Patch — two modules as port lists, each cable a row. The CV pair (gate, velocity) is just two rows; pitch fans off to the oscillator.
+
+ +
+
1
+

Ports are named, not just typed

+

Input chips read the remote endpoint as node.portMIDI→CV.gate, MIDI→CV.vel — so two cables from one node stay distinct even though both are CV. Type gates compatibility; the name carries identity.

+
+
2
+

Focus still travels & renders live

+

Tap a chip to jump to that endpoint; the minimap keeps you placed. The audio graph evaluates at the playhead, so transport stays — scrub and the envelope previews where you are.

+
+
3
+

Patch mode for cabling

+

A pair (or small cluster) shown as two port lists; each cable is a row. Parallel cables between the same pair are normal, and an output fans out (pitch → Osc). Tap a source port then a target — compatible ports light up, incompatible dim. This is the modular workflow, not one-node-at-a-time.

+
+
!
+

Honest scope

+

Phone is tune-a-module and patch-a-few; large graph restructuring stays a desktop job. Reachable for parity via the palette and these two views — without faking a tiny pannable canvas of boxes.

+
+
+
+
+ + +
+
+ PLATE 08 +

Keyboard cap & the reclaimed space

+ Portrait · keys ≤ ⅓ +
+

Key width governs playability, and width is set by the screen's short axis — so past about a third of a portrait screen, taller keys add nothing. The resize catches at that cap; pulling further reveals an instrument workspace above the keys instead. By default it's a note view of the current part, pitch-aligned to the keys; toggle it to performance controllers. Landscape has no room for this, so it's a portrait-only affordance.

+ +
+
+
+
9:41●●●
+
Stage
Time
Nodes
Mixer
Tree
+ +
+
+
DRUMS
+
BASS
+
+ +
+ Grand Piano ▾ +
NotesControllers
+ ● REC +
+ +
+ CURRENT PART · KEYS +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
2.1.03
120bpm
+ 1 + 2 + 3 +
+
Macro → micro → input: arrangement context up top, note workspace in the middle, capped keys in the thumb zone.
+
+ +
+
+
Stage
Time
Nodes
Mixer
Tree
+
+
+
DRUMS
+
+
Grand Piano ▾
KeysNotes
● REC
+
+ PIANO ROLL · time ▸ +
+
+
+
+
+
+
+
+
2.1.03
120bpm
+ 4 +
+
Landscape · Notes — standalone, the editor flips to a conventional piano roll (pitch ↕, time ▸) to spend the wide axis on time. The context ribbon still rides on top.
+
+ +
+
1
+

The cap, and what fills it

+

Keys grow to ~⅓ then stop; continued upward drag reveals the workspace. The keyboard holds its ergonomic height — the new room opens above it. Same continuous-reveal as the timeline ribbon.

+
+
2
+

Default: see what you play

+

A note view of the current part, each column sitting over its key below (a waterfall toward the amber now line). The see-what-you're-doing principle the GarageBand port breaks — and editable in place.

+
+
3
+

Or perform: Controllers

+

Toggle the zone to pitch/mod strips, an XY pad, an arpeggiator — the other natural use of space above keys. The same drag, a different occupant.

+
+
4
+

Landscape splits them — and reflows

+

No room to stack, so keys and notes become a Keys / Notes toggle, with the context ribbon persisting in both — you lose seeing keys-and-notes together, never the when. And the note view itself reflows: Synthesia-style (pitch ↔, aligned to keys) when it sits above the keyboard in portrait; a conventional piano roll (pitch ↕, time ▸) when it's standalone in landscape and the wide axis is better spent on time. Rotate back and your part, playhead and selection carry across.

+
+
+
+
+ +
+ UNIFIED-TIMELINE STUDIO · PHONE CONCEPT PLATES 01–08 + WIREFRAME — LAYOUT & GESTURE INTENT, NOT FINAL VISUAL +
+ + + From 4f66ddb51521e1d8f151d05bf694fbd76a525cc9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 09:25:52 -0400 Subject: [PATCH 24/43] =?UTF-8?q?P6b:=20mobile=20node=20editor=20=E2=80=94?= =?UTF-8?q?=20Focus=20&=20Patch=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the desktop node-graph canvas with touch-native views on mobile (wireframe Plate 07), gated on is_mobile in NodeGraphPane::render_content; the desktop draw_graph_editor path is untouched. New submodule panes/node_graph/mobile.rs. Focus view: - The focused module's parameters as touch controls (slider / dropdown / field by the desktop widget rule, with a visible slider rail); editing mutates the graph ValueType so the existing check_parameter_changes dispatches SetParameterAction. - A full-width minimap strip (tap the nearest node to focus it), travel chips naming connected endpoints (tap to jump), and an Add-node picker that creates the frontend node + AddNodeAction and focuses it. - Bespoke nodes (Sampler/Script/Sequencer/AmpSim/Oscilloscope) render their existing desktop bottom_ui; sampler/script-sample loads are wired (other custom interactions are a case-by-case follow-up). Patch view: - Per-section egui::Grid so input/output port arrows align in a column. Each port's Lucide direction arrow (from-line/to-line, tinted by DataType) is clickable to arm a cable; a compatibility-filtered picker completes it (ConnectAction). Cable chips (remote-node · remote-port) tap to disconnect (DisconnectAction). Parallel cables allowed. Also: add ARROW_RIGHT_FROM_LINE / ARROW_RIGHT_TO_LINE to mobile/icons.rs (codepoints extracted from lucide.ttf via ttx). --- .../lightningbeam-editor/src/mobile/icons.rs | 2 + .../src/panes/node_graph/mobile.rs | 736 ++++++++++++++++++ .../src/panes/node_graph/mod.rs | 12 + 3 files changed, 750 insertions(+) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs index 22c9eaf..979abd5 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/icons.rs @@ -36,6 +36,8 @@ pub fn font(size: f32) -> egui::FontId { pub const MAXIMIZE: &str = "\u{e112}"; pub const MINIMIZE: &str = "\u{e11a}"; pub const ARROW_LEFT_RIGHT: &str = "\u{e24a}"; +pub const ARROW_RIGHT_FROM_LINE: &str = "\u{e458}"; // output port +pub const ARROW_RIGHT_TO_LINE: &str = "\u{e459}"; // input port pub const GRIP_HORIZONTAL: &str = "\u{e0ea}"; pub const CHEVRONS_UP: &str = "\u{e074}"; pub const PLAY: &str = "\u{e13c}"; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs new file mode 100644 index 0000000..c35a0c0 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs @@ -0,0 +1,736 @@ +//! Mobile (touch) node editor — Focus & Patch views (wireframe Plate 07). +//! +//! Swapped in for the desktop `draw_graph_editor` canvas when `shared.is_mobile`. Focus shows one +//! module's parameters as big touch controls plus navigation; Patch does tap-to-cable wiring. All +//! edits reuse the existing dispatch: mutating a param's `ValueType` in place is picked up by +//! `check_parameter_changes`, and add/connect/disconnect go through `NodeGraphAction`. + +use super::graph_data::{DataType, NodeTemplate, ValueType}; +use super::{actions, NodeGraphPane}; +use crate::mobile::icons; +use eframe::egui; +use egui_node_graph2::{InputId, InputParamKind, NodeDataTrait, NodeId, NodeTemplateTrait, OutputId}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum NodeViewMode { + Focus, + Patch, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum PortDir { + In, + Out, +} + +/// An armed port awaiting a compatible endpoint to cable to. +#[derive(Clone, Copy)] +pub struct PatchPick { + node: NodeId, + port: usize, + dir: PortDir, + typ: DataType, +} + +pub struct MobileNodeState { + pub mode: NodeViewMode, + /// The module currently shown in Focus (and centred in Patch). + pub focus_node: Option, + /// Armed cable source in Patch: (node, output-port index). + pub patch_source: Option<(NodeId, usize)>, + /// Whether the add-node picker overlay is open. + pub show_add: bool, + /// Search filter in the add-node picker. + pub add_search: String, + /// Armed port in Patch awaiting a compatible endpoint. + patch_pick: Option, +} + +impl Default for MobileNodeState { + fn default() -> Self { + Self { + mode: NodeViewMode::Focus, + focus_node: None, + patch_source: None, + show_add: false, + add_search: String::new(), + patch_pick: None, + } + } +} + +impl NodeGraphPane { + pub(super) fn render_mobile( + &mut self, + ui: &mut egui::Ui, + rect: egui::Rect, + shared: &mut crate::panes::SharedPaneState, + ) { + let bg = shared.theme.bg_color(&["#node-editor", ".pane-content"], ui.ctx(), egui::Color32::from_gray(28)); + ui.painter().rect_filled(rect, 0.0, bg); + + // Resolve the focus node: keep the current one if it still exists, else the selected node, + // else the first node in the graph. + let focus_valid = self + .mobile + .focus_node + .map_or(false, |id| self.state.graph.nodes.get(id).is_some()); + if !focus_valid { + self.mobile.focus_node = self + .state + .selected_nodes + .iter() + .next() + .copied() + .or_else(|| self.state.graph.iter_nodes().next()); + } + + // Header: Focus/Patch toggle + focused node name. + let header_h = 44.0; + let header = egui::Rect::from_min_max(rect.min, egui::pos2(rect.right(), rect.top() + header_h)); + let body = egui::Rect::from_min_max(egui::pos2(rect.left(), header.bottom()), rect.max); + + let name = self + .mobile + .focus_node + .and_then(|id| self.state.graph.nodes.get(id)) + .map(|n| n.label.clone()) + .unwrap_or_else(|| "—".to_string()); + ui.painter().line_segment( + [egui::pos2(header.left(), header.bottom()), egui::pos2(header.right(), header.bottom())], + egui::Stroke::new(1.0, egui::Color32::from_gray(60)), + ); + let mut mode = self.mobile.mode; + ui.scope_builder( + egui::UiBuilder::new().max_rect(header.shrink(8.0)).layout(egui::Layout::left_to_right(egui::Align::Center)), + |ui| { + if ui.selectable_label(mode == NodeViewMode::Focus, "Focus").clicked() { + mode = NodeViewMode::Focus; + } + if ui.selectable_label(mode == NodeViewMode::Patch, "Patch").clicked() { + mode = NodeViewMode::Patch; + } + ui.add_space(8.0); + ui.label(egui::RichText::new(name).strong()); + }, + ); + self.mobile.mode = mode; + + // Keep backend-id map current so embedded `bottom_ui` (sampler/script/etc.) targets the + // right backend node (mirrors the desktop pre-draw sync). + self.user_state.node_backend_ids = self + .node_id_map + .iter() + .map(|(&nid, bid)| (nid, bid.index())) + .collect(); + + match self.mobile.mode { + NodeViewMode::Focus => self.render_mobile_focus(ui, body, shared), + NodeViewMode::Patch => self.render_mobile_patch(ui, body, shared), + } + + // Same dispatch tail as the desktop path: apply param edits and run any queued action. + self.check_parameter_changes(shared); + self.execute_pending_action(shared); + + // Drain the custom-UI loads that have self-contained handlers (sampler + script sample). + // Other bespoke interactions (sequencer grid, NAM model, script canvas) are a case-by-case + // follow-up; clear their queues so they don't accumulate. + if let Some(load) = self.user_state.pending_sampler_load.take() { + self.handle_pending_sampler_load(load, shared); + } + if let Some(load) = self.user_state.pending_script_sample_load.take() { + self.handle_pending_script_sample_load(load, shared); + } + self.user_state.pending_sequencer_changes.clear(); + self.user_state.pending_draw_param_changes.clear(); + self.user_state.pending_root_note_changes.clear(); + } + + fn render_mobile_focus( + &mut self, + ui: &mut egui::Ui, + body: egui::Rect, + shared: &mut crate::panes::SharedPaneState, + ) { + let Some(focus) = self.mobile.focus_node else { + return; + }; + + // Full-width minimap strip across the top; everything else below it. + let mm_h = 96.0; + let mm = egui::Rect::from_min_max(body.min, egui::pos2(body.right(), body.top() + mm_h)); + // Reserve an add-node button strip at the bottom. + let add_h = 46.0; + let add_rect = egui::Rect::from_min_max(egui::pos2(body.left(), body.bottom() - add_h), body.max); + let content = egui::Rect::from_min_max(egui::pos2(body.left(), mm.bottom()), egui::pos2(body.right(), add_rect.top())); + + // Minimap first (tap a node to focus it). + let mut jump: Option = self.render_minimap(ui, mm, focus); + + // Connection travel chips (owned) + params. + let (in_chips, out_chips) = self.focus_chips(focus); + let inputs: Vec<(String, InputId)> = self + .state + .graph + .nodes + .get(focus) + .map(|n| n.inputs.clone()) + .unwrap_or_default(); + + ui.scope_builder( + egui::UiBuilder::new().max_rect(content.shrink(10.0)).layout(egui::Layout::top_down(egui::Align::Min)), + |ui| { + if !in_chips.is_empty() { + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new("in").weak()); + for (label, node) in &in_chips { + if ui.button(label.as_str()).clicked() { + jump = Some(*node); + } + } + }); + } + if !out_chips.is_empty() { + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new("out").weak()); + for (label, node) in &out_chips { + if ui.button(label.as_str()).clicked() { + jump = Some(*node); + } + } + }); + } + if !in_chips.is_empty() || !out_chips.is_empty() { + ui.separator(); + } + egui::ScrollArea::vertical().show(ui, |ui| { + let mut any = false; + for (name, input_id) in &inputs { + let Some(param) = self.state.graph.inputs.get_mut(*input_id) else { + continue; + }; + if matches!(param.kind, InputParamKind::ConnectionOnly) { + continue; + } + any = true; + render_param_row(ui, name, &mut param.value); + ui.add_space(6.0); + } + if !any { + ui.label(egui::RichText::new("No editable parameters").weak()); + } + // Embedded desktop custom UI (sampler picker, sequencer grid, script UI, …). + // Standard nodes render nothing here. + if let Some(node) = self.state.graph.nodes.get(focus) { + let _ = node.user_data.bottom_ui(ui, focus, &self.state.graph, &mut self.user_state); + } + }); + }, + ); + + // Add-node button. + ui.scope_builder( + egui::UiBuilder::new().max_rect(add_rect.shrink(8.0)).layout(egui::Layout::left_to_right(egui::Align::Center)), + |ui| { + if ui.button(egui::RichText::new("+ Add node").size(15.0)).clicked() { + self.mobile.show_add = true; + } + }, + ); + + if let Some(nid) = jump { + self.mobile.focus_node = Some(nid); + } + + if self.mobile.show_add { + self.render_add_picker(ui, body, shared); + } + } + + /// Connection chips for the focus node: (label, remote node) for upstream inputs and downstream + /// outputs. Label names the remote endpoint (`node ▸ port`). + fn focus_chips(&self, focus: NodeId) -> (Vec<(String, NodeId)>, Vec<(String, NodeId)>) { + let mut ins = Vec::new(); + let mut outs = Vec::new(); + for (input_id, outputs) in self.state.graph.iter_connection_groups() { + let in_node = self.state.graph.inputs.get(input_id).map(|p| p.node); + for output_id in outputs { + let out_node = self.state.graph.outputs.get(output_id).map(|p| p.node); + if in_node == Some(focus) { + if let Some(src) = out_node { + ins.push((format!("{} · {}", self.node_label(src), self.output_port_name(src, output_id)), src)); + } + } + if out_node == Some(focus) { + if let Some(dst) = in_node { + outs.push((format!("{} · {}", self.node_label(dst), self.input_port_name(dst, input_id)), dst)); + } + } + } + } + (ins, outs) + } + + fn node_label(&self, n: NodeId) -> String { + self.state.graph.nodes.get(n).map(|x| x.label.clone()).unwrap_or_default() + } + fn output_port_name(&self, n: NodeId, oid: OutputId) -> String { + self.state + .graph + .nodes + .get(n) + .and_then(|node| node.outputs.iter().find(|(_, id)| *id == oid).map(|(nm, _)| nm.clone())) + .unwrap_or_default() + } + fn input_port_name(&self, n: NodeId, iid: InputId) -> String { + self.state + .graph + .nodes + .get(n) + .and_then(|node| node.inputs.iter().find(|(_, id)| *id == iid).map(|(nm, _)| nm.clone())) + .unwrap_or_default() + } + + /// Draw a minimap of the graph; returns a node id if the user tapped one. + fn render_minimap(&self, ui: &mut egui::Ui, rect: egui::Rect, focus: NodeId) -> Option { + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 4.0, egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120)); + painter.rect_stroke(rect, 4.0, egui::Stroke::new(1.0, egui::Color32::from_gray(70)), egui::StrokeKind::Inside); + + let nodes: Vec<(NodeId, egui::Pos2)> = self + .state + .graph + .iter_nodes() + .filter_map(|n| self.state.node_positions.get(n).map(|p| (n, *p))) + .collect(); + if nodes.is_empty() { + return None; + } + let (mut mnx, mut mny, mut mxx, mut mxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN); + for (_, p) in &nodes { + mnx = mnx.min(p.x); + mny = mny.min(p.y); + mxx = mxx.max(p.x); + mxy = mxy.max(p.y); + } + let pad = 10.0; + let inner = rect.shrink(pad); + let span = egui::vec2((mxx - mnx).max(1.0), (mxy - mny).max(1.0)); + let map = |p: egui::Pos2| { + egui::pos2( + inner.left() + (p.x - mnx) / span.x * inner.width(), + inner.top() + (p.y - mny) / span.y * inner.height(), + ) + }; + + // Connection lines. + for (input_id, outputs) in self.state.graph.iter_connection_groups() { + let in_pos = self.state.graph.inputs.get(input_id).and_then(|p| self.state.node_positions.get(p.node)).copied(); + for output_id in outputs { + let out_pos = self.state.graph.outputs.get(output_id).and_then(|p| self.state.node_positions.get(p.node)).copied(); + if let (Some(a), Some(b)) = (out_pos, in_pos) { + painter.line_segment([map(a), map(b)], egui::Stroke::new(1.0, egui::Color32::from_gray(90))); + } + } + } + + let resp = ui.interact(rect, ui.id().with("node_minimap"), egui::Sense::click()); + let click = if resp.clicked() { resp.interact_pointer_pos() } else { None }; + let mut hit = None; + let mut best_d = f32::MAX; + for (nid, p) in &nodes { + let c = map(*p); + let focused = *nid == focus; + let color = if focused { egui::Color32::from_rgb(0x4a, 0xa3, 0xff) } else { egui::Color32::from_gray(200) }; + painter.circle_filled(c, if focused { 7.0 } else { 5.0 }, color); + if focused { + painter.circle_stroke(c, 9.0, egui::Stroke::new(1.5, color)); + } + // Tap selects the nearest node (dots are small, so don't require a precise hit). + if let Some(cp) = click { + let d = (cp - c).length(); + if d < best_d { + best_d = d; + hit = Some(*nid); + } + } + } + hit + } + + /// The add-node picker overlay: a searchable list of templates. + fn render_add_picker(&mut self, ui: &mut egui::Ui, body: egui::Rect, shared: &mut crate::panes::SharedPaneState) { + let panel = egui::Rect::from_center_size(body.center(), egui::vec2(body.width().min(320.0), body.height().min(420.0))); + let mut chosen: Option = None; + let mut close = false; + ui.scope_builder(egui::UiBuilder::new().max_rect(panel), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Add node").strong()); + if ui.button("✕").clicked() { + close = true; + } + }); + ui.add(egui::TextEdit::singleline(&mut self.mobile.add_search).hint_text("Search…")); + let q = self.mobile.add_search.to_lowercase(); + egui::ScrollArea::vertical().show(ui, |ui| { + for template in NodeTemplate::all_finder_kinds() { + let label = template.node_finder_label(&mut self.user_state).to_string(); + if !q.is_empty() && !label.to_lowercase().contains(&q) { + continue; + } + if ui.button(&label).clicked() { + chosen = Some(template); + } + } + }); + }); + }); + if let Some(t) = chosen { + self.mobile_add_node(t, shared); + self.mobile.show_add = false; + self.mobile.add_search.clear(); + } + if close { + self.mobile.show_add = false; + } + } + + /// Create a node (frontend + backend action) and focus it, mirroring the desktop CreatedNode path. + fn mobile_add_node(&mut self, template: NodeTemplate, _shared: &mut crate::panes::SharedPaneState) { + let Some(track_id) = self.track_id else { + return; + }; + // Place near the current focus node, else the origin. + let base = self + .mobile + .focus_node + .and_then(|id| self.state.node_positions.get(id).copied()) + .unwrap_or(egui::Pos2::ZERO); + let pos = base + egui::vec2(160.0, 0.0); + + let label = template.node_graph_label(&mut self.user_state); + let user_data = template.user_data(&mut self.user_state); + let new_id = self + .state + .graph + .add_node(label, user_data, |graph, id| template.build_node(graph, &mut self.user_state, id)); + self.state.node_positions.insert(new_id, pos); + + let node_type = template.backend_type_name().to_string(); + self.pending_action = Some(Box::new(actions::NodeGraphAction::AddNode( + actions::AddNodeAction::new(track_id, node_type.clone(), (pos.x, pos.y)), + ))); + self.pending_node_addition = Some((new_id, node_type, (pos.x, pos.y))); + self.mobile.focus_node = Some(new_id); + } + + fn render_mobile_patch( + &mut self, + ui: &mut egui::Ui, + body: egui::Rect, + _shared: &mut crate::panes::SharedPaneState, + ) { + let Some(focus) = self.mobile.focus_node else { + return; + }; + + // Build owned row data (immutable reads) so rendering can freely queue ops. + let inputs: Vec<(String, InputId)> = self.state.graph.nodes.get(focus).map(|n| n.inputs.clone()).unwrap_or_default(); + let outputs: Vec<(String, OutputId)> = self.state.graph.nodes.get(focus).map(|n| n.outputs.clone()).unwrap_or_default(); + + // Rows: (idx, name, type, cables[(remote_node, remote_port, other_node, other_port)]). + let mut in_rows: Vec<(usize, String, DataType, Vec<(String, String, NodeId, usize)>)> = Vec::new(); + for (idx, (name, input_id)) in inputs.iter().enumerate() { + let Some(typ) = self.state.graph.inputs.get(*input_id).map(|p| p.typ) else { continue }; + let mut cables = Vec::new(); + if let Some(outs) = self.state.graph.connections.get(*input_id) { + for oid in outs { + if let Some(src) = self.state.graph.outputs.get(*oid).map(|o| o.node) { + let sp = self.output_port_index(src, *oid); + cables.push((self.node_label(src), self.output_port_name(src, *oid), src, sp)); + } + } + } + in_rows.push((idx, name.clone(), typ, cables)); + } + let mut out_rows: Vec<(usize, String, DataType, Vec<(String, String, NodeId, usize)>)> = Vec::new(); + for (idx, (name, output_id)) in outputs.iter().enumerate() { + let Some(typ) = self.state.graph.outputs.get(*output_id).map(|o| o.typ) else { continue }; + let mut cables = Vec::new(); + for (input_id, outs) in self.state.graph.iter_connection_groups() { + if outs.iter().any(|o| o == output_id) { + if let Some(dst) = self.state.graph.inputs.get(input_id).map(|i| i.node) { + let dp = self.input_port_index(dst, input_id); + cables.push((self.node_label(dst), self.input_port_name(dst, input_id), dst, dp)); + } + } + } + out_rows.push((idx, name.clone(), typ, cables)); + } + + // Queue ops during render, apply after (avoids borrowing self mid-render). + enum Op { + Disconnect(NodeId, usize, NodeId, usize), // (out_node, out_port, in_node, in_port) + Connect(NodeId, usize, NodeId, usize), + Pick(PatchPick), + ClearPick, + } + let mut ops: Vec = Vec::new(); + let pick = self.mobile.patch_pick; + + ui.scope_builder( + egui::UiBuilder::new().max_rect(body.shrink(10.0)).layout(egui::Layout::top_down(egui::Align::Min)), + |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + // A Grid aligns each section's port arrows into a column. Tapping a port arrow + // arms a cable from it (also for unconnected ports); tapping a cable chip removes + // it. Inputs read `[cables] ⥓ name`; outputs read `name ↦ [cables]`. + ui.label(egui::RichText::new("Inputs").weak()); + egui::Grid::new("patch_inputs").num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| { + for (idx, name, typ, cables) in &in_rows { + ui.horizontal(|ui| { + for (rnode, rport, sn, sp) in cables { + if cable_chip(ui, rnode, rport, *typ).clicked() { + ops.push(Op::Disconnect(*sn, *sp, focus, *idx)); + } + } + }); + if arrow_button(ui, icons::ARROW_RIGHT_TO_LINE, *typ).clicked() { + ops.push(Op::Pick(PatchPick { node: focus, port: *idx, dir: PortDir::In, typ: *typ })); + } + ui.label(name.as_str()); + ui.end_row(); + } + }); + ui.add_space(10.0); + ui.label(egui::RichText::new("Outputs").weak()); + egui::Grid::new("patch_outputs").num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| { + for (idx, name, typ, cables) in &out_rows { + ui.label(name.as_str()); + if arrow_button(ui, icons::ARROW_RIGHT_FROM_LINE, *typ).clicked() { + ops.push(Op::Pick(PatchPick { node: focus, port: *idx, dir: PortDir::Out, typ: *typ })); + } + ui.horizontal(|ui| { + for (rnode, rport, dn, dp) in cables { + if cable_chip(ui, rnode, rport, *typ).clicked() { + ops.push(Op::Disconnect(focus, *idx, *dn, *dp)); + } + } + }); + ui.end_row(); + } + }); + }); + }, + ); + + // Compatible-endpoint picker overlay when a port is armed. + if let Some(p) = pick { + let candidates = self.patch_candidates(p); + let panel = egui::Rect::from_center_size(body.center(), egui::vec2(body.width().min(320.0), body.height().min(360.0))); + ui.scope_builder(egui::UiBuilder::new().max_rect(panel), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.horizontal(|ui| { + port_marker(ui, p.typ, p.dir); + ui.label(egui::RichText::new("Connect to…").strong()); + if ui.button("✕").clicked() { + ops.push(Op::ClearPick); + } + }); + egui::ScrollArea::vertical().show(ui, |ui| { + if candidates.is_empty() { + ui.label(egui::RichText::new("No compatible ports").weak()); + } + for (label, other, other_port) in &candidates { + if ui.button(egui::RichText::new(label.as_str()).color(type_color(p.typ))).clicked() { + // Orient the cable: armed In needs an Out source; armed Out needs an In target. + match p.dir { + PortDir::In => ops.push(Op::Connect(*other, *other_port, p.node, p.port)), + PortDir::Out => ops.push(Op::Connect(p.node, p.port, *other, *other_port)), + } + ops.push(Op::ClearPick); + } + } + }); + }); + }); + } + + for op in ops { + match op { + Op::Disconnect(on, op_, inn, ip) => self.mobile_disconnect(on, op_, inn, ip), + Op::Connect(on, op_, inn, ip) => self.mobile_connect(on, op_, inn, ip), + Op::Pick(p) => self.mobile.patch_pick = Some(p), + Op::ClearPick => self.mobile.patch_pick = None, + } + } + } + + /// Compatible endpoints on OTHER nodes for an armed port (opposite direction, same type). + fn patch_candidates(&self, p: PatchPick) -> Vec<(String, NodeId, usize)> { + let mut out = Vec::new(); + for node in self.state.graph.iter_nodes() { + if node == p.node { + continue; + } + let Some(n) = self.state.graph.nodes.get(node) else { continue }; + match p.dir { + // Armed input → list outputs of matching type. + PortDir::In => { + for (i, (name, oid)) in n.outputs.iter().enumerate() { + if self.state.graph.outputs.get(*oid).map(|o| o.typ) == Some(p.typ) { + out.push((format!("{} · {}", self.node_label(node), name), node, i)); + } + } + } + // Armed output → list inputs of matching type. + PortDir::Out => { + for (i, (name, iid)) in n.inputs.iter().enumerate() { + if self.state.graph.inputs.get(*iid).map(|x| x.typ) == Some(p.typ) { + out.push((format!("{} · {}", self.node_label(node), name), node, i)); + } + } + } + } + } + out + } + + fn output_port_index(&self, n: NodeId, oid: OutputId) -> usize { + self.state.graph.nodes.get(n).and_then(|node| node.outputs.iter().position(|(_, id)| *id == oid)).unwrap_or(0) + } + fn input_port_index(&self, n: NodeId, iid: InputId) -> usize { + self.state.graph.nodes.get(n).and_then(|node| node.inputs.iter().position(|(_, id)| *id == iid)).unwrap_or(0) + } + + /// Cable an output port → input port: update the frontend graph + dispatch `Connect`. + fn mobile_connect(&mut self, out_node: NodeId, out_port: usize, in_node: NodeId, in_port: usize) { + let Some(track_id) = self.track_id else { return }; + let output_id = self.state.graph.nodes.get(out_node).and_then(|n| n.outputs.get(out_port)).map(|(_, id)| *id); + let input_id = self.state.graph.nodes.get(in_node).and_then(|n| n.inputs.get(in_port)).map(|(_, id)| *id); + let (Some(output_id), Some(input_id)) = (output_id, input_id) else { return }; + if let Some(conns) = self.state.graph.connections.get_mut(input_id) { + if !conns.contains(&output_id) { + conns.push(output_id); + } + } else { + self.state.graph.connections.insert(input_id, vec![output_id]); + } + if let (Some(&from_id), Some(&to_id)) = (self.node_id_map.get(&out_node), self.node_id_map.get(&in_node)) { + self.pending_action = Some(Box::new(actions::NodeGraphAction::Connect( + actions::ConnectAction::new(track_id, from_id, out_port, to_id, in_port), + ))); + } + } + + /// Remove a cable: update the frontend graph + dispatch `Disconnect`. + fn mobile_disconnect(&mut self, out_node: NodeId, out_port: usize, in_node: NodeId, in_port: usize) { + let Some(track_id) = self.track_id else { return }; + let output_id = self.state.graph.nodes.get(out_node).and_then(|n| n.outputs.get(out_port)).map(|(_, id)| *id); + let input_id = self.state.graph.nodes.get(in_node).and_then(|n| n.inputs.get(in_port)).map(|(_, id)| *id); + let (Some(output_id), Some(input_id)) = (output_id, input_id) else { return }; + if let Some(conns) = self.state.graph.connections.get_mut(input_id) { + conns.retain(|o| *o != output_id); + } + if let (Some(&from_id), Some(&to_id)) = (self.node_id_map.get(&out_node), self.node_id_map.get(&in_node)) { + self.pending_action = Some(Box::new(actions::NodeGraphAction::Disconnect( + actions::DisconnectAction::new(track_id, from_id, out_port, to_id, in_port), + ))); + } + } +} + +/// Desktop port color per signal type (matches `DataType::data_type_color`). +fn type_color(t: DataType) -> egui::Color32 { + match t { + DataType::Audio => egui::Color32::from_rgb(100, 150, 255), // blue + DataType::Midi => egui::Color32::from_rgb(100, 255, 100), // green + DataType::CV => egui::Color32::from_rgb(255, 150, 100), // orange + } +} + +/// A single Lucide direction arrow glyph, tinted by signal type. +fn arrow(ui: &mut egui::Ui, glyph: &str, t: DataType) { + ui.label( + egui::RichText::new(glyph) + .color(type_color(t)) + .family(egui::FontFamily::Name(icons::FAMILY.into())) + .size(15.0), + ); +} + +/// A port marker: a Lucide direction arrow (out = from-line, in = to-line) tinted by signal type. +fn port_marker(ui: &mut egui::Ui, t: DataType, dir: PortDir) { + let glyph = match dir { + PortDir::In => icons::ARROW_RIGHT_TO_LINE, + PortDir::Out => icons::ARROW_RIGHT_FROM_LINE, + }; + arrow(ui, glyph, t); +} + +/// A clickable, aligned port arrow (out = from-line, in = to-line) tinted by signal type. Tapping it +/// arms a cable from the port. Frameless so it reads like the desktop port dot. +fn arrow_button(ui: &mut egui::Ui, glyph: &str, t: DataType) -> egui::Response { + ui.add( + egui::Button::new( + egui::RichText::new(glyph) + .color(type_color(t)) + .family(egui::FontFamily::Name(icons::FAMILY.into())) + .size(16.0), + ) + .frame(false), + ) +} + +/// A cable badge naming the remote endpoint (`remote-node · remote-port`), framed and tinted by +/// signal type. Returns a click response (used to disconnect the cable). +fn cable_chip(ui: &mut egui::Ui, remote_node: &str, remote_port: &str, t: DataType) -> egui::Response { + egui::Frame::group(ui.style()) + .inner_margin(egui::Margin::symmetric(6, 1)) + .show(ui, |ui| { + ui.label(egui::RichText::new(format!("{remote_node} · {remote_port}")).color(type_color(t))); + }) + .response + .interact(egui::Sense::click()) +} + +/// One parameter row: a touch control matching the desktop widget rule (enum → dropdown, ranged → +/// slider, plain → stepper, string → field). Mutates the value in place; dispatch happens later via +/// `check_parameter_changes`. +fn render_param_row(ui: &mut egui::Ui, name: &str, value: &mut ValueType) { + match value { + ValueType::Float { value, min, max, unit, enum_labels, .. } => { + ui.label(name); + if let Some(labels) = enum_labels { + let mut sel = (*value as usize).min(labels.len().saturating_sub(1)); + egui::ComboBox::from_id_salt(name) + .width(ui.available_width().min(240.0)) + .selected_text(labels.get(sel).copied().unwrap_or("?")) + .show_ui(ui, |ui| { + for (i, label) in labels.iter().enumerate() { + ui.selectable_value(&mut sel, i, *label); + } + }); + *value = sel as f32; + } else if *max > *min { + // Give the rail a visible color so the full track length reads (the theme's default + // inactive fill can be near-transparent); trailing_fill shows progress along it. + ui.scope(|ui| { + let rail = egui::Color32::from_gray(90); + ui.visuals_mut().widgets.inactive.bg_fill = rail; + ui.visuals_mut().widgets.inactive.weak_bg_fill = rail; + ui.visuals_mut().widgets.hovered.bg_fill = rail; + ui.add(egui::Slider::new(value, *min..=*max).suffix(*unit).trailing_fill(true)); + }); + } else { + ui.add(egui::DragValue::new(value).speed(0.1)); + } + } + ValueType::String { value } => { + ui.label(name); + ui.text_edit_singleline(value); + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index 7b6bca1..0b36b85 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -6,6 +6,7 @@ pub mod actions; pub mod audio_backend; pub mod backend; pub mod graph_data; +mod mobile; use backend::{BackendNodeId, GraphBackend}; use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NamModelInfo, NodeData, NodeTemplate, PendingAmpSimLoad, ValueType}; @@ -142,6 +143,9 @@ pub struct NodeGraphPane { last_oscilloscope_poll: std::time::Instant, /// Backend track ID (u32) for oscilloscope queries backend_track_id: Option, + + /// Mobile (touch) Focus/Patch view state. + mobile: mobile::MobileNodeState, } impl NodeGraphPane { @@ -171,6 +175,7 @@ impl NodeGraphPane { pending_script_resolutions: Vec::new(), last_oscilloscope_poll: std::time::Instant::now(), backend_track_id: None, + mobile: mobile::MobileNodeState::default(), } } @@ -2365,6 +2370,13 @@ impl crate::panes::PaneRenderer for NodeGraphPane { painter.galley(text_pos, galley, text_color); return; } + + // Mobile: touch-native Focus/Patch views instead of the desktop canvas. + if shared.is_mobile { + self.render_mobile(ui, rect, shared); + return; + } + // Poll oscilloscope data at ~20 FPS let has_oscilloscopes; if self.last_oscilloscope_poll.elapsed() >= std::time::Duration::from_millis(50) { From 2cbc35f1813f4623664d4aa94bca5e276d14615d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 2 Jul 2026 10:07:39 -0400 Subject: [PATCH 25/43] Mobile context-menu coverage + z-order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook up menu-bar actions that lacked a mobile affordance, and implement z-order. - Stage/object long-press menu (stage.rs): add Paste (on clip, geometry, and empty stage) and Send to Back / Bring to Front on the selection. - Timeline long-press menu (timeline.rs): the timeline had no mobile context menu — add one via manual long-press detection. Clip actions (Split, Duplicate, Cut, Copy, Paste, Delete) on a clip; animation actions (New/Blank keyframe, Add keyframe at playhead, Duplicate keyframe, Delete frame, Add motion/shape tween) on an empty lane. Gate the desktop right-click menu to !is_mobile so mobile shows only the long-press menu. - Implement Send to Back / Bring to Front (were // TODO no-ops): new undoable ReorderClipInstancesAction in lightningbeam-core reorders the selected instances within their layer's clip_instances Vec (stacking order; last = on top; geometry stays flattened underneath). Wired via handle_menu_action. --- .../lightningbeam-core/src/actions/mod.rs | 2 + .../src/actions/reorder_clip_instances.rs | 78 +++++++++++++++++++ .../lightningbeam-editor/src/main.rs | 39 +++++++++- .../lightningbeam-editor/src/panes/stage.rs | 9 +++ .../src/panes/timeline.rs | 66 +++++++++++++++- 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-core/src/actions/reorder_clip_instances.rs diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs b/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs index 81a36f6..7bed42f 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/mod.rs @@ -11,6 +11,7 @@ pub mod add_layer; pub mod add_shape; pub mod modify_shape_path; pub mod move_clip_instances; +pub mod reorder_clip_instances; pub mod paint_bucket; pub mod remove_effect; pub mod set_document_properties; @@ -54,6 +55,7 @@ pub use add_layer::AddLayerAction; pub use add_shape::AddShapeAction; pub use modify_shape_path::ModifyGraphAction; pub use move_clip_instances::MoveClipInstancesAction; +pub use reorder_clip_instances::ReorderClipInstancesAction; pub use paint_bucket::PaintBucketAction; pub use remove_effect::RemoveEffectAction; pub use set_document_properties::SetDocumentPropertiesAction; diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/reorder_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/reorder_clip_instances.rs new file mode 100644 index 0000000..ae429ec --- /dev/null +++ b/lightningbeam-ui/lightningbeam-core/src/actions/reorder_clip_instances.rs @@ -0,0 +1,78 @@ +//! Reorder clip instances within a layer's stacking order (Send to Back / Bring to Front). +//! +//! A layer's `clip_instances` Vec order *is* the stacking order — the last element renders on top +//! (hit-testing walks it in reverse). Geometry renders underneath and is unaffected. This action +//! moves the selected instances to the front (end) or back (start) of that Vec. + +use crate::action::Action; +use crate::clip::ClipInstance; +use crate::document::Document; +use crate::layer::AnyLayer; +use uuid::Uuid; + +pub struct ReorderClipInstancesAction { + layer_id: Uuid, + instance_ids: Vec, + /// `true` = bring to front (top), `false` = send to back (bottom). + to_front: bool, + /// Full instance order captured on `execute`, for `rollback`. + old_order: Option>, +} + +impl ReorderClipInstancesAction { + pub fn new(layer_id: Uuid, instance_ids: Vec, to_front: bool) -> Self { + Self { layer_id, instance_ids, to_front, old_order: None } + } +} + +/// The clip-instance stack for a layer, if it has one (Group/Raster/Text don't). +fn clip_instances_mut<'a>(document: &'a mut Document, layer_id: &Uuid) -> Option<&'a mut Vec> { + match document.get_layer_mut(layer_id)? { + AnyLayer::Vector(l) => Some(&mut l.clip_instances), + AnyLayer::Audio(l) => Some(&mut l.clip_instances), + AnyLayer::Video(l) => Some(&mut l.clip_instances), + AnyLayer::Effect(l) => Some(&mut l.clip_instances), + _ => None, + } +} + +impl Action for ReorderClipInstancesAction { + fn execute(&mut self, document: &mut Document) -> Result<(), String> { + let instances = clip_instances_mut(document, &self.layer_id) + .ok_or_else(|| "Layer has no clip-instance stack".to_string())?; + self.old_order = Some(instances.iter().map(|c| c.id).collect()); + + let mut selected = Vec::new(); + let mut rest = Vec::new(); + for ci in instances.drain(..) { + if self.instance_ids.contains(&ci.id) { + selected.push(ci); + } else { + rest.push(ci); + } + } + if self.to_front { + rest.extend(selected); // selected last → rendered on top + *instances = rest; + } else { + selected.extend(rest); // selected first → rendered at the bottom (of the stack) + *instances = selected; + } + Ok(()) + } + + fn rollback(&mut self, document: &mut Document) -> Result<(), String> { + let Some(order) = self.old_order.clone() else { + return Ok(()); + }; + let instances = clip_instances_mut(document, &self.layer_id) + .ok_or_else(|| "Layer has no clip-instance stack".to_string())?; + let rank = |id: &Uuid| order.iter().position(|o| o == id).unwrap_or(usize::MAX); + instances.sort_by(|a, b| rank(&a.id).cmp(&rank(&b.id))); + Ok(()) + } + + fn description(&self) -> String { + if self.to_front { "Bring to Front".to_string() } else { "Send to Back".to_string() } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index ca21996..3a6f03b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -2948,6 +2948,39 @@ impl EditorApp { } } + /// Send the selected clip/group instances to the back or front of their layer's stacking order. + fn reorder_selected_clips(&mut self, to_front: bool) { + use lightningbeam_core::layer::AnyLayer; + let Some(layer_id) = self.active_layer_id else { + return; + }; + let ids: Vec = { + let document = self.action_executor.document(); + let Some(layer) = document.get_layer(&layer_id) else { + return; + }; + let instances: &[lightningbeam_core::clip::ClipInstance] = match layer { + AnyLayer::Vector(l) => &l.clip_instances, + AnyLayer::Audio(l) => &l.clip_instances, + AnyLayer::Video(l) => &l.clip_instances, + AnyLayer::Effect(l) => &l.clip_instances, + _ => &[], + }; + instances + .iter() + .filter(|ci| self.selection.contains_clip_instance(&ci.id)) + .map(|ci| ci.id) + .collect() + }; + if ids.is_empty() { + return; + } + let action = lightningbeam_core::actions::ReorderClipInstancesAction::new(layer_id, ids, to_front); + if let Err(e) = self.action_executor.execute(Box::new(action)) { + eprintln!("Failed to reorder clip instances: {e}"); + } + } + /// Duplicate the selected clip instances on the active layer. /// Each duplicate is placed immediately after the original clip. fn duplicate_selected_clips(&mut self) { @@ -3588,12 +3621,10 @@ impl EditorApp { } } MenuAction::SendToBack => { - println!("Menu: Send to Back"); - // TODO: Implement send to back + self.reorder_selected_clips(false); } MenuAction::BringToFront => { - println!("Menu: Bring to Front"); - // TODO: Implement bring to front + self.reorder_selected_clips(true); } MenuAction::SplitClip => { self.split_clips_at_playhead(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 01adcd7..bef45be 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -11537,14 +11537,23 @@ impl StagePane { // A clip instance is already a clip — no "convert" here. items.push(("Cut".into(), MenuAction::Cut)); items.push(("Copy".into(), MenuAction::Copy)); + items.push(("Paste".into(), MenuAction::Paste)); items.push(("Duplicate".into(), MenuAction::DuplicateClip)); items.push(("Delete".into(), MenuAction::Delete)); + items.push(("Send to back".into(), MenuAction::SendToBack)); + items.push(("Bring to front".into(), MenuAction::BringToFront)); } else if shared.selection.has_geometry_selection() { // Geometry can be gathered up into a movie clip. items.push(("Cut".into(), MenuAction::Cut)); items.push(("Copy".into(), MenuAction::Copy)); + items.push(("Paste".into(), MenuAction::Paste)); items.push(("Convert to movie clip".into(), MenuAction::ConvertToMovieClip)); items.push(("Delete".into(), MenuAction::Delete)); + items.push(("Send to back".into(), MenuAction::SendToBack)); + items.push(("Bring to front".into(), MenuAction::BringToFront)); + } else { + // Nothing selected — offer paste onto the stage. + items.push(("Paste".into(), MenuAction::Paste)); } if !items.is_empty() { if let Some(pos) = ui.input(|i| i.pointer.interact_pos()) { diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index 663678b..b139a57 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -268,6 +268,9 @@ pub struct TimelinePane { /// Is the user panning the timeline? is_panning: bool, last_pan_pos: Option, + /// Manual long-press tracking for the mobile context menu: press start time + position. + lp_time: Option, + lp_pos: egui::Pos2, /// Clip drag state (None if not dragging) clip_drag_state: Option, @@ -788,6 +791,8 @@ impl TimelinePane { is_scrubbing: false, is_panning: false, last_pan_pos: None, + lp_time: None, + lp_pos: egui::Pos2::ZERO, clip_drag_state: None, drag_offset: 0.0, drag_anchor_start: 0.0, @@ -5837,9 +5842,10 @@ impl PaneRenderer for TimelinePane { } ui.set_clip_rect(original_clip_rect); - // Context menu: detect right-click on clips or empty timeline space + // Context menu: detect right-click on clips or empty timeline space (desktop only — mobile + // uses the long-press menu below). let mut just_opened_menu = false; - let secondary_clicked = ui.input(|i| i.pointer.button_clicked(egui::PointerButton::Secondary)); + let secondary_clicked = !shared.is_mobile && ui.input(|i| i.pointer.button_clicked(egui::PointerButton::Secondary)); if secondary_clicked { if let Some(pos) = ui.input(|i| i.pointer.interact_pos()) { if content_rect.contains(pos) { @@ -5859,6 +5865,62 @@ impl PaneRenderer for TimelinePane { } } + // Mobile: a manual long-press (hold in place ~0.4s) builds a persistent context menu rendered + // by the shell — clip actions on a clip, animation (keyframe/tween) actions on an empty lane. + let mut long_press: Option = None; + if shared.is_mobile { + let now = ui.input(|i| i.time); + let down = ui.input(|i| i.pointer.any_down()); + let ptr = ui.input(|i| i.pointer.latest_pos()); + if ui.input(|i| i.pointer.primary_pressed()) { + match ptr { + Some(p) if content_rect.contains(p) => { + self.lp_time = Some(now); + self.lp_pos = p; + } + _ => self.lp_time = None, + } + } + if !down { + self.lp_time = None; + } + if let Some(t0) = self.lp_time { + let moved = ptr.map_or(0.0, |p| (p - self.lp_pos).length()); + if moved > 8.0 { + self.lp_time = None; + } else if now - t0 >= 0.4 { + self.lp_time = None; + long_press = Some(self.lp_pos); + } + } + } + if let Some(pos) = long_press { + use crate::menu::MenuAction; + let mut items: Vec<(String, MenuAction)> = Vec::new(); + if let Some((_drag_type, clip_id)) = self.detect_clip_at_pointer(pos, document, content_rect, layer_headers_rect, editing_clip_id.as_ref(), &audio_cache) { + if !shared.selection.contains_clip_instance(&clip_id) { + shared.selection.select_only_clip_instance(clip_id); + } + *shared.focus = lightningbeam_core::selection::FocusSelection::ClipInstances(shared.selection.clip_instances().to_vec()); + items.push(("Split clip".into(), MenuAction::SplitClip)); + items.push(("Duplicate clip".into(), MenuAction::DuplicateClip)); + items.push(("Cut".into(), MenuAction::Cut)); + items.push(("Copy".into(), MenuAction::Copy)); + items.push(("Paste".into(), MenuAction::Paste)); + items.push(("Delete".into(), MenuAction::Delete)); + } else { + items.push(("New keyframe".into(), MenuAction::NewKeyframe)); + items.push(("New blank keyframe".into(), MenuAction::NewBlankKeyframe)); + items.push(("Add keyframe at playhead".into(), MenuAction::AddKeyframeAtPlayhead)); + items.push(("Duplicate keyframe".into(), MenuAction::DuplicateKeyframe)); + items.push(("Delete frame".into(), MenuAction::DeleteFrame)); + items.push(("Add motion tween".into(), MenuAction::AddMotionTween)); + items.push(("Add shape tween".into(), MenuAction::AddShapeTween)); + items.push(("Paste".into(), MenuAction::Paste)); + } + *shared.mobile_context_menu = Some(crate::panes::MobileContextMenu { pos, items }); + } + // Render context menu if let Some((ctx_clip_id, menu_pos)) = self.context_menu_clip { let has_clip = ctx_clip_id.is_some(); From 6f67ddb70974e45db6a966be78665fac4b9dcbe2 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 3 Jul 2026 01:07:44 -0400 Subject: [PATCH 26/43] Mobile P7: landscape / orientation support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the phone shell orientation-aware (portrait vs landscape). Orientation is aspect-based (rotation = a resize the shell re-lays-out each frame); LB_MOBILE_UI=2 opens a landscape dev window. `SharedPaneState.is_portrait` is published to panes. - Stack caps at 2 panes in landscape (max_panes through the R1–R6 drag ops), with a per-frame clamp so rotating while 3 are open drops to 2. - Edge drags are a continuous full-height reveal instead of stalling at the capped trigger: 1-pane → split → next-pane-fullscreen, and (landscape 2-pane) a two-phase slide-then-collapse to 1 pane. Both top and bottom edges, mirrored. Release snaps to stay / split / fullscreen. Dividers resize and collapse-to-1 at the extremes. - Inspector becomes a right-side vertical column in landscape (inspector_width_frac, left-edge horizontal resize); portrait keeps the bottom sheet. - Piano roll: vertical "Synthesia" view in portrait; landscape shows a Keys/Notes segmented toggle over the conventional horizontal roll / full keyboard. - Start screen reflows to 2 rows × 3 with the recent list beside it in landscape. - Landscape headers are shorter (52→34px) and the top bar folds into the middle of the top pane header (no separate band), reclaiming vertical space. All gated to mobile; desktop unchanged (is_portrait defaults true). --- .../lightningbeam-editor/src/main.rs | 5 +- .../src/mobile/inspector.rs | 67 +++--- .../lightningbeam-editor/src/mobile/intent.rs | 50 +++-- .../lightningbeam-editor/src/mobile/mod.rs | 80 +++++--- .../lightningbeam-editor/src/mobile/stack.rs | 191 +++++++++++++++--- .../lightningbeam-editor/src/mobile/topbar.rs | 59 ++++++ .../lightningbeam-editor/src/panes/mod.rs | 4 + .../src/panes/piano_roll.rs | 59 +++++- 8 files changed, 418 insertions(+), 97 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 3a6f03b..10d5a6a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -201,7 +201,9 @@ fn main() -> eframe::Result { // When developing the mobile UI on desktop (LB_MOBILE_UI), open a phone-aspect window so // the shell can be exercised at a realistic size. Gating the shell itself is done at // render time on the flag, not on window aspect. - let initial_size = if mobile::is_mobile_env() { + let initial_size = if mobile::is_mobile_landscape_env() { + [874.0, 402.0] // iPhone-ish landscape (LB_MOBILE_UI=2) + } else if mobile::is_mobile_env() { [402.0, 874.0] // iPhone-ish portrait } else { [1920.0, 1080.0] @@ -7294,6 +7296,7 @@ impl EditorApp { rc: RenderContext { shared: panes::SharedPaneState { is_mobile, + is_portrait: true, // set by the mobile shell from the available rect keyboard_octave: &mut self.keyboard_octave, keyboard_pan_x: &mut self.keyboard_pan_x, instrument_show_roll: false, diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs index 9edbfb1..5a3e5ca 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/inspector.rs @@ -44,7 +44,7 @@ pub fn selection_sig(shared: &SharedPaneState) -> u64 { pub fn target_slot(shared: &SharedPaneState) -> usize { match &*shared.focus { FocusSelection::Notes { .. } => 4, // PianoRoll - FocusSelection::Nodes(_) => 6, // Node/Instrument + FocusSelection::Nodes(_) => 5, // Node/Instrument FocusSelection::Assets(_) => 1, // Asset Library FocusSelection::ClipInstances(_) | FocusSelection::Layers(_) => 3, // Timeline FocusSelection::Geometry { .. } | FocusSelection::None => 2, // Stage @@ -105,44 +105,61 @@ struct Chip { const CHIPS: [Chip; 2] = [ Chip { label: "Timeline", window: (3, 1) }, // Timeline = STACK index 3 - Chip { label: "Nodes", window: (6, 1) }, // Node/Instrument = STACK index 6 + Chip { label: "Nodes", window: (5, 1) }, // Node/Instrument = STACK index 5 ]; pub fn render( ui: &mut egui::Ui, rect: egui::Rect, - region_h: f32, + region: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette, + landscape: bool, ) { - // Sheet background with rounded top + a border stroke that follows the rounded corners (a plain - // hline would overrun the rounded corners and detach from them). - let radius = egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }; + // Panel background + border, with rounded corners on the edge that faces the content (top in + // portrait, left in landscape). + let radius = if landscape { + egui::CornerRadius { nw: 14, ne: 0, sw: 14, se: 0 } + } else { + egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 } + }; ui.painter().rect_filled(rect, radius, pal.surface_alt); ui.painter().rect_stroke(rect, radius, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside); - // Grab handle — drag to resize the sheet. - let grab = egui::Rect::from_min_max( - rect.left_top(), - egui::pos2(rect.right(), rect.top() + GRAB_H), - ); - let gresp = ui.interact(grab, ui.id().with("mobile_inspector_grab"), egui::Sense::drag()); - let pill = egui::Rect::from_center_size(grab.center(), egui::vec2(34.0, 4.0)); - ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { pal.accent } else { pal.line }); - if gresp.dragged() && region_h > 1.0 { - state.inspector_frac = (state.inspector_frac - gresp.drag_delta().y / region_h).clamp(0.2, 0.85); - } - if gresp.hovered() || gresp.dragged() { - ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); - } + // Grab handle — top strip (portrait, drags height) or left strip (landscape, drags width). + // `content_area` is the panel minus the grab strip. + let content_area = if landscape { + let grab = egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.left() + GRAB_H, rect.bottom())); + let gresp = ui.interact(grab, ui.id().with("mobile_inspector_grab"), egui::Sense::drag()); + let pill = egui::Rect::from_center_size(grab.center(), egui::vec2(4.0, 34.0)); + ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { pal.accent } else { pal.line }); + if gresp.dragged() && region.width() > 1.0 { + state.inspector_width_frac = (state.inspector_width_frac - gresp.drag_delta().x / region.width()).clamp(0.2, 0.7); + } + if gresp.hovered() || gresp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal); + } + egui::Rect::from_min_max(egui::pos2(grab.right(), rect.top()), rect.max) + } else { + let grab = egui::Rect::from_min_max(rect.left_top(), egui::pos2(rect.right(), rect.top() + GRAB_H)); + let gresp = ui.interact(grab, ui.id().with("mobile_inspector_grab"), egui::Sense::drag()); + let pill = egui::Rect::from_center_size(grab.center(), egui::vec2(34.0, 4.0)); + ui.painter().rect_filled(pill, 2.0, if gresp.hovered() || gresp.dragged() { pal.accent } else { pal.line }); + if gresp.dragged() && region.height() > 1.0 { + state.inspector_frac = (state.inspector_frac - gresp.drag_delta().y / region.height()).clamp(0.2, 0.85); + } + if gresp.hovered() || gresp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); + } + egui::Rect::from_min_max(egui::pos2(rect.left(), grab.bottom()), rect.max) + }; // Single header row (egui widgets — they inherit the mobile touch sizing + theme visuals): // title on the left, [Timeline] [Nodes] jump buttons + ✕ close on the right. - let head_y = rect.top() + GRAB_H; let head = egui::Rect::from_min_max( - egui::pos2(rect.left(), head_y), - egui::pos2(rect.right(), head_y + HEAD_H), + content_area.min, + egui::pos2(content_area.right(), content_area.top() + HEAD_H), ); let title_text = title(&rc.shared); let mut jump: Option<(usize, usize)> = None; @@ -184,8 +201,8 @@ pub fn render( // Properties content — reuse the Infopanel full-bleed. let content = egui::Rect::from_min_max( - egui::pos2(rect.left(), head.bottom()), - rect.max, + egui::pos2(content_area.left(), head.bottom()), + content_area.max, ); if content.height() > 1.0 { surface::render_surface_fullbleed(ui, content, &inspector_path(), PaneType::Infopanel, rc); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs index 590bdf4..b48e0ea 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs @@ -59,20 +59,35 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { pal.text, ); - // Vertical budget: ~2/3 for the intent grid, ~1/3 for the recent list. + let landscape = rect.width() > rect.height(); let content_top = rect.top() + 62.0; - let content_h = (rect.bottom() - margin) - content_top; + let content_bottom = rect.bottom() - margin; let gap = 10.0; - let grid_h = content_h * 0.66; - // 2×3 grid of intent cards filling the grid budget. - let col_w = (right - left - gap) / 2.0; - let card_h = (grid_h - 2.0 * gap) / 3.0; + // Portrait: 3 rows × 2 cols of intent cards up top, recent list below. + // Landscape: 2 rows × 3 cols on the left, recent list to the right. + let (cols, rows) = if landscape { (3usize, 2usize) } else { (2usize, 3usize) }; + let (grid_rect, recent_rect) = if landscape { + let split = left + (right - left) * 0.62; + ( + egui::Rect::from_min_max(egui::pos2(left, content_top), egui::pos2(split - gap, content_bottom)), + egui::Rect::from_min_max(egui::pos2(split + gap, content_top), egui::pos2(right, content_bottom)), + ) + } else { + let grid_h = (content_bottom - content_top) * 0.66; + ( + egui::Rect::from_min_max(egui::pos2(left, content_top), egui::pos2(right, content_top + grid_h)), + egui::Rect::from_min_max(egui::pos2(left, content_top + grid_h + 14.0), egui::pos2(right, content_bottom)), + ) + }; + + let col_w = (grid_rect.width() - (cols as f32 - 1.0) * gap) / cols as f32; + let card_h = (grid_rect.height() - (rows as f32 - 1.0) * gap) / rows as f32; for (i, intent) in intents(&pal).iter().enumerate() { - let col = (i % 2) as f32; - let row = (i / 2) as f32; - let cx = left + col * (col_w + gap); - let cy = content_top + row * (card_h + gap); + let col = (i % cols) as f32; + let row = (i / cols) as f32; + let cx = grid_rect.left() + col * (col_w + gap); + let cy = grid_rect.top() + row * (card_h + gap); let card = egui::Rect::from_min_size(egui::pos2(cx, cy), egui::vec2(col_w, card_h)); let resp = ui.interact(card, ui.id().with(("mobile_intent", i)), egui::Sense::click()); @@ -102,20 +117,21 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { } } - // Recent projects list in the bottom third. - let recent_top = content_top + grid_h + 14.0; + // Recent projects list (below the grid in portrait, beside it in landscape). + let rleft = recent_rect.left(); + let rright = recent_rect.right(); ui.painter().text( - egui::pos2(left, recent_top), + egui::pos2(rleft, recent_rect.top()), egui::Align2::LEFT_TOP, "Recent", egui::FontId::proportional(13.0), pal.text_dim, ); - let list_top = recent_top + 22.0; + let list_top = recent_rect.top() + 22.0; let recents = app.config.get_recent_files(); if recents.is_empty() { ui.painter().text( - egui::pos2(left, list_top + 8.0), + egui::pos2(rleft, list_top + 8.0), egui::Align2::LEFT_TOP, "No recent projects", egui::FontId::proportional(12.0), @@ -124,13 +140,13 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { } else { let row_h = 38.0; let row_gap = 6.0; - let avail = rect.bottom() - margin - list_top; + let avail = recent_rect.bottom() - list_top; let max_rows = ((avail + row_gap) / (row_h + row_gap)).floor().max(0.0) as usize; let mut chosen: Option = None; for (j, path) in recents.iter().take(max_rows).enumerate() { let ry = list_top + j as f32 * (row_h + row_gap); let row_rect = - egui::Rect::from_min_max(egui::pos2(left, ry), egui::pos2(right, ry + row_h)); + egui::Rect::from_min_max(egui::pos2(rleft, ry), egui::pos2(rright, ry + row_h)); let resp = ui.interact(row_rect, ui.id().with(("mobile_recent", j)), egui::Sense::click()); let p = ui.painter(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index 56cc7e7..e7be8a7 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -82,6 +82,12 @@ pub fn is_mobile_env() -> bool { .unwrap_or(false) } +/// `LB_MOBILE_UI=2` develops the mobile shell in landscape (opens a landscape phone window). +/// Orientation itself is aspect-based at render time; this only picks the initial window aspect. +pub fn is_mobile_landscape_env() -> bool { + std::env::var("LB_MOBILE_UI").map(|v| v == "2").unwrap_or(false) +} + /// The panes that make up the vertical stack, in fixed top→bottom order. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StackPane { @@ -187,8 +193,10 @@ pub struct MobileState { pub weights: [f32; 3], /// Node/Instrument band: false = node editor, true = instrument/preset browser. pub show_instruments: bool, - /// Inspector sheet height as a fraction of the region above the transport. + /// Inspector sheet height as a fraction of the region above the transport (portrait). pub inspector_frac: f32, + /// Inspector side-panel width as a fraction of the region (landscape). + pub inspector_width_frac: f32, /// Whether the inspector sheet is currently shown. Gated to appear on pointer *release* (a tap), /// not on press, so press+drag interactions aren't interrupted by the sheet popping up. pub inspector_visible: bool, @@ -226,6 +234,7 @@ impl Default for MobileState { weights: [1.0, 1.0, 1.0], show_instruments: false, inspector_frac: 0.45, + inspector_width_frac: 0.4, inspector_visible: false, inspector_anchor_y: 0.0, inspector_dismissed: false, @@ -251,12 +260,24 @@ pub fn render_mobile_shell( ) { let pal = Palette::from_theme(rc.shared.theme, ui.ctx()); + // Orientation (aspect-based; rotation = a resize). Published to panes before anything renders. + let landscape = available_rect.width() > available_rect.height(); + rc.shared.is_portrait = !landscape; + // Rotating while 3 panes are open drops to 2 (landscape caps the stack at 2). + if landscape && state.window_count > 2 { + state.window_count = 2; + state.window_top = state.window_top.min(STACK.len().saturating_sub(2)); + } + // Background (device color; bands paint over most of it). ui.painter().rect_filled(available_rect, 0.0, pal.bg); + // In landscape the top bar is folded into the top pane header (no separate band), reclaiming its + // height for the stack. + let topbar_h = if landscape { 0.0 } else { TOPBAR_H }; let topbar_rect = egui::Rect::from_min_max( available_rect.min, - egui::pos2(available_rect.right(), available_rect.top() + TOPBAR_H), + egui::pos2(available_rect.right(), available_rect.top() + topbar_h), ); let transport_rect = egui::Rect::from_min_max( egui::pos2(available_rect.left(), available_rect.bottom() - TRANSPORT_H), @@ -294,38 +315,42 @@ pub fn render_mobile_shell( state.inspector_visible = true; } let mut inspector_shown = state.inspector_visible; - let sheet_h = if inspector_shown { - (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0) - } else { - 0.0 - }; - let mut sheet_top = region.bottom() - sheet_h; - // Tapping outside the sheet (but not on the transport) dismisses (hides) the inspector. We only - // hide it — NOT clear the selection — so actions dispatched from overlays (context menu, "+New" - // grid) still see the selection. It stays dismissed until the selection changes (sig above). + // Inspector geometry: a bottom sheet in portrait, a right-side vertical column in landscape. + let inspector_rect = if landscape { + let w = (region.width() * state.inspector_width_frac).clamp(180.0, region.width() - 140.0); + egui::Rect::from_min_max(egui::pos2(region.right() - w, region.top()), region.max) + } else { + let h = (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0); + egui::Rect::from_min_max(egui::pos2(region.left(), region.bottom() - h), region.max) + }; + + // Tapping outside the inspector (but not on the transport) dismisses (hides) it. We only hide — + // NOT clear the selection — so actions dispatched from overlays (context menu, "+New" grid) still + // see the selection. It stays dismissed until the selection changes (sig above). if inspector_shown { - let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); let dismiss = ui.input(|i| { i.pointer.primary_pressed() && i.pointer.press_origin().map_or(false, |p| { - !sheet_rect.contains(p) && !transport_rect.contains(p) + !inspector_rect.contains(p) && !transport_rect.contains(p) }) }); if dismiss { state.inspector_visible = false; state.inspector_dismissed = true; inspector_shown = false; - sheet_top = region.bottom(); } } - // Reflow (shrink the stack above the sheet) only when the thing that was tapped would actually be - // hidden by the sheet — i.e. the tap landed below where the sheet now sits. Otherwise just - // overlay. Restoring on dismiss is automatic — reflow only changes the render rect. - let covered = inspector_shown && state.inspector_anchor_y > sheet_top; - let stack_rect = if covered { - egui::Rect::from_min_max(region.min, egui::pos2(region.right(), sheet_top)) + // Reflow: shrink the stack to make room for the inspector. Landscape always carves horizontally + // (side-by-side); portrait only carves when the tapped thing sits below the sheet (else overlay). + // Restoring on dismiss is automatic — reflow only changes the render rect. + let stack_rect = if !inspector_shown { + region + } else if landscape { + egui::Rect::from_min_max(region.min, egui::pos2(inspector_rect.left(), region.bottom())) + } else if state.inspector_anchor_y > inspector_rect.top() { + egui::Rect::from_min_max(region.min, egui::pos2(region.right(), inspector_rect.top())) } else { region }; @@ -347,8 +372,7 @@ pub fn render_mobile_shell( stack::render(ui, stack_rect, rc, state, &pal); if inspector_shown { - let sheet_rect = egui::Rect::from_min_max(egui::pos2(region.left(), sheet_top), region.max); - inspector::render(ui, sheet_rect, region.height(), rc, state, &pal); + inspector::render(ui, inspector_rect, region, rc, state, &pal, landscape); } // Transport floor: drawn last = always on top, the persistent spine. @@ -368,7 +392,17 @@ pub fn render_mobile_shell( } // Top bar (filename + ⌕ palette + ⋯ commands). Its menus overlay the whole shell, so it's last. - topbar::render(ui, topbar_rect, available_rect, rc, state, &pal); + // Portrait: its own band at the top. Landscape: folded into the middle of the top pane header. + if landscape { + let hh = stack::header_height(false); + let top_header = egui::Rect::from_min_max( + egui::pos2(stack_rect.left(), region.top()), + egui::pos2(stack_rect.right(), region.top() + hh), + ); + topbar::render_inline(ui, top_header, available_rect, rc, state, &pal); + } else { + topbar::render(ui, topbar_rect, available_rect, rc, state, &pal); + } // Long-press context menu (populated by whichever pane was long-pressed). Persistent popup, // dispatched via pending_menu_actions. diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index ad47a8e..566ba52 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -26,6 +26,12 @@ const N: usize = STACK.len(); /// Height of each band's drag header (and the bottom-edge footer). The whole header is the grab /// target — there's no thin divider bar. const HEADER_H: f32 = 52.0; +/// Landscape headers are shorter — vertical space is scarce, and the top one hosts the app bar. +const HEADER_H_LANDSCAPE: f32 = 34.0; +/// Header height for the current orientation. +pub fn header_height(is_portrait: bool) -> f32 { + if is_portrait { HEADER_H } else { HEADER_H_LANDSCAPE } +} /// Corner radius (px) for the rounded top of each header. const HEADER_RADIUS: u8 = 9; const FOOTER_H: f32 = 28.0; @@ -57,19 +63,19 @@ fn op_r3(top: usize, count: usize) -> Option<(usize, usize)> { fn op_r4(top: usize, count: usize) -> Option<(usize, usize)> { (count == 3).then(|| (top, 2)) // drop bottom } -fn op_r5(top: usize, count: usize) -> Option<(usize, usize)> { - if count < 3 && top + count < N { +fn op_r5(top: usize, count: usize, max: usize) -> Option<(usize, usize)> { + if count < max && top + count < N { Some((top, count + 1)) // grow bottom - } else if count == 3 && top + count < N { + } else if count == max && top + count < N { Some((top + 1, count)) // slide down } else { None } } -fn op_r6(top: usize, count: usize) -> Option<(usize, usize)> { - if count < 3 && top > 0 { +fn op_r6(top: usize, count: usize, max: usize) -> Option<(usize, usize)> { + if count < max && top > 0 { Some((top - 1, count + 1)) // grow top - } else if count == 3 && top > 0 { + } else if count == max && top > 0 { Some((top - 1, count)) // slide up } else { None @@ -84,12 +90,13 @@ fn resolve( top: usize, count: usize, trigger: f32, + max: usize, ) -> Option<(usize, usize, f32)> { let going_up = offset < 0.0; let t = (offset.abs() / trigger.max(1.0)).clamp(0.0, 1.0); let target = match handle { - Handle::TopEdge => (!going_up).then(|| op_r6(top, count)).flatten(), - Handle::BottomEdge => going_up.then(|| op_r5(top, count)).flatten(), + Handle::TopEdge => (!going_up).then(|| op_r6(top, count, max)).flatten(), + Handle::BottomEdge => going_up.then(|| op_r5(top, count, max)).flatten(), Handle::Divider(k) => { if count == 2 { if going_up { op_r1(top, count) } else { op_r2(top, count) } @@ -343,6 +350,9 @@ fn interp_layout( pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state: &mut MobileState, pal: &Palette) { let top = state.window_top; let count = state.window_count; + // Landscape caps the window at 2 panes; portrait allows 3. + let max_panes = if rc.shared.is_portrait { 3 } else { 2 }; + let header_h = header_height(rc.shared.is_portrait); // Reserve a footer bar at the very bottom for the BottomEdge handle. let footer_rect = egui::Rect::from_min_max( @@ -379,7 +389,67 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state }; config_rects(top, count, content_area, &nweights(&w, count)) } - _ => resolve(d.handle, d.offset, top, count, trigger) + Handle::BottomEdge if count == 1 && top + 1 < N => { + // Continuous reveal: dragging the bottom edge up grows the pane below out of the + // bottom, its divider tracking the finger across the FULL height (not just `trigger`), + // so one drag sweeps the current pane → split → revealed-pane-fullscreen. + let frac = (-d.offset / content_area.height().max(1.0)).clamp(0.0, 1.0); + config_rects(top, 2, content_area, &nweights(&[1.0 - frac, frac, 0.0], 2)) + } + Handle::TopEdge if count == 1 && top > 0 => { + // Symmetric reveal of the pane above: dragging the top edge down grows it from the top. + let frac = (d.offset / content_area.height().max(1.0)).clamp(0.0, 1.0); + config_rects(top - 1, 2, content_area, &nweights(&[frac, 1.0 - frac, 0.0], 2)) + } + Handle::BottomEdge if count == 2 && max_panes == 2 => { + // 2-pane (landscape) → 1-pane reveal: dragging the bottom edge up first slides the + // window down to reveal the pane below, then collapses onto it — the whole sweep + // mapped over the FULL height so the dragged boundary reaches the top (rather than + // stalling at the even split after the slide). + let frac = (-d.offset / content_area.height().max(1.0)).clamp(0.0, 1.0); + let even2 = nweights(&even_arr(2), 2); + let even1 = nweights(&even_arr(1), 1); + if top + 2 < N { + if frac <= 0.5 { + let from = config_rects(top, 2, content_area, &even2); + let to = config_rects(top + 1, 2, content_area, &even2); + interp_layout(&from, &to, top, top + 1, frac / 0.5, content_area) + } else { + let from = config_rects(top + 1, 2, content_area, &even2); + let to = config_rects(top + 2, 1, content_area, &even1); + interp_layout(&from, &to, top + 1, top + 2, (frac - 0.5) / 0.5, content_area) + } + } else { + // No pane below → just collapse onto the bottom pane. + let from = config_rects(top, 2, content_area, &even2); + let to = config_rects(top + 1, 1, content_area, &even1); + interp_layout(&from, &to, top, top + 1, frac, content_area) + } + } + Handle::TopEdge if count == 2 && max_panes == 2 => { + // Symmetric 2→1 reveal from the top: dragging the top edge down slides the window up + // to reveal the pane above, then collapses onto it. + let frac = (d.offset / content_area.height().max(1.0)).clamp(0.0, 1.0); + let even2 = nweights(&even_arr(2), 2); + let even1 = nweights(&even_arr(1), 1); + if top > 0 { + if frac <= 0.5 { + let from = config_rects(top, 2, content_area, &even2); + let to = config_rects(top - 1, 2, content_area, &even2); + interp_layout(&from, &to, top, top - 1, frac / 0.5, content_area) + } else { + let from = config_rects(top - 1, 2, content_area, &even2); + let to = config_rects(top - 1, 1, content_area, &even1); + interp_layout(&from, &to, top - 1, top - 1, (frac - 0.5) / 0.5, content_area) + } + } else { + // No pane above → collapse onto the top pane (drop the bottom). + let from = config_rects(top, 2, content_area, &even2); + let to = config_rects(top, 1, content_area, &even1); + interp_layout(&from, &to, top, top, frac, content_area) + } + } + _ => resolve(d.handle, d.offset, top, count, trigger, max_panes) .map(|(tt, tc, t)| { let target = config_rects(tt, tc, content_area, &nweights(&even_arr(tc), tc)); interp_layout(&rest_bands, &target, top, tt, t, content_area) @@ -404,9 +474,9 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state // 1) Pane content, carving the header off the top of each band. for (slot, brect) in &draw_layout { - let header_h = HEADER_H.min(brect.height()); + let hh = header_h.min(brect.height()); let content_rect = egui::Rect::from_min_max( - egui::pos2(brect.left(), brect.top() + header_h), + egui::pos2(brect.left(), brect.top() + hh), brect.max, ); if content_rect.height() > 1.0 { @@ -423,21 +493,21 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, rc: &mut RenderContext, state // 2) Header visuals (animated positions). The fullscreen icon shows "restore" when a single // pane fills the stack. - let fullscreen = count == 1; + let fullscreen = draw_layout.len() == 1; for (slot, brect) in &draw_layout { if brect.height() < 6.0 { continue; } let hr = egui::Rect::from_min_max( brect.left_top(), - egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), + egui::pos2(brect.right(), brect.top() + header_h.min(brect.height())), ); draw_header(ui, hr, STACK[*slot], state.show_instruments, fullscreen, pal); } draw_footer(ui, footer_rect, top + count >= N, pal); // 3) Interactions on the resting header/footer rects (added last → they win the press). - handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, now, state); + handle_interactions(ui, &rest_bands, content_area, footer_rect, trigger, now, state, max_panes, header_h); } fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: bool, fullscreen: bool, pal: &Palette) { @@ -518,10 +588,10 @@ fn handle_key(h: Handle) -> (usize, usize) { } /// Toggle a slot between filling the stack (count==1) and a 2-pane split with an adjacent pane. -fn toggle_fullscreen(state: &mut MobileState, slot: usize, now: f64) { +fn toggle_fullscreen(state: &mut MobileState, slot: usize, now: f64, max: usize) { if state.window_count == 1 && state.window_top == slot { // Restore: split with the pane below if possible, else above. - if let Some((t, c)) = op_r5(slot, 1).or_else(|| op_r6(slot, 1)) { + if let Some((t, c)) = op_r5(slot, 1, max).or_else(|| op_r6(slot, 1, max)) { set_window(state, t, c, now); } } else { @@ -537,13 +607,15 @@ fn handle_interactions( trigger: f32, now: f64, state: &mut MobileState, + max: usize, + header_h: f32, ) { // (handle, header_rect, slot) — slot is Some for band headers, None for the footer. let mut handles: Vec<(Handle, egui::Rect, Option)> = Vec::new(); for (i, (slot, brect)) in rest_bands.iter().enumerate() { let hr = egui::Rect::from_min_max( brect.left_top(), - egui::pos2(brect.right(), brect.top() + HEADER_H.min(brect.height())), + egui::pos2(brect.right(), brect.top() + header_h.min(brect.height())), ); let handle = if i == 0 { Handle::TopEdge } else { Handle::Divider(i - 1) }; handles.push((handle, hr, Some(*slot))); @@ -563,7 +635,7 @@ fn handle_interactions( ); let fsresp = ui.interact(fs, ui.id().with(("mobile_fs", slot)), egui::Sense::click()); if fsresp.clicked() { - toggle_fullscreen(state, slot, now); + toggle_fullscreen(state, slot, now, max); } if STACK[slot] == StackPane::NodeInstrument { let nt = egui::Rect::from_min_max( @@ -590,7 +662,7 @@ fn handle_interactions( } if resp.drag_stopped() { if let Some(d) = state.drag.take() { - commit_drag(d, state, content_area, trigger, now); + commit_drag(d, state, content_area, trigger, now, max); } } if resp.hovered() || state.drag.map(|d| d.handle == handle).unwrap_or(false) { @@ -599,18 +671,81 @@ fn handle_interactions( } } -fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32, now: f64) { +fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, trigger: f32, now: f64, max: usize) { let h = content_area.height().max(1.0); match d.handle { Handle::Divider(k) if k + 1 < state.window_count => { commit_divider(state, k, d.offset / h, now); } + Handle::BottomEdge if state.window_count == 1 && state.window_top + 1 < N => { + // Snap the continuous reveal: near the top → revealed pane fullscreen; barely moved → + // back to the current pane; in between → a 2-pane split at the nearest preset. + let top = state.window_top; + let frac = (-d.offset / h).clamp(0.0, 1.0); + let from_w = [1.0 - frac, frac, 0.0]; + if frac >= COLLAPSE_HI { + begin_anim(state, top, 2, from_w, top + 1, 1, even_arr(1), 0.0, now); + } else if frac <= COLLAPSE_LO { + begin_anim(state, top, 2, from_w, top, 1, even_arr(1), 0.0, now); + } else { + begin_anim(state, top, 2, from_w, top, 2, nearest_preset(&from_w, 2), 0.0, now); + } + } + Handle::TopEdge if state.window_count == 1 && state.window_top > 0 => { + let top = state.window_top; + let frac = (d.offset / h).clamp(0.0, 1.0); + let from_w = [frac, 1.0 - frac, 0.0]; + if frac >= COLLAPSE_HI { + begin_anim(state, top - 1, 2, from_w, top - 1, 1, even_arr(1), 0.0, now); + } else if frac <= COLLAPSE_LO { + begin_anim(state, top - 1, 2, from_w, top, 1, even_arr(1), 0.0, now); + } else { + begin_anim(state, top - 1, 2, from_w, top - 1, 2, nearest_preset(&from_w, 2), 0.0, now); + } + } + Handle::BottomEdge if state.window_count == 2 && max == 2 => { + // Snap the two-phase 2→1 reveal: near the top → revealed pane fullscreen; a middling drag + // → slid down to the next 2-pane split; barely moved → stay put. + let top = state.window_top; + let frac = (-d.offset / h).clamp(0.0, 1.0); + let even2 = even_arr(2); + if top + 2 < N { + if frac >= COLLAPSE_HI { + let t = ((frac - 0.5) / 0.5).clamp(0.0, 1.0); + begin_anim(state, top + 1, 2, even2, top + 2, 1, even_arr(1), t, now); + } else if frac > COLLAPSE_LO { + let t = (frac / 0.5).clamp(0.0, 1.0); + begin_anim(state, top, 2, even2, top + 1, 2, even2, t, now); + } + // else: barely moved → stay on the current [top, top+1] split. + } else if frac >= COLLAPSE_HI { + begin_anim(state, top, 2, even2, top + 1, 1, even_arr(1), frac, now); + } + } + Handle::TopEdge if state.window_count == 2 && max == 2 => { + // Symmetric snap for the top-edge 2→1 reveal. + let top = state.window_top; + let frac = (d.offset / h).clamp(0.0, 1.0); + let even2 = even_arr(2); + if top > 0 { + if frac >= COLLAPSE_HI { + let t = ((frac - 0.5) / 0.5).clamp(0.0, 1.0); + begin_anim(state, top - 1, 2, even2, top - 1, 1, even_arr(1), t, now); + } else if frac > COLLAPSE_LO { + let t = (frac / 0.5).clamp(0.0, 1.0); + begin_anim(state, top, 2, even2, top - 1, 2, even2, t, now); + } + // else: barely moved → stay on the current split. + } else if frac >= COLLAPSE_HI { + begin_anim(state, top, 2, even2, top, 1, even_arr(1), frac, now); + } + } _ => { // Edges (and degenerate dividers): membership transition. Animate from the current // config to the new one, continuing from where the drag's interp left off (~progress t). let top = state.window_top; let count = state.window_count; - if let Some((tt, tc, t)) = resolve(d.handle, d.offset, top, count, trigger) { + if let Some((tt, tc, t)) = resolve(d.handle, d.offset, top, count, trigger, max) { if t >= 0.5 { let from_w = to_arr(&nweights(&state.weights, count)); begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), t, now); @@ -667,17 +802,17 @@ fn commit_divider(state: &mut MobileState, k: usize, offset_frac: f32, now: f64) return; } - // 2-pane: group resize, snap to nearest 2-pane preset, slide off at the extremes. + // 2-pane: group resize snapping to the nearest 2-pane preset, but releasing near an edge + // collapses to a single fullscreen pane (the one that grew). It does NOT slide in the next pane — + // that's the job of the top/bottom edge handles. let (bmin, bmax) = boundary_bounds(count, k); let from_w = to_arr(&weights_for_boundary(&nw, k, b.clamp(bmin, bmax))); if b <= COLLAPSE_LO { - if let Some((tt, tc)) = op_r1(top, count) { - begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), 0.0, now); - } + // Divider dragged to the top → the top pane is squeezed out; the bottom pane goes fullscreen. + begin_anim(state, top, count, from_w, top + 1, 1, even_arr(1), 0.0, now); } else if b >= COLLAPSE_HI { - if let Some((tt, tc)) = op_r2(top, count) { - begin_anim(state, top, count, from_w, tt, tc, even_arr(tc), 0.0, now); - } + // Divider dragged to the bottom → the bottom pane is squeezed out; the top pane goes fullscreen. + begin_anim(state, top, count, from_w, top, 1, even_arr(1), 0.0, now); } else { let to_w = nearest_preset(&from_w, count); begin_anim(state, top, count, from_w, top, count, to_w, 0.0, now); diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs index 982382e..50f4858 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs @@ -100,6 +100,65 @@ pub fn render( } } +/// Landscape: the app bar folded into the middle of the top pane header — filename + ⌕ + ⋯ as a +/// centered cluster (the pane's own grip/label sits to the left, its buttons to the right). No bar +/// background or divider line; the header already drew them. +pub fn render_inline( + ui: &mut egui::Ui, + header: egui::Rect, + full: egui::Rect, + rc: &mut RenderContext, + state: &mut MobileState, + pal: &Palette, +) { + let name = rc + .shared + .container_path + .as_ref() + .and_then(|p| p.file_name()) + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "Lightningbeam".to_string()); + let galley = ui + .painter() + .layout_no_wrap(name, egui::FontId::proportional(14.0), pal.text); + let name_w = galley.rect.width(); + let name_h = galley.rect.height(); + let btn = header.height().min(BTN); + let gap = 6.0; + let cluster_w = name_w + gap + 2.0 * btn; + let x0 = (header.center().x - cluster_w / 2.0).max(header.left() + 8.0); + let cy = header.center().y; + let bt = header.top() + (header.height() - btn) / 2.0; + + ui.painter().galley(egui::pos2(x0, cy - name_h / 2.0), galley, pal.text); + + let palette_rect = egui::Rect::from_min_size(egui::pos2(x0 + name_w + gap, bt), egui::vec2(btn, btn)); + let overflow_rect = egui::Rect::from_min_size(egui::pos2(palette_rect.right(), bt), egui::vec2(btn, btn)); + + let sresp = ui.interact(palette_rect, ui.id().with("mobile_topbar_search"), egui::Sense::click()); + ui.painter().text(palette_rect.center(), egui::Align2::CENTER_CENTER, icons::SEARCH, icons::font(17.0), + if sresp.hovered() || state.palette_open { pal.text } else { pal.text_dim }); + if sresp.clicked() { + state.palette_open = !state.palette_open; + state.overflow_open = false; + state.palette_query.clear(); + } + + let oresp = ui.interact(overflow_rect, ui.id().with("mobile_topbar_overflow"), egui::Sense::click()); + ui.painter().text(overflow_rect.center(), egui::Align2::CENTER_CENTER, icons::ELLIPSIS, icons::font(18.0), + if oresp.hovered() || state.overflow_open { pal.text } else { pal.text_dim }); + if oresp.clicked() { + state.overflow_open = !state.overflow_open; + state.palette_open = false; + } + + if state.overflow_open { + render_overflow(ui, full, rc, state, pal); + } else if state.palette_open { + render_palette(ui, full, rc, state, pal); + } +} + /// Common modal scrim + panel. Returns (backdrop-tapped, panel inner rect). fn open_panel(ui: &mut egui::Ui, full: egui::Rect, id: &str, pal: &Palette) -> (bool, egui::Rect) { let scrim = ui.interact(full, ui.id().with(("mobile_topbar_scrim", id)), egui::Sense::click()); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index fdca5ce..d79eebe 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -364,6 +364,10 @@ pub struct SharedPaneState<'a> { pub brush_preview_pixels: &'a std::sync::Arc)>>>, /// True when rendering the phone/mobile shell (panes can render more compactly). pub is_mobile: bool, + /// Device orientation for the mobile shell: portrait (tall) vs landscape (wide). Defaults to + /// `true`; the mobile shell sets it from the available rect. Panes that reflow (e.g. the Piano + /// Roll's vertical vs conventional layout) key off this. + pub is_portrait: bool, /// Shared keyboard octave offset (C4-relative), so the mobile Virtual Piano and the portrait /// Piano Roll agree on which keys are visible and stay column-aligned. pub keyboard_octave: &'a mut i8, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs index 19ddd0b..08296f6 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs @@ -173,6 +173,8 @@ pub struct PianoRollPane { // Mobile portrait "Synthesia" mode: the playable keyboard is drawn as a strip at the bottom of // this pane (one unified surface), reusing the Virtual Piano's rendering + MIDI logic. keyboard: super::virtual_piano::VirtualPianoPane, + // Landscape mobile: the Keys/Notes toggle — true shows the full keyboard, false the roll. + landscape_keys: bool, // Manual long-press detection for the vertical roll (works with mouse-hold and touch): time the // press started, and where. `None` = disarmed (no fresh press, or the press became a pan). lp_press_time: Option, @@ -218,6 +220,7 @@ impl PianoRollPane { last_snap_selection: HashSet::new(), snap_user_changed: false, keyboard: super::virtual_piano::VirtualPianoPane::new(), + landscape_keys: false, lp_press_time: None, lp_press_pos: egui::Pos2::ZERO, editing_index: None, @@ -385,6 +388,20 @@ impl PianoRollPane { rect: Rect, shared: &mut SharedPaneState, ) { + // Landscape mobile: a Keys/Notes toggle bar switches this pane between the full keyboard and + // the conventional piano roll. (Portrait uses the keyboard-primary "Synthesia" view below.) + let mut rect = rect; + if shared.is_mobile && !shared.is_portrait { + let bar = Rect::from_min_max(rect.min, pos2(rect.right(), rect.min.y + 30.0)); + rect = Rect::from_min_max(pos2(rect.left(), bar.bottom()), rect.max); + self.render_keys_notes_toggle(ui, bar, shared); + if self.landscape_keys { + let kb_path: NodePath = Vec::new(); + self.keyboard.render_content(ui, rect, &kb_path, shared); + return; + } + } + let keyboard_rect = Rect::from_min_size(rect.min, vec2(KEYBOARD_WIDTH, rect.height())); let grid_rect = Rect::from_min_max( pos2(rect.min.x + KEYBOARD_WIDTH, rect.min.y), @@ -461,9 +478,9 @@ impl PianoRollPane { } } - // Mobile: keyboard-primary "Synthesia" instrument surface (keys + falling-notes roll). Always - // used on mobile (device is portrait; the landscape/conventional flip is P7). - if shared.is_mobile { + // Mobile portrait: keyboard-primary "Synthesia" instrument surface (keys + falling-notes + // roll). Landscape falls through to the conventional horizontal roll (pitch↕ / time▸). + if shared.is_mobile && shared.is_portrait { self.render_vertical_mode(ui, rect, shared, &clip_data); return; } @@ -910,6 +927,42 @@ impl PianoRollPane { } } + /// Landscape Keys/Notes segmented toggle drawn in `bar` (updates `self.landscape_keys`). + fn render_keys_notes_toggle(&mut self, ui: &mut egui::Ui, bar: Rect, shared: &mut SharedPaneState) { + let painter = ui.painter_at(bar); + let bg = shared.theme.bg_color(&[".pane-header"], ui.ctx(), Color32::from_rgb(24, 24, 28)); + painter.rect_filled(bar, 0.0, bg); + painter.line_segment( + [pos2(bar.left(), bar.bottom()), pos2(bar.right(), bar.bottom())], + Stroke::new(1.0, Color32::from_gray(60)), + ); + + let seg_w = 74.0; + let x0 = bar.center().x - seg_w; + let keys_rect = Rect::from_min_size(pos2(x0, bar.top() + 4.0), vec2(seg_w, bar.height() - 8.0)); + let notes_rect = Rect::from_min_size(pos2(x0 + seg_w, bar.top() + 4.0), vec2(seg_w, bar.height() - 8.0)); + if ui.interact(keys_rect, ui.id().with("pr_kn_keys"), egui::Sense::click()).clicked() { + self.landscape_keys = true; + } + if ui.interact(notes_rect, ui.id().with("pr_kn_notes"), egui::Sense::click()).clicked() { + self.landscape_keys = false; + } + let accent = Color32::from_rgb(0x4a, 0xa3, 0xff); + for (r, label, on) in [ + (keys_rect, "Keys", self.landscape_keys), + (notes_rect, "Notes", !self.landscape_keys), + ] { + painter.rect_filled(r, 6.0, if on { accent } else { Color32::from_gray(46) }); + painter.text( + r.center(), + egui::Align2::CENTER_CENTER, + label, + egui::FontId::proportional(13.0), + if on { Color32::WHITE } else { Color32::from_gray(200) }, + ); + } + } + fn render_keyboard(&self, painter: &egui::Painter, rect: Rect) { // Background painter.rect_filled(rect, 0.0, Color32::from_rgb(40, 40, 45)); From 9646eef487255a43b6b73aede3bd0cf17f16fa29 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 3 Jul 2026 01:08:05 -0400 Subject: [PATCH 27/43] Mobile: hide the desktop menu bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate the top menu bar on !mobile_active(). Its commands are all reachable from the mobile shell (⌕ command palette, ⋯ overflow, and the stage/timeline context menus), so the desktop menu bar is redundant on mobile and just wastes a strip of vertical space. Desktop unchanged. --- lightningbeam-ui/lightningbeam-editor/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 10d5a6a..38acae8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6577,7 +6577,9 @@ impl eframe::App for EditorApp { } } - // Top menu bar (egui-rendered on all platforms) + // Top menu bar (egui-rendered on desktop). On mobile the shell's ⌕ palette + ⋯ overflow + // expose every command, so the desktop menu bar is suppressed. + if !self.mobile_active() { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { if let Some(menu_system) = &self.menu_system { let recent_files = self.config.get_recent_files(); @@ -6613,6 +6615,7 @@ impl eframe::App for EditorApp { } } }); + } // Render start screen or editor based on app mode if self.app_mode == AppMode::StartScreen { From bb6b6fa9e3290fb6d539a6c4cb40239bc3f537aa Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 3 Jul 2026 01:55:40 -0400 Subject: [PATCH 28/43] Mobile: address code-review findings Fixes from a review of the mobile-UI branch. - Guard the inspector size clamps so min<=max (f32::clamp panics otherwise) and set a window min_inner_size; prevents a crash / degenerate layout at small sizes. Same small-size guards for the piano-roll landscape toggle bar and the intent grid. - Landscape 2->1 edge reveal: snap to fullscreen at frac>=0.5 (the preview's phase boundary) instead of COLLAPSE_HI, so releasing mid-drag no longer jumps backward. Both edges. - Gate the double-tap-drag marquee to mobile so it can't hijack a desktop Brush/Draw drag. - Timeline mobile long-press: request a repaint while counting down so it fires even when the finger is held perfectly still. - Landscape folded top bar: lay the filename+search+overflow cluster out within a reserved center span (gutters for the pane's own label/buttons) and elide the filename, so it can't overlap or overflow the header. - Node editor connect/disconnect: assert + document the top-level-graph assumption. - Palette search field focuses only when unfocused (no per-frame focus stomp); reset the inspector tap-anchor on orientation change; remove stale main.rs.backup. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/main.rs | 1 + .../lightningbeam-editor/src/main.rs.backup | 1568 ----------------- .../lightningbeam-editor/src/mobile/intent.rs | 5 +- .../lightningbeam-editor/src/mobile/mod.rs | 20 +- .../lightningbeam-editor/src/mobile/stack.rs | 13 +- .../lightningbeam-editor/src/mobile/topbar.rs | 34 +- .../src/panes/node_graph/mobile.rs | 7 +- .../src/panes/piano_roll.rs | 4 +- .../lightningbeam-editor/src/panes/stage.rs | 10 +- .../src/panes/timeline.rs | 5 + 10 files changed, 75 insertions(+), 1592 deletions(-) delete mode 100644 lightningbeam-ui/lightningbeam-editor/src/main.rs.backup diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 38acae8..c719a09 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -211,6 +211,7 @@ fn main() -> eframe::Result { let mut viewport_builder = egui::ViewportBuilder::default() .with_inner_size(initial_size) + .with_min_inner_size([360.0, 300.0]) // keep layouts (esp. the mobile shell) above degenerate sizes .with_title("Lightningbeam Editor") .with_app_id("lightningbeam-editor"); // Set app_id for Wayland diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs.backup b/lightningbeam-ui/lightningbeam-editor/src/main.rs.backup deleted file mode 100644 index 3ffdea7..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs.backup +++ /dev/null @@ -1,1568 +0,0 @@ -use eframe::egui; -use lightningbeam_core::layout::{LayoutDefinition, LayoutNode}; -use lightningbeam_core::pane::PaneType; -use lightningbeam_core::tool::Tool; -use std::collections::HashMap; -use clap::Parser; -use uuid::Uuid; - -mod panes; -use panes::{PaneInstance, PaneRenderer, SharedPaneState}; - -mod menu; -use menu::{MenuAction, MenuSystem}; - -mod theme; -use theme::{Theme, ThemeMode}; - -/// Lightningbeam Editor - Animation and video editing software -#[derive(Parser, Debug)] -#[command(name = "Lightningbeam Editor")] -#[command(author, version, about, long_about = None)] -struct Args { - /// Use light theme - #[arg(long, conflicts_with = "dark")] - light: bool, - - /// Use dark theme - #[arg(long, conflicts_with = "light")] - dark: bool, -} - -fn main() -> eframe::Result { - println!("🚀 Starting Lightningbeam Editor..."); - - // Parse command line arguments - let args = Args::parse(); - - // Determine theme mode from arguments - let theme_mode = if args.light { - ThemeMode::Light - } else if args.dark { - ThemeMode::Dark - } else { - ThemeMode::System - }; - - // Load theme - let mut theme = Theme::load_default().expect("Failed to load theme"); - theme.set_mode(theme_mode); - println!("✅ Loaded theme with {} selectors (mode: {:?})", theme.len(), theme_mode); - - // Debug: print theme info - theme.debug_print(); - - // Load layouts from JSON - let layouts = load_layouts(); - println!("✅ Loaded {} layouts", layouts.len()); - for layout in &layouts { - println!(" - {}: {}", layout.name, layout.description); - } - - // Initialize native menus for macOS (app-wide, doesn't need window) - #[cfg(target_os = "macos")] - { - if let Ok(menu_system) = MenuSystem::new() { - menu_system.init_for_macos(); - println!("✅ Native macOS menus initialized"); - } - } - - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_inner_size([1920.0, 1080.0]) - .with_title("Lightningbeam Editor"), - ..Default::default() - }; - - eframe::run_native( - "Lightningbeam Editor", - options, - Box::new(move |cc| Ok(Box::new(EditorApp::new(cc, layouts, theme)))), - ) -} - -fn load_layouts() -> Vec { - let json = include_str!("../assets/layouts.json"); - serde_json::from_str(json).expect("Failed to parse layouts.json") -} - -/// 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, -} - -/// 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 { - icons: HashMap::new(), - assets_path, - } - } - - 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 for event checking - pending_view_action: Option, // Pending view action (zoom, recenter) to be handled by hovered pane - theme: Theme, // Theme system for colors and dimensions - action_executor: lightningbeam_core::action::ActionExecutor, // Action system for undo/redo - active_layer_id: Option, // Currently active layer for editing - selection: lightningbeam_core::selection::Selection, // Current selection state - tool_state: lightningbeam_core::tool::ToolState, // Current tool interaction state -} - -impl EditorApp { - fn new(cc: &eframe::CreationContext, layouts: Vec, theme: Theme) -> Self { - let current_layout = layouts[0].layout.clone(); - - // Initialize native menu system - let menu_system = MenuSystem::new().ok(); - - // Create default document with a simple test scene - let mut document = lightningbeam_core::document::Document::with_size("Untitled Animation", 1920.0, 1080.0) - .with_duration(10.0) - .with_framerate(60.0); - - // Add a test layer with a simple shape to visualize - use lightningbeam_core::layer::{AnyLayer, VectorLayer}; - use lightningbeam_core::object::Object; - use lightningbeam_core::shape::{Shape, ShapeColor}; - use vello::kurbo::{Circle, Shape as KurboShape}; - - let circle = Circle::new((200.0, 150.0), 50.0); - let path = circle.to_path(0.1); - let shape = Shape::new(path).with_fill(ShapeColor::rgb(100, 150, 250)); - let object = Object::new(shape.id); - - let mut vector_layer = VectorLayer::new("Layer 1"); - vector_layer.add_shape(shape); - vector_layer.add_object(object); - let layer_id = document.root.add_child(AnyLayer::Vector(vector_layer)); - - // Wrap document in ActionExecutor - let action_executor = lightningbeam_core::action::ActionExecutor::new(document); - - 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, - pending_view_action: None, - theme, - action_executor, - active_layer_id: Some(layer_id), - selection: lightningbeam_core::selection::Selection::new(), - tool_state: lightningbeam_core::tool::ToolState::default(), - } - } - - 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 => { - if self.action_executor.undo() { - println!("Undid: {}", self.action_executor.redo_description().unwrap_or_default()); - } else { - println!("Nothing to undo"); - } - } - MenuAction::Redo => { - if self.action_executor.redo() { - println!("Redid: {}", self.action_executor.undo_description().unwrap_or_default()); - } else { - println!("Nothing to 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 => { - self.pending_view_action = Some(MenuAction::ZoomIn); - } - MenuAction::ZoomOut => { - self.pending_view_action = Some(MenuAction::ZoomOut); - } - MenuAction::ActualSize => { - self.pending_view_action = Some(MenuAction::ActualSize); - } - MenuAction::RecenterView => { - self.pending_view_action = Some(MenuAction::RecenterView); - } - 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) { - // Disable egui's built-in Ctrl+Plus/Minus zoom behavior - // We handle zoom ourselves for the Stage pane - ctx.options_mut(|o| { - o.zoom_with_keyboard = false; - }); - - // Check for native menu events (macOS) - if let Some(menu_system) = &self.menu_system { - if let Some(action) = menu_system.check_events() { - self.handle_menu_action(action); - } - } - - // Check keyboard shortcuts (works on all platforms) - ctx.input(|i| { - if let Some(action) = MenuSystem::check_shortcuts(i) { - self.handle_menu_action(action); - } - }); - - // Top menu bar (egui-rendered on all platforms) - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - if let Some(action) = MenuSystem::render_egui_menu_bar(ui) { - self.handle_menu_action(action); - } - }); - - // Main pane area - let mut layout_action: Option = None; - egui::CentralPanel::default().show(ctx, |ui| { - let available_rect = ui.available_rect_before_wrap(); - - // Reset hovered divider each frame - self.hovered_divider = None; - - // Track fallback pane priority for view actions (reset each frame) - let mut fallback_pane_priority: Option = None; - - // Registry for view action handlers (two-phase dispatch) - let mut pending_handlers: Vec = Vec::new(); - - 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 - &mut self.pending_view_action, - &mut fallback_pane_priority, - &mut pending_handlers, - &self.theme, - self.action_executor.document(), - &mut self.selection, - &self.active_layer_id, - &mut self.tool_state, - ); - - // Execute action on the best handler (two-phase dispatch) - if let Some(action) = &self.pending_view_action { - if let Some(best_handler) = pending_handlers.iter().min_by_key(|h| h.priority) { - // Look up the pane instance and execute the action - if let Some(pane_instance) = self.pane_instances.get_mut(&best_handler.pane_path) { - match pane_instance { - panes::PaneInstance::Stage(stage_pane) => { - stage_pane.execute_view_action(action, best_handler.zoom_center); - } - _ => {} // Other pane types don't handle view actions yet - } - } - } - // Clear the pending action after execution - self.pending_view_action = None; - } - - // 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 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, - pending_view_action: &mut Option, - fallback_pane_priority: &mut Option, - pending_handlers: &mut Vec, - theme: &Theme, - document: &lightningbeam_core::document::Document, - selection: &mut lightningbeam_core::selection::Selection, - active_layer_id: &Option, - tool_state: &mut lightningbeam_core::tool::ToolState, -) { - match node { - LayoutNode::Pane { name } => { - 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, pending_view_action, fallback_pane_priority, pending_handlers, theme, document, selection, active_layer_id); - } - 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 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); - - // Render children - 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, - pending_view_action, - fallback_pane_priority, - pending_handlers, - theme, - document, - selection, - active_layer_id, - ); - - 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, - pending_view_action, - fallback_pane_priority, - pending_handlers, - theme, - document, - selection, - active_layer_id, - ); - - // 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) - }; - - ui.painter().vline( - split_x, - rect.y_range(), - 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 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); - - // Render children - 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, - pending_view_action, - fallback_pane_priority, - pending_handlers, - theme, - document, - selection, - active_layer_id, - ); - - 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, - pending_view_action, - fallback_pane_priority, - pending_handlers, - theme, - document, - selection, - active_layer_id, - ); - - // 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) - }; - - ui.painter().hline( - rect.x_range(), - split_y, - egui::Stroke::new(2.0, divider_color), - ); - } - } -} - -/// Render a single pane with its content -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, - pending_view_action: &mut Option, - fallback_pane_priority: &mut Option, - pending_handlers: &mut Vec, - theme: &Theme, - document: &lightningbeam_core::document::Document, - selection: &mut lightningbeam_core::selection::Selection, - active_layer_id: &Option, -) { - let pane_type = PaneType::from_name(pane_name); - - // 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 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(border_width, border_color), - ); - - // 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.as_str() - }; - let title_pos = header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0, header_height / 2.0); - ui.painter().text( - title_pos, - egui::Align2::LEFT_CENTER, - title_text, - egui::FontId::proportional(14.0), - egui::Color32::from_gray(220), - ); - - // Create header controls area (positioned after title) - let title_width = 150.0; // Approximate width for title - let header_controls_rect = egui::Rect::from_min_size( - header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0 + title_width, 0.0), - egui::vec2(header_rect.width() - (icon_padding * 2.0 + icon_size + 8.0 + title_width), header_height), - ); - - // Render pane-specific header controls (if pane has them) - if let Some(pane_type) = pane_type { - // Get or create pane instance for header rendering - 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(), panes::PaneInstance::new(pane_type)); - } - - if let Some(pane_instance) = pane_instances.get_mut(path) { - let mut header_ui = ui.new_child(egui::UiBuilder::new().max_rect(header_controls_rect).layout(egui::Layout::left_to_right(egui::Align::Center))); - let mut shared = panes::SharedPaneState { - tool_icon_cache, - icon_cache, - selected_tool, - fill_color, - stroke_color, - pending_view_action, - fallback_pane_priority, - theme, - pending_handlers, - document, - selection, - active_layer_id, - }; - pane_instance.render_header(&mut header_ui, &mut shared); - } - } - - // Make pane content clickable (use full rect for split preview interaction) - let pane_id = ui.id().with(("pane", path)); - let response = ui.interact(rect, pane_id, egui::Sense::click()); - - // 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 its content - 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, - pending_view_action, - fallback_pane_priority, - theme, - pending_handlers, - document, - selection, - active_layer_id, - }; - - // Render pane content (header was already rendered above) - pane_instance.render_content(ui, 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), - ); - } - - // Handle split preview mode (rendered AFTER pane content for proper z-ordering) - 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), - ); - } - - // Create a high-priority interaction for split preview (rendered last = highest priority) - let split_preview_id = ui.id().with(("split_preview", path)); - let split_response = ui.interact(rect, split_preview_id, egui::Sense::click()); - - // If clicked, perform the split - if split_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 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 -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::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::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/mobile/intent.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs index b48e0ea..86f948a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs @@ -81,8 +81,9 @@ pub fn render(app: &mut EditorApp, ctx: &egui::Context) { ) }; - let col_w = (grid_rect.width() - (cols as f32 - 1.0) * gap) / cols as f32; - let card_h = (grid_rect.height() - (rows as f32 - 1.0) * gap) / rows as f32; + // Floor at 0 so a very small window can't produce negative/inverted card rects. + let col_w = ((grid_rect.width() - (cols as f32 - 1.0) * gap) / cols as f32).max(0.0); + let card_h = ((grid_rect.height() - (rows as f32 - 1.0) * gap) / rows as f32).max(0.0); for (i, intent) in intents(&pal).iter().enumerate() { let col = (i % cols) as f32; let row = (i / cols) as f32; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index e7be8a7..df1ec1f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -197,6 +197,8 @@ pub struct MobileState { pub inspector_frac: f32, /// Inspector side-panel width as a fraction of the region (landscape). pub inspector_width_frac: f32, + /// Last frame's orientation (landscape?), to reset orientation-specific cached state on rotation. + pub was_landscape: bool, /// Whether the inspector sheet is currently shown. Gated to appear on pointer *release* (a tap), /// not on press, so press+drag interactions aren't interrupted by the sheet popping up. pub inspector_visible: bool, @@ -235,6 +237,7 @@ impl Default for MobileState { show_instruments: false, inspector_frac: 0.45, inspector_width_frac: 0.4, + was_landscape: false, inspector_visible: false, inspector_anchor_y: 0.0, inspector_dismissed: false, @@ -268,6 +271,13 @@ pub fn render_mobile_shell( state.window_count = 2; state.window_top = state.window_top.min(STACK.len().saturating_sub(2)); } + // On an orientation change, the cached inspector tap-anchor is in the old layout's coordinate + // space; drop it to a non-covering default so the portrait reflow doesn't mis-decide until the + // next tap re-anchors. + if landscape != state.was_landscape { + state.was_landscape = landscape; + state.inspector_anchor_y = available_rect.top(); + } // Background (device color; bands paint over most of it). ui.painter().rect_filled(available_rect, 0.0, pal.bg); @@ -317,11 +327,17 @@ pub fn render_mobile_shell( let mut inspector_shown = state.inspector_visible; // Inspector geometry: a bottom sheet in portrait, a right-side vertical column in landscape. + // Clamp defensively: on a very small region the desired minimum can exceed the available space, + // and `f32::clamp` panics if `min > max`, so keep `min <= max`. let inspector_rect = if landscape { - let w = (region.width() * state.inspector_width_frac).clamp(180.0, region.width() - 140.0); + let lo = 180.0_f32.min(region.width()); + let hi = (region.width() - 140.0).max(lo); + let w = (region.width() * state.inspector_width_frac).clamp(lo, hi); egui::Rect::from_min_max(egui::pos2(region.right() - w, region.top()), region.max) } else { - let h = (region.height() * state.inspector_frac).clamp(120.0, region.height() - 60.0); + let lo = 120.0_f32.min(region.height()); + let hi = (region.height() - 60.0).max(lo); + let h = (region.height() * state.inspector_frac).clamp(lo, hi); egui::Rect::from_min_max(egui::pos2(region.left(), region.bottom() - h), region.max) }; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs index 566ba52..ab703a1 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/stack.rs @@ -704,13 +704,14 @@ fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, } } Handle::BottomEdge if state.window_count == 2 && max == 2 => { - // Snap the two-phase 2→1 reveal: near the top → revealed pane fullscreen; a middling drag - // → slid down to the next 2-pane split; barely moved → stay put. + // Snap the two-phase 2→1 reveal to match the preview's phases: past the midpoint (phase 2, + // collapsing) → revealed pane fullscreen; a middling drag (phase 1, sliding) → the next + // 2-pane split; barely moved → stay put. let top = state.window_top; let frac = (-d.offset / h).clamp(0.0, 1.0); let even2 = even_arr(2); if top + 2 < N { - if frac >= COLLAPSE_HI { + if frac >= 0.5 { let t = ((frac - 0.5) / 0.5).clamp(0.0, 1.0); begin_anim(state, top + 1, 2, even2, top + 2, 1, even_arr(1), t, now); } else if frac > COLLAPSE_LO { @@ -718,7 +719,7 @@ fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, begin_anim(state, top, 2, even2, top + 1, 2, even2, t, now); } // else: barely moved → stay on the current [top, top+1] split. - } else if frac >= COLLAPSE_HI { + } else if frac >= 0.5 { begin_anim(state, top, 2, even2, top + 1, 1, even_arr(1), frac, now); } } @@ -728,7 +729,7 @@ fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, let frac = (d.offset / h).clamp(0.0, 1.0); let even2 = even_arr(2); if top > 0 { - if frac >= COLLAPSE_HI { + if frac >= 0.5 { let t = ((frac - 0.5) / 0.5).clamp(0.0, 1.0); begin_anim(state, top - 1, 2, even2, top - 1, 1, even_arr(1), t, now); } else if frac > COLLAPSE_LO { @@ -736,7 +737,7 @@ fn commit_drag(d: StackDrag, state: &mut MobileState, content_area: egui::Rect, begin_anim(state, top, 2, even2, top - 1, 2, even2, t, now); } // else: barely moved → stay on the current split. - } else if frac >= COLLAPSE_HI { + } else if frac >= 0.5 { begin_anim(state, top, 2, even2, top, 1, even_arr(1), frac, now); } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs index 50f4858..5fd2815 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/topbar.rs @@ -118,15 +118,31 @@ pub fn render_inline( .and_then(|p| p.file_name()) .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "Lightningbeam".to_string()); - let galley = ui - .painter() - .layout_no_wrap(name, egui::FontId::proportional(14.0), pal.text); - let name_w = galley.rect.width(); - let name_h = galley.rect.height(); let btn = header.height().min(BTN); let gap = 6.0; + // Reserve gutters so the cluster never overlaps the pane header's own grip/label (left) or its + // fullscreen / node-toggle buttons (right). + let left_gutter = 150.0; + let right_gutter = 2.0 * BTN + 16.0; + let span_left = header.left() + left_gutter; + let span_right = (header.right() - right_gutter).max(span_left); + let span_w = span_right - span_left; + + // Filename elided to fit whatever's left after the two buttons. + let max_name_w = (span_w - gap - 2.0 * btn).max(0.0); + let galley = { + let mut job = + egui::text::LayoutJob::simple_singleline(name, egui::FontId::proportional(14.0), pal.text); + job.wrap.max_width = max_name_w; + job.wrap.max_rows = 1; + job.wrap.break_anywhere = true; + ui.painter().layout_job(job) + }; + let name_w = galley.rect.width(); + let name_h = galley.rect.height(); let cluster_w = name_w + gap + 2.0 * btn; - let x0 = (header.center().x - cluster_w / 2.0).max(header.left() + 8.0); + // Center within the reserved span, then clamp so the right edge stays inside it. + let x0 = (span_left + (span_w - cluster_w) / 2.0).clamp(span_left, (span_right - cluster_w).max(span_left)); let cy = header.center().y; let bt = header.top() + (header.height() - btn) / 2.0; @@ -223,7 +239,11 @@ fn render_palette(ui: &mut egui::Ui, full: egui::Rect, rc: &mut RenderContext, s .hint_text("Search commands…") .desired_width(inner.width()), ); - te.request_focus(); + // Focus the field only when it isn't already focused — re-requesting every frame would stomp + // focus and prevent anything else in the panel from taking it. + if !te.has_focus() { + te.request_focus(); + } // Filtered list. let q = state.palette_query.to_lowercase(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs index c35a0c0..051a29d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs @@ -607,6 +607,9 @@ impl NodeGraphPane { /// Cable an output port → input port: update the frontend graph + dispatch `Connect`. fn mobile_connect(&mut self, out_node: NodeId, out_port: usize, in_node: NodeId, in_port: usize) { + // Mobile has no subgraph navigation yet, so cables always target the track-level graph. If + // subgraph nav is added, route through `va_context()` / `graph_connect_in_template` like desktop. + debug_assert!(self.subgraph_stack.is_empty(), "mobile node connect assumes the top-level graph"); let Some(track_id) = self.track_id else { return }; let output_id = self.state.graph.nodes.get(out_node).and_then(|n| n.outputs.get(out_port)).map(|(_, id)| *id); let input_id = self.state.graph.nodes.get(in_node).and_then(|n| n.inputs.get(in_port)).map(|(_, id)| *id); @@ -625,8 +628,10 @@ impl NodeGraphPane { } } - /// Remove a cable: update the frontend graph + dispatch `Disconnect`. + /// Remove a cable: update the frontend graph + dispatch `Disconnect`. Like `mobile_connect`, this + /// assumes the track-level graph (mobile has no subgraph navigation). fn mobile_disconnect(&mut self, out_node: NodeId, out_port: usize, in_node: NodeId, in_port: usize) { + debug_assert!(self.subgraph_stack.is_empty(), "mobile node disconnect assumes the top-level graph"); let Some(track_id) = self.track_id else { return }; let output_id = self.state.graph.nodes.get(out_node).and_then(|n| n.outputs.get(out_port)).map(|(_, id)| *id); let input_id = self.state.graph.nodes.get(in_node).and_then(|n| n.inputs.get(in_port)).map(|(_, id)| *id); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs index 08296f6..ad47424 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/piano_roll.rs @@ -392,7 +392,9 @@ impl PianoRollPane { // the conventional piano roll. (Portrait uses the keyboard-primary "Synthesia" view below.) let mut rect = rect; if shared.is_mobile && !shared.is_portrait { - let bar = Rect::from_min_max(rect.min, pos2(rect.right(), rect.min.y + 30.0)); + // Clamp the bar height so a very short pane can't invert the remaining content rect. + let bar_h = 30.0_f32.min(rect.height()); + let bar = Rect::from_min_max(rect.min, pos2(rect.right(), rect.min.y + bar_h)); rect = Rect::from_min_max(pos2(rect.left(), bar.bottom()), rect.max); self.render_keys_notes_toggle(ui, bar, shared); if self.landscape_keys { diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index bef45be..5c2f15b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -11407,12 +11407,12 @@ impl StagePane { } } - // P5 gesture: double-tap-drag on empty space → transient marquee, regardless of the active - // tool. The second tap arms it; a drag then drives ToolState::MarqueeSelecting (resolved by - // the release handler above). While armed we suppress the normal tool dispatch so a + // P5 gesture (mobile): double-tap-drag on empty space → transient marquee, regardless of the + // active tool. The second tap arms it; a drag then drives ToolState::MarqueeSelecting (resolved + // by the release handler above). While armed we suppress the normal tool dispatch so a // brush/etc. doesn't paint during the gesture. A double-tap without a drag falls through - // (Select tool handles zoom-to-fit). - if !is_replaying && !alt_held { + // (Select tool handles zoom-to-fit). Mobile-only so it can't hijack a desktop Brush/Draw drag. + if shared.is_mobile && !is_replaying && !alt_held { use vello::kurbo::Point; let point = Point::new(world_pos.x as f64, world_pos.y as f64); let press_pos = mouse_canvas_pos.to_pos2(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index b139a57..793835b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -5891,6 +5891,11 @@ impl PaneRenderer for TimelinePane { } else if now - t0 >= 0.4 { self.lp_time = None; long_press = Some(self.lp_pos); + } else { + // Still counting down — request a repaint so the threshold is re-evaluated even if + // the finger is perfectly still and egui would otherwise go idle (no input events). + let remaining = (0.4 - (now - t0)).max(0.0); + ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(remaining)); } } } From 6e361aa30c083c852426763de2620e3f12b3c4bb Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 03:36:56 -0400 Subject: [PATCH 29/43] Reset audio backend on new file / project load The backend's Command::Reset (full teardown: rebuilds Project, audio/buffer pools, ID counters) was never invoked from the UI. New File and project loads only cleared app-side maps, so the previous file's tracks and loaded instruments stayed resident in the backend and kept getting mixed on every audio callback. Add a reset_audio_backend() helper (controller.reset() + clear app-side track maps and backend-derived caches) and call it in the three teardown paths: NewFile, create_new_project_with_focus, and apply_loaded_project. Also drops a duplicate layer_to_track_map.clear() in the NewFile handler. Race-free: the audio thread drains all command_tx commands before any query_tx queries each callback, so Reset (a command) runs before the track/pool rebuild (queries), even though they travel on separate channels. --- .../lightningbeam-editor/src/main.rs | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index c719a09..40f9e37 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -1735,10 +1735,39 @@ impl EditorApp { ); } + /// Tear down the audio backend for the currently-open project. + /// + /// Sends `Command::Reset` (fully rebuilds the backend `Project`, audio/buffer pools, and ID + /// counters) and clears the app-side track maps + backend-derived caches that pointed at the old + /// tracks. Must be called before building a new document's tracks, otherwise the previous file's + /// tracks/instruments stay resident in the backend and keep getting mixed (orphaned voices). + /// + /// Ordering is safe: the audio thread drains all `command_tx` commands before any `query_tx` + /// queries each callback, so a `reset()` pushed here always runs before the `create_*_track_sync` + /// queries that rebuild the project. + fn reset_audio_backend(&mut self) { + if let Some(ref controller_arc) = self.audio_controller { + controller_arc.lock().unwrap().reset(); + } + self.layer_to_track_map.clear(); + self.track_to_layer_map.clear(); + self.clip_instance_to_backend_map.clear(); + self.midi_event_cache.clear(); + self.audio_duration_cache.clear(); + self.raw_audio_cache.clear(); + self.waveform_gpu_dirty.clear(); + self.waveform_minmax_pools.clear(); + self.waveform_pyramid_blobs.clear(); + } + /// Create a new project with the specified focus/layout fn create_new_project_with_focus(&mut self, layout_index: usize) { use lightningbeam_core::layer::{AnyLayer, AudioLayer, VectorLayer, VideoLayer}; + // Drop the previous project's backend tracks/instruments before building the new one, so a + // "new file" while a project is open doesn't leave orphaned tracks resident in the backend. + self.reset_audio_backend(); + // Create a new blank document let mut document = lightningbeam_core::document::Document::with_size( "Untitled", @@ -3185,23 +3214,17 @@ impl EditorApp { println!("Menu: New File"); // TODO: Prompt to save current file if modified - // Reset state and return to start screen - self.layer_to_track_map.clear(); - self.track_to_layer_map.clear(); - self.layer_to_track_map.clear(); - self.clip_instance_to_backend_map.clear(); + // Tear down the backend (stops old instruments/voices immediately) and clear the + // app-side track maps + backend-derived caches. + self.reset_audio_backend(); + + // Reset UI state and return to start screen self.current_file_path = None; self.selection.clear(); self.editing_context = EditingContext::default(); self.active_layer_id = None; self.playback_time = 0.0; self.is_playing = false; - self.midi_event_cache.clear(); - self.audio_duration_cache.clear(); - self.raw_audio_cache.clear(); - self.waveform_gpu_dirty.clear(); - self.waveform_minmax_pools.clear(); - self.waveform_pyramid_blobs.clear(); self.pane_instances.clear(); self.project_generation += 1; self.app_mode = AppMode::StartScreen; @@ -4146,6 +4169,12 @@ impl EditorApp { // TODO Phase 5: Show recovery dialog } + // Tear down the previously-open project's backend tracks/instruments before restoring this + // file's audio pool + tracks, so an open-over-open doesn't leave orphaned tracks resident in + // the backend. Reset is a command; the audio-pool/track restoration below uses queries, which + // the audio thread drains after all commands each callback, so the ordering holds. + self.reset_audio_backend(); + // Replace document let step1_start = std::time::Instant::now(); self.action_executor = ActionExecutor::new(loaded_project.document); From c373af461e5da2ae7a8c37c1d3f94e04a1a916c9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 06:02:47 -0400 Subject: [PATCH 30/43] Add animated GIF export New export format alongside audio/image/video/SVG. GIF is multi-frame like video but palette-quantized with no audio, so it reuses the per-frame RGBA render/readback path (render_frame_to_gpu_rgba) and streams frames to a background encoder. - core: GifExportSettings (resolution, framerate, loop, transparency, fit, time range) with centisecond-quantized frame delay + tests. - gif_exporter: encoder pipeline. Per-frame NeuQuant quantization is the dominant cost and is per-frame independent, so it's fanned out across a worker pool (cores-1, capped 8); a writer thread reorders and LZW-encodes sequentially. Uses the `gif` crate directly (already resolved via `image`). - orchestrator: start_gif_export + render_next_gif_frame (one frame per egui update), wired into is_exporting/has_pending_progress/cancel. - dialog: GIF tab + settings; main.rs: handle ExportResult::Gif and pump frames. - Cargo: opt-level=3 for gif/color_quant/weezl in the dev profile so debug builds aren't crippled by unoptimized NeuQuant loops. Together these cut a 10s GIF export from ~1:43 to ~3s. Co-Authored-By: Claude Opus 4.8 (1M context) --- lightningbeam-ui/Cargo.lock | 1 + lightningbeam-ui/Cargo.toml | 9 + .../lightningbeam-core/src/export.rs | 103 ++++++++ .../lightningbeam-editor/Cargo.toml | 4 + .../lightningbeam-editor/src/export/dialog.rs | 75 ++++++ .../src/export/gif_exporter.rs | 196 +++++++++++++++ .../lightningbeam-editor/src/export/mod.rs | 233 +++++++++++++++++- .../lightningbeam-editor/src/main.rs | 26 ++ 8 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 lightningbeam-ui/lightningbeam-editor/src/export/gif_exporter.rs diff --git a/lightningbeam-ui/Cargo.lock b/lightningbeam-ui/Cargo.lock index 1f6c4ca..48824a8 100644 --- a/lightningbeam-ui/Cargo.lock +++ b/lightningbeam-ui/Cargo.lock @@ -3641,6 +3641,7 @@ dependencies = [ "egui_extras", "egui_node_graph2", "ffmpeg-next", + "gif", "gpu-video-encoder", "half", "image", diff --git a/lightningbeam-ui/Cargo.toml b/lightningbeam-ui/Cargo.toml index b20938d..1b7ea56 100644 --- a/lightningbeam-ui/Cargo.toml +++ b/lightningbeam-ui/Cargo.toml @@ -83,6 +83,15 @@ opt-level = 2 [profile.dev.package.cpal] opt-level = 2 +# GIF export: NeuQuant palette quantization is tight numeric loops that are punishingly slow +# unoptimized (10–30× in debug). Optimize the encoder crates even in dev builds. +[profile.dev.package.gif] +opt-level = 3 +[profile.dev.package.color_quant] +opt-level = 3 +[profile.dev.package.weezl] +opt-level = 3 + # Use local egui fork with ibus/Wayland text input fix [patch.crates-io] egui = { path = "../../egui-fork/crates/egui" } diff --git a/lightningbeam-ui/lightningbeam-core/src/export.rs b/lightningbeam-ui/lightningbeam-core/src/export.rs index 6e62d50..0f773ec 100644 --- a/lightningbeam-ui/lightningbeam-core/src/export.rs +++ b/lightningbeam-ui/lightningbeam-core/src/export.rs @@ -543,6 +543,82 @@ impl ImageExportSettings { } } +// ── Animated GIF export ────────────────────────────────────────────────────── + +/// Settings for exporting an animated GIF (multi-frame, palette-quantized, no audio). +/// +/// GIF stores a per-frame delay in centiseconds (1/100 s), so effective frame rate is quantized to +/// whole centiseconds — [`Self::frame_delay_ms`] rounds accordingly and the dialog offers sensible +/// GIF rates. Each frame is quantized to a 256-color palette by the encoder. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GifExportSettings { + /// Output width in pixels (None = use document width). + pub width: Option, + /// Output height in pixels (None = use document height). + pub height: Option, + /// Frame rate (fps). Snapped to whole-centisecond delays at encode time. + pub framerate: f64, + /// Loop the animation forever (GIF `NETSCAPE2.0` infinite loop). False = play once. + pub loop_forever: bool, + /// Preserve full alpha as GIF 1-bit transparency (pixels below the alpha threshold become the + /// transparent index). When false, frames are composited onto an opaque background first. + pub transparency: bool, + /// How the document is fit into the output frame when aspect ratios differ (default Letterbox). + #[serde(default)] + pub fit: ExportFitMode, + /// Start time in seconds. + pub start_time: f64, + /// End time in seconds. + pub end_time: f64, +} + +impl Default for GifExportSettings { + fn default() -> Self { + Self { + width: None, + height: None, + framerate: 15.0, + loop_forever: true, + transparency: false, + fit: ExportFitMode::Letterbox, + start_time: 0.0, + end_time: 5.0, + } + } +} + +impl GifExportSettings { + pub fn validate(&self) -> Result<(), String> { + if let Some(w) = self.width { if w == 0 { return Err("Width must be > 0".into()); } } + if let Some(h) = self.height { if h == 0 { return Err("Height must be > 0".into()); } } + if self.framerate <= 0.0 { + return Err("Framerate must be greater than 0".into()); + } + if self.start_time < 0.0 { + return Err("Start time cannot be negative".into()); + } + if self.end_time <= self.start_time { + return Err("End time must be greater than start time".into()); + } + Ok(()) + } + + /// Duration in seconds. + pub fn duration(&self) -> f64 { self.end_time - self.start_time } + + /// Total number of frames to render. + pub fn total_frames(&self) -> usize { + (self.duration() * self.framerate).ceil().max(1.0) as usize + } + + /// Per-frame delay in milliseconds, from the framerate (GIF stores this at centisecond + /// resolution, so the effective rate is snapped to the nearest 10 ms, min 10 ms). + pub fn frame_delay_ms(&self) -> u32 { + let ms = 1000.0 / self.framerate; + ((ms / 10.0).round().max(1.0) * 10.0) as u32 + } +} + /// Progress updates during export #[derive(Debug, Clone)] pub enum ExportProgress { @@ -728,6 +804,33 @@ mod tests { assert_eq!(settings.total_frames(), 300); } + #[test] + fn test_gif_frame_delay_and_frames() { + // Frame rates that map to clean centisecond delays. + let mk = |fps: f64| GifExportSettings { framerate: fps, ..Default::default() }; + assert_eq!(mk(10.0).frame_delay_ms(), 100); // 100 ms + assert_eq!(mk(20.0).frame_delay_ms(), 50); // 50 ms + assert_eq!(mk(50.0).frame_delay_ms(), 20); // 20 ms + // 15 fps = 66.6 ms rounds to 70 ms (7 cs); 25 fps = 40 ms. + assert_eq!(mk(15.0).frame_delay_ms(), 70); + assert_eq!(mk(25.0).frame_delay_ms(), 40); + // Very high fps clamps to the 10 ms minimum (1 cs). + assert_eq!(mk(1000.0).frame_delay_ms(), 10); + + let settings = GifExportSettings { framerate: 20.0, start_time: 0.0, end_time: 3.0, ..Default::default() }; + assert_eq!(settings.total_frames(), 60); + } + + #[test] + fn test_gif_validate() { + let mut s = GifExportSettings::default(); + assert!(s.validate().is_ok()); + s.framerate = 0.0; + assert!(s.validate().is_err()); + s = GifExportSettings { start_time: 5.0, end_time: 2.0, ..Default::default() }; + assert!(s.validate().is_err()); + } + #[test] fn test_export_progress_percentage() { let progress = ExportProgress::FrameRendered { frame: 50, total: 100 }; diff --git a/lightningbeam-ui/lightningbeam-editor/Cargo.toml b/lightningbeam-ui/lightningbeam-editor/Cargo.toml index c80d068..77019b4 100644 --- a/lightningbeam-ui/lightningbeam-editor/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-editor/Cargo.toml @@ -41,6 +41,10 @@ serde_json = { workspace = true } # Image loading image = { workspace = true } +# Animated GIF encoding — used directly (not just via `image`) so per-frame NeuQuant palette +# quantization can be parallelized across a worker pool. Pinned to the version `image` already +# resolves (0.13), so this adds no new transitive graph. +gif = "0.13" resvg = { workspace = true } tiny-skia = "0.11" diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs index 7438836..17fcee5 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs @@ -5,6 +5,7 @@ use eframe::egui; use lightningbeam_core::export::{ AudioExportSettings, AudioFormat, + GifExportSettings, ImageExportSettings, ImageFormat, VideoExportSettings, VideoCodec, VideoQuality, ColorRange, }; @@ -25,6 +26,8 @@ pub enum ExportType { Audio, Image, Video, + /// Animated GIF (multi-frame, palette-quantized, no audio). + Gif, /// Vector-only SVG of the current frame (lossless; raster/video layers skipped). Svg, } @@ -36,6 +39,8 @@ pub enum ExportResult { Image(ImageExportSettings, PathBuf), VideoOnly(VideoExportSettings, PathBuf), VideoWithAudio(VideoExportSettings, AudioExportSettings, PathBuf), + /// Animated GIF export. + Gif(GifExportSettings, PathBuf), /// SVG of vector layers at the given document time. Svg(f64, PathBuf), } @@ -57,6 +62,9 @@ pub struct ExportDialog { /// Video export settings pub video_settings: VideoExportSettings, + /// Animated GIF export settings + pub gif_settings: GifExportSettings, + /// Include audio with video? pub include_audio: bool, @@ -104,6 +112,7 @@ impl Default for ExportDialog { audio_settings: AudioExportSettings::standard_mp3(), image_settings: ImageExportSettings::default(), video_settings: VideoExportSettings::default(), + gif_settings: GifExportSettings::default(), include_audio: true, output_path: None, error_message: None, @@ -124,6 +133,7 @@ impl ExportDialog { self.open = true; self.audio_settings.end_time = timeline_duration; self.video_settings.end_time = timeline_duration; + self.gif_settings.end_time = timeline_duration; self.image_settings.time = hint.current_time; // Propagate document dimensions as defaults (None means "use doc size"). self.image_settings.width = None; @@ -160,6 +170,7 @@ impl ExportDialog { ExportType::Audio => self.audio_settings.format.extension(), ExportType::Image => self.image_settings.format.extension(), ExportType::Video => self.video_settings.codec.container_format(), + ExportType::Gif => "gif", ExportType::Svg => "svg", } } @@ -203,6 +214,7 @@ impl ExportDialog { ExportType::Audio => "Export Audio", ExportType::Image => "Export Image", ExportType::Video => "Export Video", + ExportType::Gif => "Export GIF", ExportType::Svg => "Export SVG", }; @@ -225,6 +237,7 @@ impl ExportDialog { (ExportType::Audio, "Audio"), (ExportType::Image, "Image"), (ExportType::Video, "Video"), + (ExportType::Gif, "GIF"), (ExportType::Svg, "SVG"), ] { if ui.selectable_value(&mut self.export_type, variant, label).clicked() { @@ -242,6 +255,7 @@ impl ExportDialog { ExportType::Audio => self.render_audio_basic(ui), ExportType::Image => self.render_image_settings(ui), ExportType::Video => self.render_video_basic(ui), + ExportType::Gif => self.render_gif_basic(ui), ExportType::Svg => self.render_svg_settings(ui), } @@ -261,6 +275,7 @@ impl ExportDialog { ExportType::Audio => self.render_audio_advanced(ui), ExportType::Image => self.render_image_advanced(ui), ExportType::Video => self.render_video_advanced(ui), + ExportType::Gif => self.render_gif_advanced(ui), ExportType::Svg => {} // SVG has no advanced settings } } @@ -614,12 +629,65 @@ impl ExportDialog { self.render_time_range(ui); } + /// GIF frame-rate presets (fps). GIF delays are centisecond-quantized, so these map to clean + /// per-frame delays (10/15/20/25/50 fps → 100/70/50/40/20 ms after rounding). + const GIF_FPS: &'static [f64] = &[10.0, 15.0, 20.0, 25.0, 50.0]; + + /// Render basic GIF settings (frame rate + loop). + fn render_gif_basic(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.label("Frame rate:"); + egui::ComboBox::from_id_salt("gif_fps") + .selected_text(format!("{} fps", self.gif_settings.framerate as u32)) + .show_ui(ui, |ui| { + for &fps in Self::GIF_FPS { + ui.selectable_value(&mut self.gif_settings.framerate, fps, format!("{} fps", fps as u32)); + } + }); + }); + + ui.checkbox(&mut self.gif_settings.loop_forever, "Loop forever"); + + ui.add_space(8.0); + self.render_time_range(ui); + } + + /// Render advanced GIF settings (resolution, fit, transparency). + fn render_gif_advanced(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.label("Size:"); + let mut w = self.gif_settings.width.unwrap_or(0); + let mut h = self.gif_settings.height.unwrap_or(0); + let changed_w = ui.add(egui::DragValue::new(&mut w).range(0..=u32::MAX).prefix("W ")).changed(); + let changed_h = ui.add(egui::DragValue::new(&mut h).range(0..=u32::MAX).prefix("H ")).changed(); + if changed_w { self.gif_settings.width = if w == 0 { None } else { Some(w) }; } + if changed_h { self.gif_settings.height = if h == 0 { None } else { Some(h) }; } + ui.weak("(0 = document size)"); + }); + + ui.horizontal(|ui| { + use lightningbeam_core::export::ExportFitMode; + ui.label("Fit:"); + egui::ComboBox::from_id_salt("gif_fit_mode") + .selected_text(self.gif_settings.fit.name()) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.gif_settings.fit, ExportFitMode::Letterbox, ExportFitMode::Letterbox.name()); + ui.selectable_value(&mut self.gif_settings.fit, ExportFitMode::Crop, ExportFitMode::Crop.name()); + ui.selectable_value(&mut self.gif_settings.fit, ExportFitMode::Stretch, ExportFitMode::Stretch.name()); + }); + }); + + ui.checkbox(&mut self.gif_settings.transparency, "Preserve transparency (1-bit)"); + ui.label(egui::RichText::new("GIF supports only on/off transparency; semi-transparent pixels are keyed out.").weak().small()); + } + /// Render time range UI (common to both audio and video) fn render_time_range(&mut self, ui: &mut egui::Ui) { let (start_time, end_time) = match self.export_type { ExportType::Audio => (&mut self.audio_settings.start_time, &mut self.audio_settings.end_time), ExportType::Image | ExportType::Svg => return, // single time field, not a range ExportType::Video => (&mut self.video_settings.start_time, &mut self.video_settings.end_time), + ExportType::Gif => (&mut self.gif_settings.start_time, &mut self.gif_settings.end_time), }; ui.horizontal(|ui| { @@ -693,6 +761,13 @@ impl ExportDialog { Some(ExportResult::Image(self.image_settings.clone(), output_path)) } ExportType::Svg => Some(ExportResult::Svg(self.image_settings.time, output_path)), + ExportType::Gif => { + if let Err(err) = self.gif_settings.validate() { + self.error_message = Some(err); + return None; + } + Some(ExportResult::Gif(self.gif_settings.clone(), output_path)) + } ExportType::Audio => { // Validate audio settings if let Err(err) = self.audio_settings.validate() { diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/gif_exporter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/gif_exporter.rs new file mode 100644 index 0000000..51c95b7 --- /dev/null +++ b/lightningbeam-ui/lightningbeam-editor/src/export/gif_exporter.rs @@ -0,0 +1,196 @@ +//! Animated GIF encoding. +//! +//! Palette-quantizes a stream of RGBA8 frames and writes them to a `.gif`. The expensive part — +//! per-frame NeuQuant 256-color quantization — is embarrassingly parallel (each frame gets its own +//! local palette), so it's fanned out across a worker pool. A single writer thread collects the +//! quantized frames, reorders them, and LZW-encodes them to the file in sequence. +//! +//! Pipeline (all off the UI thread): +//! ```text +//! UI render thread ──RGBA──▶ coordinator ──round-robin──▶ N quantizer workers +//! │ (idx, gif::Frame) +//! ▼ +//! writer thread ──▶ .gif +//! ``` +//! Rendering + readback happen on the UI thread (see `render_next_gif_frame`); this module owns +//! everything after a raw RGBA frame arrives. + +use lightningbeam_core::export::ExportProgress; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::Arc; + +/// Message from the UI (render) thread to the GIF encoder coordinator. +pub enum GifFrameMessage { + /// One RGBA8 frame (top-left origin, tightly packed `width*height*4` bytes). + Frame { frame_num: usize, pixels: Vec }, + /// All frames have been sent. + Done, +} + +/// gif crate quantization speed (1 = slowest/best, 30 = fastest/worst). 10 balances palette quality +/// against per-frame cost; the parallelism below is what actually recovers the wall-clock. +const QUANT_SPEED: i32 = 10; + +/// Run the GIF encoder pipeline. Receives RGBA8 frames from `frame_rx`, quantizes them in parallel, +/// and writes the ordered result to `output_path`, reporting progress. `transparency == false` +/// composites each frame onto opaque black first (GIF's 1-bit transparency would otherwise key out +/// semi-transparent pixels). +#[allow(clippy::too_many_arguments)] +pub fn run_gif_encoder( + frame_rx: Receiver, + output_path: PathBuf, + width: u32, + height: u32, + total_frames: usize, + delay_ms: u32, + loop_forever: bool, + transparency: bool, + progress_tx: Sender, + cancel_flag: Arc, +) { + let _ = progress_tx.send(ExportProgress::Started { total_frames }); + + let delay_cs = ((delay_ms / 10).max(1)) as u16; + let expected_len = (width as usize) * (height as usize) * 4; + + // One quantizer worker per spare core (leave one for the UI render thread), capped so we don't + // spawn absurdly many for short exports. + let n_workers = std::thread::available_parallelism() + .map(|n| n.get().saturating_sub(1)) + .unwrap_or(1) + .clamp(1, 8); + + // Per-worker input channels (coordinator dispatches round-robin) + one shared result channel. + let mut worker_txs: Vec)>> = Vec::with_capacity(n_workers); + let (result_tx, result_rx) = channel::<(usize, gif::Frame<'static>)>(); + let mut worker_handles = Vec::with_capacity(n_workers); + + for _ in 0..n_workers { + let (wtx, wrx) = channel::<(usize, Vec)>(); + worker_txs.push(wtx); + let result_tx = result_tx.clone(); + let cancel = Arc::clone(&cancel_flag); + worker_handles.push(std::thread::spawn(move || { + while let Ok((idx, mut pixels)) = wrx.recv() { + if cancel.load(Ordering::Relaxed) { + break; + } + // NeuQuant local-palette quantization (the expensive step). `from_rgba_speed` uses + // the RGBA buffer as scratch, so it's fine that we own `pixels` here. + let mut frame = + gif::Frame::from_rgba_speed(width as u16, height as u16, &mut pixels, QUANT_SPEED); + frame.delay = delay_cs; + if result_tx.send((idx, frame)).is_err() { + break; // writer gone + } + } + })); + } + drop(result_tx); // only the workers hold senders now; writer's rx ends when they all finish + + // Writer thread: order frames by index and LZW-encode them sequentially. + let writer_progress = progress_tx.clone(); + let writer_cancel = Arc::clone(&cancel_flag); + let writer_output = output_path.clone(); + let writer = std::thread::spawn(move || -> Result<(), String> { + let file = std::fs::File::create(&writer_output) + .map_err(|e| format!("Failed to create GIF file: {e}"))?; + let mut buf = std::io::BufWriter::new(file); + let mut encoder = gif::Encoder::new(&mut buf, width as u16, height as u16, &[]) + .map_err(|e| format!("GIF encoder init failed: {e}"))?; + if loop_forever { + encoder + .set_repeat(gif::Repeat::Infinite) + .map_err(|e| format!("GIF set_repeat failed: {e}"))?; + } + + // Frames may arrive out of order; hold stragglers until their turn. + let mut pending: HashMap> = HashMap::new(); + let mut next = 0usize; + let mut written = 0usize; + + while let Ok((idx, frame)) = result_rx.recv() { + if writer_cancel.load(Ordering::Relaxed) { + break; + } + pending.insert(idx, frame); + while let Some(f) = pending.remove(&next) { + encoder + .write_frame(&f) + .map_err(|e| format!("GIF write_frame failed: {e}"))?; + next += 1; + written += 1; + let _ = writer_progress.send(ExportProgress::FrameRendered { + frame: written, + total: total_frames, + }); + } + } + // Encoder/BufWriter flush on drop. + Ok(()) + }); + + // Coordinator: pull RGBA frames from the UI thread and dispatch round-robin to the workers. + let mut dispatched = 0usize; + let mut fatal: Option = None; + loop { + match frame_rx.recv() { + Ok(GifFrameMessage::Frame { frame_num, mut pixels }) => { + if cancel_flag.load(Ordering::Relaxed) { + break; + } + if pixels.len() != expected_len { + fatal = Some("GIF frame size mismatch".into()); + break; + } + if !transparency { + // Premultiply onto opaque black, then force alpha opaque. + for px in pixels.chunks_exact_mut(4) { + let a = px[3] as u32; + px[0] = (px[0] as u32 * a / 255) as u8; + px[1] = (px[1] as u32 * a / 255) as u8; + px[2] = (px[2] as u32 * a / 255) as u8; + px[3] = 255; + } + } + let w = dispatched % n_workers; + if worker_txs[w].send((frame_num, pixels)).is_err() { + fatal = Some("GIF quantizer worker died".into()); + break; + } + dispatched += 1; + } + Ok(GifFrameMessage::Done) => break, + Err(_) => break, // UI thread dropped the sender + } + } + + let _ = progress_tx.send(ExportProgress::Finalizing); + + // Close worker inputs → workers finish → their result senders drop → writer's loop ends. + drop(worker_txs); + for h in worker_handles { + let _ = h.join(); + } + let writer_result = writer.join().unwrap_or_else(|_| Err("GIF writer thread panicked".into())); + + if cancel_flag.load(Ordering::Relaxed) { + std::fs::remove_file(&output_path).ok(); + // Emit Complete so the UI poll loop clears its state; the dialog was closed on cancel. + let _ = progress_tx.send(ExportProgress::Complete { output_path }); + return; + } + + match fatal.or_else(|| writer_result.err()) { + Some(message) => { + std::fs::remove_file(&output_path).ok(); + let _ = progress_tx.send(ExportProgress::Error { message }); + } + None => { + let _ = progress_tx.send(ExportProgress::Complete { output_path }); + } + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index d5b2851..a493f79 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -5,6 +5,7 @@ pub mod audio_exporter; pub mod dialog; +pub mod gif_exporter; pub mod image_exporter; pub mod video_exporter; pub mod readback_pipeline; @@ -13,7 +14,8 @@ pub mod cpu_yuv_converter; pub mod gpu_yuv; pub mod hdr_frame; -use lightningbeam_core::export::{AudioExportSettings, ImageExportSettings, VideoExportSettings, ExportProgress}; +use lightningbeam_core::export::{AudioExportSettings, GifExportSettings, ImageExportSettings, VideoExportSettings, ExportProgress}; +use gif_exporter::GifFrameMessage; use lightningbeam_core::document::Document; use lightningbeam_core::renderer::ImageCache; use lightningbeam_core::video::VideoManager; @@ -131,6 +133,38 @@ pub struct ExportOrchestrator { /// Single-frame image export state image_state: Option, + + /// Animated GIF export state (frames rendered on the UI thread, encoded on `thread_handle`). + gif_state: Option, +} + +/// State for an in-progress animated GIF export. Frames are rendered + read back on the UI thread +/// (one per `render_next_gif_frame` call) and streamed to the encoder thread over `frame_tx`. +struct GifExportState { + /// Resolved pixel dimensions (after applying any width/height overrides). + width: u32, + height: u32, + /// Total frames to render. + total_frames: usize, + /// Next frame index to render (0-based). + next_frame: usize, + /// Document time (seconds) of frame 0. + start_time: f64, + /// Seconds between frames (1 / framerate). + frame_step: f64, + /// How the document is fit into the export frame. + fit: lightningbeam_core::export::ExportFitMode, + /// Preserve alpha as GIF transparency (else the encoder flattens onto black). + transparency: bool, + /// GPU resources allocated on the first render call, reused each frame. + gpu_resources: Option, + /// Output RGBA texture (kept separate from gpu_resources to avoid split-borrow issues). + output_texture: Option, + output_texture_view: Option, + /// Staging buffer for synchronous GPU→CPU readback (reused each frame). + staging_buffer: Option, + /// Sender to the encoder thread; dropped after the final frame to signal completion. + frame_tx: Option>, } /// State for parallel audio+video export @@ -168,6 +202,7 @@ impl ExportOrchestrator { video_state: None, parallel_export: None, image_state: None, + gif_state: None, } } @@ -250,7 +285,7 @@ impl ExportOrchestrator { /// unconsumed terminal message). Used to gate the UI poll loop so it doesn't run every /// repaint forever after an export finishes. pub fn has_pending_progress(&self) -> bool { - self.parallel_export.is_some() || self.image_state.is_some() || self.progress_rx.is_some() + self.parallel_export.is_some() || self.image_state.is_some() || self.gif_state.is_some() || self.progress_rx.is_some() } /// Poll progress for parallel video+audio export @@ -534,6 +569,9 @@ impl ExportOrchestrator { } self.video_state = None; self.image_state = None; + // Dropping gif_state drops its frame sender, unblocking the encoder thread's recv() so it + // observes the cancel flag, removes the partial file, and exits. + self.gif_state = None; self.progress_rx = None; self.thread_handle = None; } @@ -542,6 +580,7 @@ impl ExportOrchestrator { pub fn is_exporting(&self) -> bool { if self.parallel_export.is_some() { return true; } if self.image_state.is_some() { return true; } + if self.gif_state.is_some() { return true; } if let Some(handle) = &self.thread_handle { !handle.is_finished() } else { @@ -719,6 +758,196 @@ impl ExportOrchestrator { result.map(|_| true) } + /// Enqueue an animated GIF export. Spawns the encoder thread now; call `render_next_gif_frame()` + /// from the egui update loop (where the wgpu device/queue are available) to render + stream each + /// frame to it. + pub fn start_gif_export( + &mut self, + settings: GifExportSettings, + output_path: PathBuf, + doc_width: u32, + doc_height: u32, + ) { + self.cancel_flag.store(false, Ordering::Relaxed); + + let width = settings.width.unwrap_or(doc_width).max(1); + let height = settings.height.unwrap_or(doc_height).max(1); + let total_frames = settings.total_frames(); + let delay_ms = settings.frame_delay_ms(); + let frame_step = 1.0 / settings.framerate; + + let (progress_tx, progress_rx) = channel(); + let (frame_tx, frame_rx) = channel(); + self.progress_rx = Some(progress_rx); + + let cancel_flag = Arc::clone(&self.cancel_flag); + let loop_forever = settings.loop_forever; + let transparency = settings.transparency; + let handle = std::thread::spawn(move || { + gif_exporter::run_gif_encoder( + frame_rx, output_path, width, height, total_frames, delay_ms, + loop_forever, transparency, progress_tx, cancel_flag, + ); + }); + self.thread_handle = Some(handle); + + self.gif_state = Some(GifExportState { + width, + height, + total_frames, + next_frame: 0, + start_time: settings.start_time, + frame_step, + fit: settings.fit, + transparency, + gpu_resources: None, + output_texture: None, + output_texture_view: None, + staging_buffer: None, + frame_tx: Some(frame_tx), + }); + } + + /// Drive the animated GIF export: render + read back one frame per call and stream it to the + /// encoder thread. Returns `Ok(true)` while more frames remain (call again next egui frame), + /// `Ok(false)` once every frame has been sent (encoding then finishes on the background thread). + pub fn render_next_gif_frame( + &mut self, + document: &mut Document, + device: &wgpu::Device, + queue: &wgpu::Queue, + renderer: &mut vello::Renderer, + image_cache: &mut ImageCache, + video_manager: &Arc>, + raster_store: Option<&lightningbeam_core::raster_store::RasterStore>, + ) -> Result { + if self.cancel_flag.load(Ordering::Relaxed) { + // Dropping frame_tx unblocks the encoder thread's recv() so it can clean up. + self.gif_state = None; + return Ok(false); + } + + let state = match self.gif_state.as_mut() { + Some(s) => s, + None => return Ok(false), + }; + + // All frames sent → drop the sender (signals the encoder to finalize) and finish. + if state.next_frame >= state.total_frames { + if let Some(tx) = state.frame_tx.take() { + let _ = tx.send(GifFrameMessage::Done); + drop(tx); + } + self.gif_state = None; + return Ok(false); + } + + let w = state.width; + let h = state.height; + let fit = state.fit; + let timestamp = state.start_time + state.next_frame as f64 * state.frame_step; + + if state.gpu_resources.is_none() { + state.gpu_resources = Some(video_exporter::ExportGpuResources::new(device, w, h)); + } + if state.output_texture.is_none() { + let tex = device.create_texture(&wgpu::TextureDescriptor { + label: Some("gif_export_output"), + size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + state.output_texture_view = Some(tex.create_view(&wgpu::TextureViewDescriptor::default())); + state.output_texture = Some(tex); + } + + // Render the frame (transparency preserved through readback; the encoder flattens if needed). + { + let gpu = state.gpu_resources.as_mut().unwrap(); + let output_view = state.output_texture_view.as_ref().unwrap(); + let encoder = video_exporter::render_frame_to_gpu_rgba( + document, + timestamp, + w, h, + device, queue, renderer, image_cache, video_manager, + gpu, + output_view, + None, // no floating raster selection during export + state.transparency, + raster_store, + true, // GIF export composites on the shared device + fit, + )?; + queue.submit(Some(encoder.finish())); + } + + // Synchronous readback (wgpu requires bytes_per_row aligned to 256). + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let bytes_per_row = (w * 4 + align - 1) / align * align; + if state.staging_buffer.is_none() { + state.staging_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: Some("gif_export_staging"), + size: (bytes_per_row * h) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + })); + } + let staging = state.staging_buffer.as_ref().unwrap(); + + let mut copy_enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gif_export_copy"), + }); + let output_tex = state.output_texture.as_ref().unwrap(); + copy_enc.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: output_tex, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: staging, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: Some(h), + }, + }, + wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, + ); + queue.submit(Some(copy_enc.finish())); + + let slice = staging.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + let _ = device.poll(wgpu::PollType::wait_indefinitely()); + + let pixels: Vec = { + let mapped = slice.get_mapped_range(); + let mut out = Vec::with_capacity((w * h * 4) as usize); + for row in 0..h { + let start = (row * bytes_per_row) as usize; + out.extend_from_slice(&mapped[start..start + (w * 4) as usize]); + } + out + }; + staging.unmap(); + + let frame_num = state.next_frame; + if let Some(tx) = state.frame_tx.as_ref() { + // If the encoder thread died, stop — its dropped receiver returns an error here. + if tx.send(GifFrameMessage::Frame { frame_num, pixels }).is_err() { + self.gif_state = None; + return Ok(false); + } + } + state.next_frame += 1; + Ok(true) + } + /// Wait for the export to complete /// /// This blocks until the export thread finishes. diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 40f9e37..b01a4de 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6355,6 +6355,17 @@ impl eframe::App for EditorApp { } false // synchronous; no progress dialog } + ExportResult::Gif(settings, output_path) => { + println!("🎞 [MAIN] Starting GIF export: {}", output_path.display()); + let doc = self.action_executor.document(); + orchestrator.start_gif_export( + settings, + output_path, + doc.width as u32, + doc.height as u32, + ); + true // background encode with progress dialog + } ExportResult::AudioOnly(settings, output_path) => { println!("🎵 [MAIN] Starting audio-only export: {}", output_path.display()); @@ -6509,6 +6520,21 @@ impl eframe::App for EditorApp { } } + // Drive incremental GIF export (one frame rendered + streamed per call). + match orchestrator.render_next_gif_frame( + self.action_executor.document_mut(), + device, + queue, + renderer, + image_cache, + &self.video_manager, + Some(&self.raster_store), + ) { + Ok(true) => { ctx.request_repaint(); } // more frames to render + Ok(false) => {} // done or not a GIF export + Err(e) => { eprintln!("GIF export failed: {e}"); } + } + // Drive single-frame image export (two-frame async: render then readback). match orchestrator.render_image_frame( self.action_executor.document_mut(), From 53ffb7d5283b17b2599dddf57f042bf186ed3775 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 06:55:23 -0400 Subject: [PATCH 31/43] Export honesty: real lossy WebP, working ProRes, VP8+audio container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cases where an export produced something that didn't match what the UI offered: - WebP quality slider was a no-op: image 0.25's WebP encoder is lossless-only, so the slider did nothing and files were needlessly large. Encode lossy WebP via ffmpeg's libwebp instead (already linked); the quality knob is now real and alpha is preserved as YUVA420P. Test asserts a lossy VP8 chunk + that quality changes file size. - ProRes 422 always failed to open: the SDR path fed prores_ks 8-bit YUV420P, but it requires 10-bit 4:2:2. Add a CpuYuv422P10Converter (RGBA→YUV422P10LE, BT.709) and route ProRes through the existing async pipeline in CPU mode; setup_video_encoder now emits YUV422P10LE + prores_ks HQ profile and encode_frame handles 4:2:2 chroma. Test guards that the encoder opens. - VP8+audio failed at mux: the parallel path wrote the temp video to a hardcoded .mp4, which VP8 can't live in. Derive the temp container from the codec (VP8/VP9 → .webm). --- .../src/export/cpu_yuv_converter.rs | 102 ++++++++++ .../src/export/image_exporter.rs | 176 +++++++++++++++++- .../lightningbeam-editor/src/export/mod.rs | 63 +++++-- .../src/export/video_exporter.rs | 33 +++- 4 files changed, 356 insertions(+), 18 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/cpu_yuv_converter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/cpu_yuv_converter.rs index bfd4271..54630b4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/cpu_yuv_converter.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/cpu_yuv_converter.rs @@ -101,6 +101,94 @@ impl CpuYuvConverter { } } +/// CPU RGBA→YUV422P10LE converter (10-bit, 4:2:2) via swscale, for ProRes 422 export. +/// +/// ProRes (`prores_ks`) requires a 10-bit 4:2:2 input; the SDR pipeline otherwise produces 8-bit +/// 4:2:0. Source is still 8-bit RGBA (bit-depth is promoted, not conjured), which is normal for +/// SDR ProRes. BT.709 with the requested range, matching the encoder's color tags. +pub struct CpuYuv422P10Converter { + width: u32, + height: u32, + scaler: ffmpeg::software::scaling::Context, + rgba_frame: ffmpeg::frame::Video, + yuv_frame: ffmpeg::frame::Video, +} + +impl CpuYuv422P10Converter { + pub fn new(width: u32, height: u32, full_range: bool) -> Result { + let mut scaler = ffmpeg::software::scaling::Context::get( + ffmpeg::format::Pixel::RGBA, width, height, + ffmpeg::format::Pixel::YUV422P10LE, width, height, + ffmpeg::software::scaling::Flags::BILINEAR, + ) + .map_err(|e| format!("Failed to create YUV422P10 swscale context: {}", e))?; + + // BT.709, requested output range (matches setup_video_encoder's SDR tags). No safe + // ffmpeg-next wrapper for sws_setColorspaceDetails, so this is the raw call (as in + // CpuYuvConverter::new above). + unsafe { + let coeffs = ffmpeg::ffi::sws_getCoefficients(ffmpeg::ffi::SWS_CS_ITU709 as i32); + let dst_range = if full_range { 1 } else { 0 }; + let one = 1 << 16; + ffmpeg::ffi::sws_setColorspaceDetails( + scaler.as_mut_ptr(), + coeffs, 1, + coeffs, dst_range, + 0, one, one, + ); + } + + let rgba_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::RGBA, width, height); + let yuv_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV422P10LE, width, height); + Ok(Self { width, height, scaler, rgba_frame, yuv_frame }) + } + + /// Convert packed RGBA (width*height*4) to tight YUV422P10LE planes (little-endian, 2 bytes per + /// sample): Y is width×height, U and V are (width/2)×height. Planes are returned tight (stride + /// padding stripped) to match what `encode_frame` expects. + pub fn convert(&mut self, rgba_data: &[u8]) -> Result<(Vec, Vec, Vec), String> { + let expected = (self.width * self.height * 4) as usize; + assert_eq!(rgba_data.len(), expected, + "RGBA data size mismatch: expected {} bytes, got {}", expected, rgba_data.len()); + + // Copy RGBA into the source frame honoring its stride (may be padded). + let row_bytes = (self.width * 4) as usize; + let src_stride = self.rgba_frame.stride(0); + { + let dst = self.rgba_frame.data_mut(0); + for row in 0..self.height as usize { + let s = row * row_bytes; + let d = row * src_stride; + dst[d..d + row_bytes].copy_from_slice(&rgba_data[s..s + row_bytes]); + } + } + + self.scaler + .run(&self.rgba_frame, &mut self.yuv_frame) + .map_err(|e| format!("YUV422P10 swscale conversion failed: {}", e))?; + + // Extract each plane tight (2 bytes/sample). Y: width samples/row × height rows. + // Chroma (4:2:2): width/2 samples/row × height rows. + let extract = |frame: &ffmpeg::frame::Video, idx: usize, samples_w: usize, rows: usize| { + let bytes_per_row = samples_w * 2; + let stride = frame.stride(idx); + let data = frame.data(idx); + let mut out = Vec::with_capacity(bytes_per_row * rows); + for row in 0..rows { + let start = row * stride; + out.extend_from_slice(&data[start..start + bytes_per_row]); + } + out + }; + let (w, h) = (self.width as usize, self.height as usize); + let y_plane = extract(&self.yuv_frame, 0, w, h); + let u_plane = extract(&self.yuv_frame, 1, w / 2, h); + let v_plane = extract(&self.yuv_frame, 2, w / 2, h); + + Ok((y_plane, u_plane, v_plane)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -131,6 +219,20 @@ mod tests { assert_eq!(v.len(), (1920 / 2) * (1080 / 2)); } + #[test] + fn test_yuv422p10_output_sizes() { + // Use a width that forces swscale linesize padding (not a multiple of 32/64) to exercise + // the stride-stripping extraction. + let (w, h) = (1000u32, 720u32); + let mut c = CpuYuv422P10Converter::new(w, h, false).unwrap(); + let rgba = vec![0u8; (w * h * 4) as usize]; + let (y, u, v) = c.convert(&rgba).unwrap(); + // 10-bit → 2 bytes/sample. Y full res; U/V half width, full height (4:2:2). + assert_eq!(y.len(), (w * h * 2) as usize); + assert_eq!(u.len(), ((w / 2) * h * 2) as usize); + assert_eq!(v.len(), ((w / 2) * h * 2) as usize); + } + #[test] #[should_panic(expected = "RGBA data size mismatch")] fn test_wrong_input_size_panics() { diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/image_exporter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/image_exporter.rs index 9352bfe..0695eb1 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/image_exporter.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/image_exporter.rs @@ -45,13 +45,179 @@ pub fn save_rgba_image( encoder.encode_image(&rgb_img).map_err(|e| format!("JPEG encode failed: {e}")) } ImageFormat::WebP => { - if allow_transparency { - img.save(path).map_err(|e| format!("WebP save failed: {e}")) - } else { - let flat = flatten_alpha(img); - flat.save(path).map_err(|e| format!("WebP save failed: {e}")) + // `image` 0.25's WebP encoder is lossless-only, which ignored the quality slider and + // produced needlessly large files. Encode lossy WebP via ffmpeg's libwebp instead so + // the quality control is real; alpha is preserved (as YUVA420P) when requested. + save_webp_ffmpeg(pixels, width, height, quality, allow_transparency, path) + } + } +} + +/// Encode a single frame as lossy WebP via ffmpeg's `libwebp` encoder. +/// +/// `quality` is libwebp's 0–100 quality factor. When `allow_transparency` is true the source is +/// converted to YUVA420P so libwebp keeps the alpha channel; otherwise it's flattened onto black +/// and converted to YUV420P. Uses swscale's default BT.601 conversion (matching a plain +/// `ffmpeg -i in.png out.webp`). +fn save_webp_ffmpeg( + pixels: &[u8], + width: u32, + height: u32, + quality: u8, + allow_transparency: bool, + path: &Path, +) -> Result<(), String> { + use ffmpeg_next as ffmpeg; + + ffmpeg::init().map_err(|e| format!("Failed to initialize ffmpeg: {e}"))?; + + let codec = ffmpeg::encoder::find_by_name("libwebp") + .or_else(|| ffmpeg::encoder::find(ffmpeg::codec::Id::WEBP)) + .ok_or("libwebp encoder not available in this ffmpeg build")?; + + // Flatten onto black up front when alpha isn't wanted, so the source is fully opaque. + let src_rgba: Vec = if allow_transparency { + pixels.to_vec() + } else { + let mut v = pixels.to_vec(); + for px in v.chunks_exact_mut(4) { + let a = px[3] as u32; + px[0] = (px[0] as u32 * a / 255) as u8; + px[1] = (px[1] as u32 * a / 255) as u8; + px[2] = (px[2] as u32 * a / 255) as u8; + px[3] = 255; + } + v + }; + + let dst_pix = if allow_transparency { + ffmpeg::format::Pixel::YUVA420P + } else { + ffmpeg::format::Pixel::YUV420P + }; + + // RGBA → YUV(A)420P (swscale defaults: BT.601, limited range — what libwebp expects). + let mut scaler = ffmpeg::software::scaling::Context::get( + ffmpeg::format::Pixel::RGBA, width, height, + dst_pix, width, height, + ffmpeg::software::scaling::Flags::BILINEAR, + ) + .map_err(|e| format!("Failed to create swscale context: {e}"))?; + + let mut src = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::RGBA, width, height); + // Copy row-by-row honoring the frame's stride (may exceed width*4 due to alignment padding). + let stride = src.stride(0); + let row_bytes = (width * 4) as usize; + { + let dst = src.data_mut(0); + for y in 0..height as usize { + let s = y * row_bytes; + let d = y * stride; + dst[d..d + row_bytes].copy_from_slice(&src_rgba[s..s + row_bytes]); + } + } + + let mut yuv = ffmpeg::frame::Video::new(dst_pix, width, height); + scaler.run(&src, &mut yuv).map_err(|e| format!("swscale conversion failed: {e}"))?; + yuv.set_pts(Some(0)); + + let mut octx = ffmpeg::format::output(&path) + .map_err(|e| format!("Failed to create WebP output: {e}"))?; + + let mut enc = ffmpeg::codec::Context::new_with_codec(codec) + .encoder() + .video() + .map_err(|e| format!("Failed to create WebP encoder: {e}"))?; + enc.set_width(width); + enc.set_height(height); + enc.set_format(dst_pix); + enc.set_time_base(ffmpeg::Rational(1, 1)); + + // libwebp private options: quality 0–100, lossy. + let mut opts = ffmpeg::Dictionary::new(); + opts.set("quality", &quality.to_string()); + opts.set("lossless", "0"); + let mut enc = enc + .open_with(opts) + .map_err(|e| format!("Failed to open libwebp encoder: {e}"))?; + + { + let mut stream = octx.add_stream(codec) + .map_err(|e| format!("Failed to add WebP stream: {e}"))?; + stream.set_parameters(&enc); + stream.set_time_base(ffmpeg::Rational(1, 1)); + } + + octx.write_header().map_err(|e| format!("Failed to write WebP header: {e}"))?; + enc.send_frame(&yuv).map_err(|e| format!("Failed to send WebP frame: {e}"))?; + enc.send_eof().map_err(|e| format!("Failed to flush WebP encoder: {e}"))?; + + let mut packet = ffmpeg::Packet::empty(); + while enc.receive_packet(&mut packet).is_ok() { + packet.set_stream(0); + packet + .write_interleaved(&mut octx) + .map_err(|e| format!("Failed to write WebP packet: {e}"))?; + } + + octx.write_trailer().map_err(|e| format!("Failed to finalize WebP: {e}"))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use lightningbeam_core::export::ImageFormat; + + /// A gradient RGBA image so the encoder has real content to quantize/compress. + fn gradient(width: u32, height: u32) -> Vec { + let mut px = Vec::with_capacity((width * height * 4) as usize); + for y in 0..height { + for x in 0..width { + px.push((x * 255 / width.max(1)) as u8); + px.push((y * 255 / height.max(1)) as u8); + px.push(128); + px.push(255); } } + px + } + + /// The ffmpeg libwebp path must produce a valid *lossy* WebP (RIFF/WEBP container with a + /// `VP8 ` chunk — lossless would be `VP8L`), and the quality knob must actually change size. + #[test] + fn webp_export_is_real_lossy() { + let (w, h) = (96u32, 64u32); + let px = gradient(w, h); + let dir = std::env::temp_dir(); + let lo = dir.join("lb_webp_q10_test.webp"); + let hi = dir.join("lb_webp_q95_test.webp"); + + save_webp_ffmpeg(&px, w, h, 10, false, &lo).expect("low-quality webp encode"); + save_webp_ffmpeg(&px, w, h, 95, false, &hi).expect("high-quality webp encode"); + + let lo_bytes = std::fs::read(&lo).unwrap(); + let hi_bytes = std::fs::read(&hi).unwrap(); + + // RIFF....WEBP container. + assert_eq!(&lo_bytes[0..4], b"RIFF", "not a RIFF container"); + assert_eq!(&lo_bytes[8..12], b"WEBP", "not a WEBP file"); + // Lossy VP8 chunk (`VP8 ` with trailing space), NOT lossless `VP8L`. + assert_eq!(&lo_bytes[12..16], b"VP8 ", "expected lossy VP8, got {:?}", &lo_bytes[12..16]); + // The quality knob is honored: q10 is meaningfully smaller than q95. + assert!(lo_bytes.len() < hi_bytes.len(), + "quality ignored: q10 {} bytes >= q95 {} bytes", lo_bytes.len(), hi_bytes.len()); + + std::fs::remove_file(&lo).ok(); + std::fs::remove_file(&hi).ok(); + } + + /// The format enum still advertises a quality control for WebP (now that it works). + #[test] + fn webp_has_quality() { + assert!(ImageFormat::WebP.has_quality()); + assert!(ImageFormat::Jpeg.has_quality()); + assert!(!ImageFormat::Png.has_quality()); } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index a493f79..803abfd 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -70,6 +70,11 @@ pub struct VideoExportState { readback_pipeline: Option, /// CPU YUV converter for RGBA→YUV420p conversion cpu_yuv_converter: Option, + /// ProRes 422 export: forces the CPU (RGBA) readback path and converts to 10-bit 4:2:2 instead + /// of 8-bit 4:2:0. `true` only for `VideoCodec::ProRes422` (SDR). + prores: bool, + /// CPU RGBA→YUV422P10LE converter, used only on the ProRes path. + cpu_yuv422p10: Option, /// Frames that have been submitted to GPU but not yet encoded frames_in_flight: usize, /// Next frame number to send to encoder (for ordering) @@ -1101,6 +1106,8 @@ impl ExportOrchestrator { let hdr = settings.hdr; let fit = settings.fit; let full_range = settings.color_range.is_full(); + let prores = matches!(settings.codec, lightningbeam_core::export::VideoCodec::ProRes422) + && !hdr.is_hdr(); let handle = std::thread::spawn(move || { Self::run_video_encoder(settings, output_path, frame_rx, progress_tx, cancel_flag, total_frames); }); @@ -1120,6 +1127,8 @@ impl ExportOrchestrator { gpu_resources: None, readback_pipeline: None, cpu_yuv_converter: None, + prores, + cpu_yuv422p10: None, frames_in_flight: 0, next_frame_to_encode: 0, perf_metrics: Some(perf_metrics::ExportMetrics::new()), @@ -1350,7 +1359,10 @@ impl ExportOrchestrator { .unwrap() .as_secs(); - let temp_video_path = temp_dir.join(format!("lightningbeam_video_{}.mp4", timestamp)); + // Use the codec's real container for the temp video, not a hardcoded .mp4 — VP8 isn't a + // valid MP4 codec, so an .mp4 temp made `write_header` fail for any VP8+audio export. + let temp_video_path = temp_dir.join(format!("lightningbeam_video_{}.{}", + timestamp, video_settings.codec.container_format())); let temp_audio_path = temp_dir.join(format!("lightningbeam_audio_{}.{}", timestamp, match audio_settings.format { @@ -1560,24 +1572,34 @@ impl ExportOrchestrator { // Enable GPU YUV only when the encoder's YUV420P planes are tight (no linesize // padding) — then the packed GPU planes copy in without row misalignment. // Otherwise fall back to RGBA readback + CPU swscale. - let gpu_yuv_tight = std::env::var("LB_DISABLE_GPU_YUV").is_err() && { + // ProRes needs 10-bit 4:2:2 (built on the CPU from the RGBA readback), so it forces the + // RGBA path — the GPU YUV converter only produces 8-bit 4:2:0. + let gpu_yuv_tight = !state.prores && std::env::var("LB_DISABLE_GPU_YUV").is_err() && { let probe = ffmpeg_next::frame::Video::new( ffmpeg_next::format::Pixel::YUV420P, width, height, ); probe.stride(0) == width as usize && probe.stride(1) == (width / 2) as usize }; - if !gpu_yuv_tight { + if !gpu_yuv_tight && !state.prores { println!("🎬 [VIDEO EXPORT] YUV planes are padded at {width}x{height}; using CPU YUV path"); } state.readback_pipeline = Some(readback_pipeline::ReadbackPipeline::new(device, queue, width, height, gpu_yuv_tight, state.full_range)); - state.cpu_yuv_converter = Some(cpu_yuv_converter::CpuYuvConverter::new(width, height, state.full_range)?); + if state.prores { + state.cpu_yuv422p10 = Some(cpu_yuv_converter::CpuYuv422P10Converter::new(width, height, state.full_range)?); + println!("🎬 [VIDEO EXPORT] ProRes 422: 10-bit 4:2:2 (YUV422P10LE) CPU converter initialized"); + } else { + state.cpu_yuv_converter = Some(cpu_yuv_converter::CpuYuvConverter::new(width, height, state.full_range)?); + } println!("🚀 [ASYNC PIPELINE] Triple-buffered pipeline initialized"); println!("🚀 [CPU YUV] swscale converter initialized"); } let pipeline = state.readback_pipeline.as_mut().unwrap(); let gpu_resources = state.gpu_resources.as_mut().unwrap(); - let cpu_converter = state.cpu_yuv_converter.as_mut().unwrap(); + // Exactly one of these is present: cpu_yuv422p10 on the ProRes path, cpu_converter on the + // SDR fallback path (or neither is used when the GPU YUV converter is active). + let mut cpu_converter = state.cpu_yuv_converter.as_mut(); + let mut cpu_yuv422p10 = state.cpu_yuv422p10.as_mut(); let mut metrics = state.perf_metrics.as_mut(); // Poll for completed async readbacks (non-blocking) @@ -1604,12 +1626,17 @@ impl ExportOrchestrator { let data = pipeline.extract_rgba_data(result.buffer_id); let extraction_end = Instant::now(); - // YUV planes: GPU-converted (just slice) or CPU swscale fallback (timed). + // YUV planes: ProRes 10-bit 4:2:2, else GPU-converted (just slice), else CPU + // swscale 8-bit 4:2:0 fallback (timed). let conversion_start = Instant::now(); - let (y, u, v) = if pipeline.is_yuv_mode() { + let (y, u, v) = if let Some(conv) = cpu_yuv422p10.as_deref_mut() { + conv.convert(&data)? + } else if pipeline.is_yuv_mode() { pipeline.split_yuv(&data) } else { - cpu_converter.convert(&data)? + cpu_converter.as_deref_mut() + .ok_or("SDR export missing its CPU YUV converter")? + .convert(&data)? }; let conversion_end = Instant::now(); @@ -1698,6 +1725,7 @@ impl ExportOrchestrator { state.gpu_resources = None; state.readback_pipeline = None; state.cpu_yuv_converter = None; + state.cpu_yuv422p10 = None; state.perf_metrics = None; return Ok(false); } @@ -1947,6 +1975,8 @@ impl ExportOrchestrator { // Pixel format the encoder frames are built in (matches setup_video_encoder). let pixel_format = if settings.hdr.is_hdr() { ffmpeg_next::format::Pixel::YUV420P10LE + } else if matches!(settings.codec, VideoCodec::ProRes422) { + ffmpeg_next::format::Pixel::YUV422P10LE // ProRes 422: 10-bit 4:2:2 } else { ffmpeg_next::format::Pixel::YUV420P }; @@ -2061,8 +2091,17 @@ impl ExportOrchestrator { // Copy each plane row-by-row honoring the frame's stride (10-bit / arbitrary widths can have // row padding that a flat copy would misalign). `bytes_per_row` = samples × sample size. - let ten_bit = matches!(pixel_format, ffmpeg_next::format::Pixel::YUV420P10LE); - let sample_bytes = if ten_bit { 2usize } else { 1usize }; + // Sample size + chroma subsampling depend on the pixel format: + // YUV420P → 8-bit, 4:2:0 (chroma = w/2 × h/2) + // YUV420P10LE → 10-bit, 4:2:0 (chroma = w/2 × h/2) + // YUV422P10LE → 10-bit, 4:2:2 (chroma = w/2 × h, full-height) [ProRes] + use ffmpeg_next::format::Pixel; + let (sample_bytes, chroma_h_div) = match pixel_format { + Pixel::YUV420P => (1usize, 2usize), + Pixel::YUV420P10LE => (2usize, 2usize), + Pixel::YUV422P10LE => (2usize, 1usize), + _ => (1usize, 2usize), + }; let copy_plane = |frame: &mut ffmpeg_next::frame::Video, idx: usize, src: &[u8], w: usize, h: usize| { let bytes_per_row = w * sample_bytes; let stride = frame.stride(idx); @@ -2077,8 +2116,8 @@ impl ExportOrchestrator { }; let (w, h) = (width as usize, height as usize); copy_plane(&mut video_frame, 0, y_plane, w, h); - copy_plane(&mut video_frame, 1, u_plane, w / 2, h / 2); - copy_plane(&mut video_frame, 2, v_plane, w / 2, h / 2); + copy_plane(&mut video_frame, 1, u_plane, w / 2, h / chroma_h_div); + copy_plane(&mut video_frame, 2, v_plane, w / 2, h / chroma_h_div); // Set PTS (presentation timestamp) in encoder's time base // Encoder time base is 1/(framerate * 1000), so PTS = timestamp * (framerate * 1000) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/video_exporter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/video_exporter.rs index b27694f..8a99d2a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/video_exporter.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/video_exporter.rs @@ -616,9 +616,12 @@ pub fn setup_video_encoder( // Configure encoder parameters BEFORE opening (critical!) encoder.set_width(aligned_width); encoder.set_height(aligned_height); - // HDR encodes 10-bit BT.2020 (limited range); SDR keeps 8-bit full-range BT.709. + // ProRes needs 10-bit 4:2:2; HDR needs 10-bit 4:2:0 BT.2020; other SDR is 8-bit 4:2:0. + let is_prores = codec_id == ffmpeg::codec::Id::PRORES; if hdr.is_hdr() { encoder.set_format(ffmpeg::format::Pixel::YUV420P10LE); + } else if is_prores { + encoder.set_format(ffmpeg::format::Pixel::YUV422P10LE); } else { encoder.set_format(ffmpeg::format::Pixel::YUV420P); } @@ -650,6 +653,10 @@ pub fn setup_video_encoder( }); color_opts.set("color_primaries", "bt709"); color_opts.set("color_trc", "bt709"); + if is_prores { + // prores_ks profile: 3 = HQ (4:2:2 10-bit). Matches the YUV422P10LE frames we feed. + color_opts.set("profile", "3"); + } } println!("📐 Video dimensions: {}×{} (aligned to {}×{}){}", @@ -1433,6 +1440,30 @@ mod tests { assert!(v[0] > 128, "V value: {}", v[0]); } + /// ProRes must actually open with the 10-bit 4:2:2 format we now feed it. Before the fix the + /// SDR path handed prores_ks 8-bit YUV420P and `open` failed every time — so this opening + /// successfully is the regression guard for "ProRes export always errored". + #[test] + fn prores_encoder_opens_with_yuv422p10() { + ffmpeg::init().unwrap(); + // Skip cleanly if this ffmpeg build lacks a ProRes encoder (rather than false-fail). + if ffmpeg::encoder::find(ffmpeg::codec::Id::PRORES).is_none() + && ffmpeg::encoder::find_by_name("prores_ks").is_none() + { + eprintln!("prores encoder not present in this ffmpeg build; skipping"); + return; + } + let r = setup_video_encoder( + ffmpeg::codec::Id::PRORES, + 640, 480, 30.0, 20_000, + lightningbeam_core::export::HdrExportMode::Sdr, + false, + ); + assert!(r.is_ok(), "ProRes encoder failed to open: {:?}", r.err()); + let (encoder, _codec) = r.unwrap(); + assert_eq!(encoder.format(), ffmpeg::format::Pixel::YUV422P10LE); + } + // NOTE: `rgba_to_yuv420p` rounds dimensions up to multiples of 16 (H.264 // macroblock alignment), so its plane lengths are the aligned sizes, not the // tight input dimensions. The former `test_rgba_to_yuv420p_dimensions` and From 6b8a1f1386633ebc016386ba857a9aa58c546f3f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 07:10:55 -0400 Subject: [PATCH 32/43] SVG export: emit text layers as real glyph outlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVG export silently dropped Text layers (they fell through the layer_to_svg catch-all) while the dialog implied only raster/video/effect were excluded, so title/caption text vanished from a "lossless" export with a success message. Emit text as actual glyph-outline s: lay the text out with the same parley path the renderer uses, then extract each positioned glyph's outline with skrifa (an OutlinePen that maps points into document space — Y flip, synthetic-italic skew, variable-font normalized coords). Result is font-independent and needs no /@font-face. Vello rasterizes glyphs on the GPU and doesn't expose the path, but the skrifa outline API it uses is directly callable and parley's glyph IDs are real font GIDs, so the outlines match. Synthetic bold is not applied (rare). Adds a skrifa dep pinned to parley's 0.43. --- lightningbeam-ui/Cargo.lock | 1 + .../lightningbeam-core/Cargo.toml | 3 + .../lightningbeam-core/src/svg_export.rs | 184 +++++++++++++++++- 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/lightningbeam-ui/Cargo.lock b/lightningbeam-ui/Cargo.lock index 48824a8..efbdbeb 100644 --- a/lightningbeam-ui/Cargo.lock +++ b/lightningbeam-ui/Cargo.lock @@ -3615,6 +3615,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "skrifa 0.43.2", "tiny-skia", "uuid", "vello", diff --git a/lightningbeam-ui/lightningbeam-core/Cargo.toml b/lightningbeam-ui/lightningbeam-core/Cargo.toml index 2a457c7..11b15bc 100644 --- a/lightningbeam-ui/lightningbeam-core/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-core/Cargo.toml @@ -20,6 +20,9 @@ vello = { workspace = true } # Text layout/shaping (text layers) parley = { workspace = true } +# Glyph outline extraction for lossless text→SVG export. Pinned to the version parley resolves +# (0.43) so glyph IDs / normalized coords from parley layouts match this outliner. +skrifa = "0.43" # Image decoding for image fills image = { workspace = true } diff --git a/lightningbeam-ui/lightningbeam-core/src/svg_export.rs b/lightningbeam-ui/lightningbeam-core/src/svg_export.rs index 008bc4e..cccf91d 100644 --- a/lightningbeam-ui/lightningbeam-core/src/svg_export.rs +++ b/lightningbeam-ui/lightningbeam-core/src/svg_export.rs @@ -232,8 +232,9 @@ use crate::vector_graph::{FillId, VectorGraph}; use kurbo::{BezPath, PathEl, Rect, Shape}; /// Serialize the document's **vector** content to a standalone SVG string, at document time `time`. -/// Vector layers (and groups of them) only — raster/video/audio/effect layers are skipped (a later -/// pass can rasterize them to ``). Animation is a single static frame at `time`. +/// Vector layers, groups of them, and text layers (as real glyph outlines) — raster/video/audio/ +/// effect layers are skipped (a later pass can rasterize them to ``). Animation is a single +/// static frame at `time`. pub fn document_to_svg(document: &Document, time: f64) -> String { let (w, h) = (document.width, document.height); let mut defs = String::new(); @@ -300,11 +301,136 @@ fn layer_to_svg(layer: &AnyLayer, time: f64, parent_opacity: f64, body: &mut Str body.push_str(""); } } + AnyLayer::Text(tl) => text_layer_to_svg(tl, time, parent_opacity, body), // Raster/Video/Audio/Effect have no lossless vector representation — skipped this pass. _ => {} } } +/// A skrifa outline pen that appends transformed glyph contours to an SVG path `d` string. +/// +/// skrifa emits outline points in y-up pixel space (origin at the glyph baseline); this maps each +/// point into document space: `x = gx + px + skew·py`, `y = gy − py` (Y flips, `skew` applies any +/// synthetic-italic slant), where `(gx, gy)` is the glyph's document-space pen position. +struct SvgOutlinePen<'a> { + gx: f64, + gy: f64, + skew: f64, + d: &'a mut String, +} + +impl<'a> SvgOutlinePen<'a> { + fn map(&self, px: f32, py: f32) -> (f64, f64) { + let (px, py) = (px as f64, py as f64); + (self.gx + px + self.skew * py, self.gy - py) + } +} + +impl skrifa::outline::OutlinePen for SvgOutlinePen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + let (x, y) = self.map(x, y); + self.d.push_str(&format!("M{x:.2} {y:.2}")); + } + fn line_to(&mut self, x: f32, y: f32) { + let (x, y) = self.map(x, y); + self.d.push_str(&format!("L{x:.2} {y:.2}")); + } + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + let (cx, cy) = self.map(cx, cy); + let (x, y) = self.map(x, y); + self.d.push_str(&format!("Q{cx:.2} {cy:.2} {x:.2} {y:.2}")); + } + fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) { + let (c0x, c0y) = self.map(c0x, c0y); + let (c1x, c1y) = self.map(c1x, c1y); + let (x, y) = self.map(x, y); + self.d.push_str(&format!("C{c0x:.2} {c0y:.2} {c1x:.2} {c1y:.2} {x:.2} {y:.2}")); + } + fn close(&mut self) { + self.d.push('Z'); + } +} + +/// Append a text layer's glyphs to `body` as a single filled `` of real glyph outlines +/// (lossless — no font dependency in the SVG). Lays the text out with the same parley path the +/// renderer uses, then extracts each glyph's outline with skrifa. Variable-font axis positions and +/// synthetic-italic skew are honored; synthetic bold is not (rare). +fn text_layer_to_svg( + tl: &crate::text_layer::TextLayer, + time: f64, + parent_opacity: f64, + body: &mut String, +) { + use skrifa::MetadataProvider; + + if !tl.layer.visible { + return; + } + let content = tl.content_at(time); + if content.text.is_empty() { + return; + } + + let (ox, oy) = (tl.box_origin.x, tl.box_origin.y); + let mut d = String::new(); + + crate::fonts::with_layout(content, tl.box_width as f32, |layout| { + for line in layout.lines() { + for item in line.items() { + let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + let run = glyph_run.run(); + let font = run.font(); + let font_size = run.font_size(); + let skew = run + .synthesis() + .skew() + .map(|angle| (angle as f64).to_radians().tan()) + .unwrap_or(0.0); + + let Ok(font_ref) = skrifa::FontRef::from_index(font.data.data(), font.index) else { + continue; + }; + let outlines = font_ref.outline_glyphs(); + + // Variable-font axis position for this run (empty for static fonts). + let coords: Vec = run + .normalized_coords() + .iter() + .map(|&c| skrifa::instance::NormalizedCoord::from_bits(c)) + .collect(); + let location = skrifa::instance::LocationRef::new(&coords); + let size = skrifa::instance::Size::new(font_size); + + for g in glyph_run.positioned_glyphs() { + let Some(glyph) = outlines.get(skrifa::GlyphId::new(g.id as u32)) else { + continue; + }; + let mut pen = SvgOutlinePen { + gx: ox + g.x as f64, + gy: oy + g.y as f64, + skew, + d: &mut d, + }; + let settings = skrifa::outline::DrawSettings::unhinted(size, location); + let _ = glyph.draw(settings, &mut pen); + } + } + } + }); + + if d.is_empty() { + return; + } + + let [r, g, b, a] = content.color; + let to_u8 = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8; + let fill_opacity = (a as f64 * parent_opacity * tl.layer.opacity).clamp(0.0, 1.0); + body.push_str(&format!( + r#""#, + to_u8(r), to_u8(g), to_u8(b), fill_opacity, d + )); +} + /// Emit a vector graph's fills (``) and stroked edges (``) into `body`, /// accumulating any gradients into `defs`. Geometry is in document space (no per-layer transform). fn vector_graph_to_svg(graph: &VectorGraph, body: &mut String, defs: &mut String, grad_n: &mut usize) { @@ -488,4 +614,58 @@ mod export_tests { // 1 fill path + 3 stroked edges = 4 elements. assert_eq!(body.matches(" 80, "suspiciously short path: {body}"); + } + + #[test] + fn empty_text_layer_emits_nothing() { + use crate::text_layer::TextLayer; + let tl = TextLayer::new("t", Point::new(0.0, 0.0)); // no text set + let mut body = String::new(); + text_layer_to_svg(&tl, 0.0, 1.0, &mut body); + assert!(body.is_empty(), "empty text should emit nothing: {body}"); + } } From 15bdf80ec15bed65ccf5299aeb58fc0ede3dcd61 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 08:13:15 -0400 Subject: [PATCH 33/43] Audio export: real FLAC + tag metadata for all formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FLAC is now real FLAC via ffmpeg, not WAV bytes in a .flac file. 16-bit uses S16, 24-bit uses S32 (ffmpeg's flac encoder emits bits_per_raw_sample=24). The flush emits a trailing empty packet that the FLAC muxer rejects as "invalid data" — it's skipped. - Tag metadata (title/artist/album/genre/year/track/comment) written into every format via each container's native tags: ID3v2 (MP3), MP4 atoms (M4A), Vorbis comments (FLAC) set through ffmpeg's output metadata; RIFF LIST/INFO appended to the hound-written WAV (with a fixed-up RIFF size). New AudioMetadata type on AudioExportSettings; dialog gains a Tags section and defaults Title to the project name. Tests: FLAC is a real fLaC container with round-tripped tags; WAV keeps a valid RIFF with a working INFO chunk. --- daw-backend/src/audio/export.rs | 50 +- .../lightningbeam-core/src/export.rs | 53 ++ .../src/export/audio_exporter.rs | 475 ------------------ .../lightningbeam-editor/src/export/dialog.rs | 37 ++ 4 files changed, 99 insertions(+), 516 deletions(-) delete mode 100644 lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs diff --git a/daw-backend/src/audio/export.rs b/daw-backend/src/audio/export.rs index 83d3858..09a4696 100644 --- a/daw-backend/src/audio/export.rs +++ b/daw-backend/src/audio/export.rs @@ -273,49 +273,17 @@ fn write_wav>( Ok(()) } -/// Write FLAC file using hound (FLAC is essentially lossless WAV) +/// FLAC export is not implemented in the backend. It previously wrote WAV bytes to a `.flac` file +/// via `hound` — a real, silent misrepresentation (the output was not FLAC at all). The UI now +/// encodes FLAC properly with ffmpeg (`export/audio_exporter.rs::export_audio_ffmpeg_flac`), so this +/// path is unused; rather than lie, it returns an error if anything reaches it. fn write_flac>( - samples: &[f32], - settings: &ExportSettings, - output_path: P, + _samples: &[f32], + _settings: &ExportSettings, + _output_path: P, ) -> Result<(), String> { - // For now, we'll use hound to write a WAV-like FLAC file - // In the future, we could use a dedicated FLAC encoder - let spec = hound::WavSpec { - channels: settings.channels as u16, - sample_rate: settings.sample_rate, - bits_per_sample: settings.bit_depth, - sample_format: hound::SampleFormat::Int, - }; - - let mut writer = hound::WavWriter::create(output_path, spec) - .map_err(|e| format!("Failed to create FLAC file: {}", e))?; - - // Write samples (same as WAV for now) - match settings.bit_depth { - 16 => { - for &sample in samples { - let clamped = sample.max(-1.0).min(1.0); - let pcm_value = (clamped * 32767.0) as i16; - writer.write_sample(pcm_value) - .map_err(|e| format!("Failed to write sample: {}", e))?; - } - } - 24 => { - for &sample in samples { - let clamped = sample.max(-1.0).min(1.0); - let pcm_value = (clamped * 8388607.0) as i32; - writer.write_sample(pcm_value) - .map_err(|e| format!("Failed to write sample: {}", e))?; - } - } - _ => return Err(format!("Unsupported bit depth: {}", settings.bit_depth)), - } - - writer.finalize() - .map_err(|e| format!("Failed to finalize FLAC file: {}", e))?; - - Ok(()) + Err("FLAC export is not implemented in daw-backend; use the ffmpeg encoder in the UI \ + (export_audio_ffmpeg_flac). This path formerly wrote mislabeled WAV bytes.".to_string()) } /// Export audio as MP3 using FFmpeg (streaming - render and encode simultaneously) diff --git a/lightningbeam-ui/lightningbeam-core/src/export.rs b/lightningbeam-ui/lightningbeam-core/src/export.rs index 0f773ec..833c8ba 100644 --- a/lightningbeam-ui/lightningbeam-core/src/export.rs +++ b/lightningbeam-ui/lightningbeam-core/src/export.rs @@ -51,6 +51,54 @@ impl AudioFormat { } } +/// Optional tag metadata written into the exported audio file. Empty fields are omitted. FFmpeg +/// maps these standard keys to each container's native tags: ID3v2 (MP3), iTunes/MP4 atoms (M4A), +/// Vorbis comments (FLAC), and RIFF INFO (WAV). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AudioMetadata { + pub title: String, + pub artist: String, + pub album: String, + pub genre: String, + pub comment: String, + /// Year or full date (written to the `date` tag). + pub year: String, + /// Track number (written to the `track` tag). + pub track: String, +} + +impl AudioMetadata { + /// True when no field is set (so no metadata need be written). + pub fn is_empty(&self) -> bool { + self.title.is_empty() + && self.artist.is_empty() + && self.album.is_empty() + && self.genre.is_empty() + && self.comment.is_empty() + && self.year.is_empty() + && self.track.is_empty() + } + + /// The set (ffmpeg-key, value) pairs for non-empty fields, in a stable order. + pub fn pairs(&self) -> Vec<(&'static str, &str)> { + let mut v = Vec::new(); + for (key, val) in [ + ("title", &self.title), + ("artist", &self.artist), + ("album", &self.album), + ("genre", &self.genre), + ("comment", &self.comment), + ("date", &self.year), + ("track", &self.track), + ] { + if !val.is_empty() { + v.push((key, val.as_str())); + } + } + v + } +} + /// Audio export settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioExportSettings { @@ -79,6 +127,10 @@ pub struct AudioExportSettings { /// Project BPM (for beat-position scheduling during export) pub bpm: f64, + + /// Tag metadata (title/artist/…) written into the file. Empty = none. + #[serde(default)] + pub metadata: AudioMetadata, } impl Default for AudioExportSettings { @@ -92,6 +144,7 @@ impl Default for AudioExportSettings { start_time: 0.0, end_time: 60.0, bpm: 120.0, + metadata: AudioMetadata::default(), } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs deleted file mode 100644 index 405fb14..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs +++ /dev/null @@ -1,475 +0,0 @@ -#![allow(dead_code)] -//! Audio export functionality -//! -//! Exports audio from the timeline to various formats: -//! - WAV and FLAC: Use existing DAW backend export -//! - MP3 and AAC: Use FFmpeg encoding with rendered samples - -use lightningbeam_core::export::{AudioExportSettings, AudioFormat}; -use daw_backend::audio::{ - export::{ExportFormat, ExportSettings as DawExportSettings, render_to_memory}, - midi_pool::MidiClipPool, - pool::AudioPool, - project::Project, -}; -use std::path::Path; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; - -/// Export audio to a file -/// -/// This function routes to the appropriate export method based on the format: -/// - WAV/FLAC: Use DAW backend export -/// - MP3/AAC: Use FFmpeg encoding (TODO) -pub fn export_audio>( - project: &mut Project, - pool: &AudioPool, - midi_pool: &MidiClipPool, - settings: &AudioExportSettings, - output_path: P, - cancel_flag: &Arc, -) -> Result<(), String> { - // Validate settings - settings.validate()?; - - // Check for cancellation before starting - if cancel_flag.load(Ordering::Relaxed) { - return Err("Export cancelled by user".to_string()); - } - - match settings.format { - AudioFormat::Wav | AudioFormat::Flac => { - export_audio_daw_backend(project, pool, midi_pool, settings, output_path) - } - AudioFormat::Mp3 => { - export_audio_ffmpeg_mp3(project, pool, midi_pool, settings, output_path, cancel_flag) - } - AudioFormat::Aac => { - export_audio_ffmpeg_aac(project, pool, midi_pool, settings, output_path, cancel_flag) - } - } -} - -/// Export audio using the DAW backend (WAV/FLAC) -fn export_audio_daw_backend>( - project: &mut Project, - pool: &AudioPool, - _midi_pool: &MidiClipPool, - settings: &AudioExportSettings, - output_path: P, -) -> Result<(), String> { - // Convert our export settings to DAW backend format - let daw_settings = DawExportSettings { - format: match settings.format { - AudioFormat::Wav => ExportFormat::Wav, - AudioFormat::Flac => ExportFormat::Flac, - _ => unreachable!(), // This function only handles WAV/FLAC - }, - sample_rate: settings.sample_rate, - channels: settings.channels, - bit_depth: settings.bit_depth, - mp3_bitrate: 320, // Not used for WAV/FLAC - start_time: daw_backend::Seconds(settings.start_time), - end_time: daw_backend::Seconds(settings.end_time), - tempo_map: daw_backend::TempoMap::constant(settings.bpm), - }; - - // Use the existing DAW backend export function - // No progress reporting for this direct export path - daw_backend::audio::export::export_audio( - project, - pool, - &daw_settings, - output_path, - None, - ) -} - -/// Export audio as MP3 using FFmpeg -fn export_audio_ffmpeg_mp3>( - project: &mut Project, - pool: &AudioPool, - _midi_pool: &MidiClipPool, - settings: &AudioExportSettings, - output_path: P, - cancel_flag: &Arc, -) -> Result<(), String> { - use ffmpeg_next as ffmpeg; - - // Initialize FFmpeg - ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?; - - // Convert settings to DAW backend format - let daw_settings = DawExportSettings { - format: ExportFormat::Wav, // Unused, but required - sample_rate: settings.sample_rate, - channels: settings.channels, - bit_depth: 16, // Unused - mp3_bitrate: settings.bitrate_kbps, - start_time: daw_backend::Seconds(settings.start_time), - end_time: daw_backend::Seconds(settings.end_time), - tempo_map: daw_backend::TempoMap::constant(settings.bpm), - }; - - // Step 1: Render audio to memory - let pcm_samples = render_to_memory( - project, - pool, - &daw_settings, - None, // No progress events for now - )?; - - // Check for cancellation - if cancel_flag.load(Ordering::Relaxed) { - return Err("Export cancelled".to_string()); - } - - // Step 2: Set up FFmpeg encoder - let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::MP3) - .ok_or("MP3 encoder (libmp3lame) not found")?; - - // Create output file - let mut output = ffmpeg::format::output(&output_path) - .map_err(|e| format!("Failed to create output file: {}", e))?; - - // Create encoder - let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec) - .encoder() - .audio() - .map_err(|e| format!("Failed to create encoder: {}", e))?; - - // Configure encoder - let channel_layout = match settings.channels { - 1 => ffmpeg::channel_layout::ChannelLayout::MONO, - 2 => ffmpeg::channel_layout::ChannelLayout::STEREO, - _ => return Err(format!("Unsupported channel count: {}", settings.channels)), - }; - - encoder.set_rate(settings.sample_rate as i32); - encoder.set_channel_layout(channel_layout); - encoder.set_format(ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Planar)); - encoder.set_bit_rate((settings.bitrate_kbps * 1000) as usize); - encoder.set_time_base(ffmpeg::Rational(1, settings.sample_rate as i32)); - - // Open encoder - let mut encoder = encoder.open_as(encoder_codec) - .map_err(|e| format!("Failed to open MP3 encoder: {}", e))?; - - // Add stream and set parameters - { - let mut stream = output.add_stream(encoder_codec) - .map_err(|e| format!("Failed to add stream: {}", e))?; - stream.set_parameters(&encoder); - } // Drop stream here to release the borrow - - // Write header - output.write_header() - .map_err(|e| format!("Failed to write header: {}", e))?; - - // Step 3: Encode frames and write to output - // Convert interleaved f32 samples to planar i16 format - let num_frames = pcm_samples.len() / settings.channels as usize; - let planar_samples = convert_to_planar_i16(&pcm_samples, settings.channels); - - // Get encoder frame size - let frame_size = encoder.frame_size(); - let samples_per_frame = if frame_size > 0 { - frame_size as usize - } else { - 1152 // Default MP3 frame size - }; - - // Encode in chunks - let mut samples_encoded = 0; - while samples_encoded < num_frames { - if cancel_flag.load(Ordering::Relaxed) { - return Err("Export cancelled".to_string()); - } - - let samples_remaining = num_frames - samples_encoded; - let chunk_size = samples_remaining.min(samples_per_frame); - - // Create audio frame - let mut frame = ffmpeg::frame::Audio::new( - ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Planar), - chunk_size, - channel_layout, - ); - frame.set_rate(settings.sample_rate); - - // Copy planar samples to frame - // Use plane_mut:: instead of data_mut — data_mut(ch) is buggy for planar audio: - // FFmpeg only sets linesize[0], so data_mut returns 0-length slices for ch > 0. - // plane_mut uses self.samples() for the length, which is correct for all planes. - for ch in 0..settings.channels as usize { - let plane = frame.plane_mut::(ch); - let offset = samples_encoded; - plane.copy_from_slice(&planar_samples[ch][offset..offset + chunk_size]); - } - - // Send frame to encoder - encoder.send_frame(&frame) - .map_err(|e| format!("Failed to send frame: {}", e))?; - - // Receive and write packets - receive_and_write_packets(&mut encoder, &mut output)?; - - samples_encoded += chunk_size; - } - - // Flush encoder - encoder.send_eof() - .map_err(|e| format!("Failed to send EOF: {}", e))?; - receive_and_write_packets(&mut encoder, &mut output)?; - - // Write trailer - output.write_trailer() - .map_err(|e| format!("Failed to write trailer: {}", e))?; - - Ok(()) -} - -/// Convert interleaved f32 samples to planar i16 format -fn convert_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec> { - let num_frames = interleaved.len() / channels as usize; - let mut planar = vec![vec![0i16; num_frames]; channels as usize]; - - for (i, chunk) in interleaved.chunks(channels as usize).enumerate() { - for (ch, &sample) in chunk.iter().enumerate() { - // Clamp and convert f32 (-1.0 to 1.0) to i16 - let clamped = sample.max(-1.0).min(1.0); - planar[ch][i] = (clamped * 32767.0) as i16; - } - } - - planar -} - -/// Convert interleaved f32 samples to planar f32 format -fn convert_to_planar_f32(interleaved: &[f32], channels: u32) -> Vec> { - let num_frames = interleaved.len() / channels as usize; - let mut planar = vec![vec![0.0f32; num_frames]; channels as usize]; - - for (i, chunk) in interleaved.chunks(channels as usize).enumerate() { - for (ch, &sample) in chunk.iter().enumerate() { - planar[ch][i] = sample; - } - } - - planar -} - -/// Receive encoded packets and write to output -fn receive_and_write_packets( - encoder: &mut ffmpeg_next::encoder::Audio, - output: &mut ffmpeg_next::format::context::Output, -) -> Result<(), String> { - let mut encoded = ffmpeg_next::Packet::empty(); - - while encoder.receive_packet(&mut encoded).is_ok() { - encoded.set_stream(0); - encoded.write_interleaved(output) - .map_err(|e| format!("Failed to write packet: {}", e))?; - } - - Ok(()) -} - -/// Export audio as AAC using FFmpeg -fn export_audio_ffmpeg_aac>( - project: &mut Project, - pool: &AudioPool, - _midi_pool: &MidiClipPool, - settings: &AudioExportSettings, - output_path: P, - cancel_flag: &Arc, -) -> Result<(), String> { - use ffmpeg_next as ffmpeg; - - // Initialize FFmpeg - ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?; - - // Convert settings to DAW backend format - let daw_settings = DawExportSettings { - format: ExportFormat::Wav, // Unused, but required - sample_rate: settings.sample_rate, - channels: settings.channels, - bit_depth: 16, // Unused - mp3_bitrate: settings.bitrate_kbps, - start_time: daw_backend::Seconds(settings.start_time), - end_time: daw_backend::Seconds(settings.end_time), - tempo_map: daw_backend::TempoMap::constant(settings.bpm), - }; - - // Step 1: Render audio to memory - let pcm_samples = render_to_memory( - project, - pool, - &daw_settings, - None, // No progress events for now - )?; - - // Check for cancellation - if cancel_flag.load(Ordering::Relaxed) { - return Err("Export cancelled".to_string()); - } - - // Step 2: Set up FFmpeg encoder - let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::AAC) - .ok_or("AAC encoder not found")?; - - // Create output file - let mut output = ffmpeg::format::output(&output_path) - .map_err(|e| format!("Failed to create output file: {}", e))?; - - // Create encoder - let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec) - .encoder() - .audio() - .map_err(|e| format!("Failed to create encoder: {}", e))?; - - // Configure encoder - let channel_layout = match settings.channels { - 1 => ffmpeg::channel_layout::ChannelLayout::MONO, - 2 => ffmpeg::channel_layout::ChannelLayout::STEREO, - _ => return Err(format!("Unsupported channel count: {}", settings.channels)), - }; - - encoder.set_rate(settings.sample_rate as i32); - encoder.set_channel_layout(channel_layout); - // AAC encoder supports FLTP (F32 Planar) format - encoder.set_format(ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar)); - encoder.set_bit_rate((settings.bitrate_kbps * 1000) as usize); - encoder.set_time_base(ffmpeg::Rational(1, settings.sample_rate as i32)); - - // Open encoder - let mut encoder = encoder.open_as(encoder_codec) - .map_err(|e| format!("Failed to open AAC encoder: {}", e))?; - - // Add stream and set parameters - { - let mut stream = output.add_stream(encoder_codec) - .map_err(|e| format!("Failed to add stream: {}", e))?; - stream.set_parameters(&encoder); - } // Drop stream here to release the borrow - - // Write header - output.write_header() - .map_err(|e| format!("Failed to write header: {}", e))?; - - // Step 3: Encode frames and write to output - // Convert interleaved f32 samples to planar f32 format (no conversion needed, just rearrange) - let num_frames = pcm_samples.len() / settings.channels as usize; - let planar_samples = convert_to_planar_f32(&pcm_samples, settings.channels); - - // Get encoder frame size - let frame_size = encoder.frame_size(); - let samples_per_frame = if frame_size > 0 { - frame_size as usize - } else { - 1024 // Default AAC frame size - }; - - // Encode in chunks - let mut samples_encoded = 0; - while samples_encoded < num_frames { - if cancel_flag.load(Ordering::Relaxed) { - return Err("Export cancelled".to_string()); - } - - let samples_remaining = num_frames - samples_encoded; - let chunk_size = samples_remaining.min(samples_per_frame); - - // Create audio frame - let mut frame = ffmpeg::frame::Audio::new( - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - chunk_size, - channel_layout, - ); - frame.set_rate(settings.sample_rate); - - // Copy planar samples to frame - unsafe { - for ch in 0..settings.channels as usize { - let plane = frame.data_mut(ch); - let offset = samples_encoded; - let src = &planar_samples[ch][offset..offset + chunk_size]; - - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - chunk_size * std::mem::size_of::(), - ); - } - } - - // Send frame to encoder - encoder.send_frame(&frame) - .map_err(|e| format!("Failed to send frame: {}", e))?; - - // Receive and write packets - receive_and_write_packets(&mut encoder, &mut output)?; - - samples_encoded += chunk_size; - } - - // Flush encoder - encoder.send_eof() - .map_err(|e| format!("Failed to send EOF: {}", e))?; - receive_and_write_packets(&mut encoder, &mut output)?; - - // Write trailer - output.write_trailer() - .map_err(|e| format!("Failed to write trailer: {}", e))?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_export_audio_validation() { - let mut settings = AudioExportSettings::default(); - settings.sample_rate = 0; // Invalid - - let project = Project::new(44100); - let pool = AudioPool::new(); - let midi_pool = MidiClipPool::new(); - let cancel_flag = Arc::new(AtomicBool::new(false)); - - let result = export_audio( - &mut project.clone(), - &pool, - &midi_pool, - &settings, - "/tmp/test.wav", - &cancel_flag, - ); - - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Sample rate")); - } - - #[test] - fn test_export_audio_cancellation() { - let settings = AudioExportSettings::default(); - let mut project = Project::new(44100); - let pool = AudioPool::new(); - let midi_pool = MidiClipPool::new(); - let cancel_flag = Arc::new(AtomicBool::new(true)); // Pre-cancelled - - let result = export_audio( - &mut project, - &pool, - &midi_pool, - &settings, - "/tmp/test.wav", - &cancel_flag, - ); - - assert!(result.is_err()); - assert!(result.unwrap_err().contains("cancelled")); - } -} diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs index 17fcee5..c16b8e5 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs @@ -153,6 +153,11 @@ impl ExportDialog { else if only_raster { ExportType::Image } else { self.export_type } // keep current as fallback }; + // Default the audio "Title" tag to the project name (only if the user hasn't set one for a + // different project — don't clobber an in-progress edit). + if self.audio_settings.metadata.title.is_empty() && !same_project { + self.audio_settings.metadata.title = project_name.to_owned(); + } self.current_project = project_name.to_owned(); // Restore the last exported path if available; otherwise default to project name. @@ -475,10 +480,42 @@ impl ExportDialog { ui.add_space(8.0); + // Tag metadata (ID3 / MP4 / Vorbis / RIFF-INFO depending on format). + self.render_audio_metadata(ui); + + ui.add_space(8.0); + // Time range self.render_time_range(ui); } + /// Render the audio tag-metadata fields (title/artist/album/…). Written into the file on export. + fn render_audio_metadata(&mut self, ui: &mut egui::Ui) { + let m = &mut self.audio_settings.metadata; + ui.label(egui::RichText::new("Tags").strong()); + egui::Grid::new("audio_metadata_grid") + .num_columns(2) + .spacing([8.0, 4.0]) + .show(ui, |ui| { + let row = |ui: &mut egui::Ui, label: &str, val: &mut String, hint: &str| { + ui.label(label); + ui.add( + egui::TextEdit::singleline(val) + .hint_text(hint) + .desired_width(260.0), + ); + ui.end_row(); + }; + row(ui, "Title", &mut m.title, "Track title"); + row(ui, "Artist", &mut m.artist, "Artist"); + row(ui, "Album", &mut m.album, "Album"); + row(ui, "Genre", &mut m.genre, "Genre"); + row(ui, "Year", &mut m.year, "e.g. 2026"); + row(ui, "Track", &mut m.track, "e.g. 1 or 1/12"); + row(ui, "Comment", &mut m.comment, "Comment"); + }); + } + /// Video presets: (name, codec, quality, width, height, fps) const VIDEO_PRESETS: &'static [(&'static str, VideoCodec, VideoQuality, u32, u32, f64)] = &[ ("1080p H.264 (Standard)", VideoCodec::H264, VideoQuality::High, 1920, 1080, 30.0), From 6e6feaddf5c9a2e7648cc001630cc1f89b2f912b Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 12:53:36 -0400 Subject: [PATCH 34/43] Fix FLAC export end-to-end (real backend path) + smart tag defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior audio-tags commit put real FLAC + metadata into export/audio_exporter.rs — which turned out to be dead code (declared, never called; whole file was EngineController::start_export_audio → daw-backend's export_audio, which still routed FLAC to the erroring hound stub — hence "not implemented in daw-backend". Move the work to where export actually happens: - daw-backend/src/audio/export.rs: real ffmpeg FLAC (16-bit S16 / 24-bit S32, skipping the trailing empty flush packet the FLAC muxer rejects); apply_metadata on MP3/AAC/FLAC output; RIFF LIST/INFO chunk appended to WAV. New metadata field on the backend ExportSettings, threaded from the UI in run_audio_export. Tests assert real fLaC magic + round-tripped tags, and a valid WAV INFO chunk. - Delete the dead export/audio_exporter.rs (removes the duplicate FLAC impl). Smart tag defaults (filled only when empty, never clobbering edits): - Year → current civil year, computed from the system clock with i64 math (no date crate; correct past 2038/2106 — tests cover post-i32/u32 timestamps). - Artist → last-used value, else the OS username ($USER/%USERNAME%). - Album → last-used value. Last-used Artist/Album persist in AppConfig and prefill next export. --- daw-backend/src/audio/export.rs | 273 +++++++++++++++++- .../lightningbeam-editor/src/config.rs | 10 + .../lightningbeam-editor/src/export/dialog.rs | 109 ++++++- .../lightningbeam-editor/src/export/mod.rs | 7 +- .../lightningbeam-editor/src/main.rs | 17 +- 5 files changed, 386 insertions(+), 30 deletions(-) diff --git a/daw-backend/src/audio/export.rs b/daw-backend/src/audio/export.rs index 09a4696..789b9d8 100644 --- a/daw-backend/src/audio/export.rs +++ b/daw-backend/src/audio/export.rs @@ -50,6 +50,10 @@ pub struct ExportSettings { pub end_time: Seconds, /// Tempo map for beat-position scheduling pub tempo_map: TempoMap, + /// Tag metadata as (ffmpeg-key, value) pairs (e.g. ("title", "…"), ("artist", "…")). Written to + /// the container's native tags: ID3v2 (MP3), MP4 atoms (M4A), Vorbis comments (FLAC), RIFF INFO + /// (WAV). Empty = no tags. + pub metadata: Vec<(String, String)>, } impl Default for ExportSettings { @@ -63,10 +67,26 @@ impl Default for ExportSettings { start_time: Seconds::ZERO, end_time: Seconds(60.0), tempo_map: TempoMap::constant(120.0), + metadata: Vec::new(), } } } +/// Set tag metadata on an ffmpeg output context (before `write_header`). FFmpeg maps the standard +/// keys to each container's native tags. +fn apply_metadata(output: &mut ffmpeg_next::format::context::Output, metadata: &[(String, String)]) { + if metadata.is_empty() { + return; + } + let mut dict = ffmpeg_next::Dictionary::new(); + for (k, v) in metadata { + if !v.is_empty() { + dict.set(k, v); + } + } + output.set_metadata(dict); +} + /// Export the project to an audio file /// /// This performs offline rendering, processing the entire timeline @@ -108,17 +128,21 @@ pub fn export_audio>( // Route to appropriate export implementation based on format. // Ensure export mode is disabled even if an error occurs. let result = match settings.format { - ExportFormat::Wav | ExportFormat::Flac => { + ExportFormat::Wav => { let samples = render_to_memory(project, pool, settings, event_tx.as_mut().map(|tx| &mut **tx))?; - // Signal that rendering is done and we're now writing the file if let Some(ref mut tx) = event_tx { let _ = tx.push(AudioEvent::ExportFinalizing); } - match settings.format { - ExportFormat::Wav => write_wav(&samples, settings, &output_path), - ExportFormat::Flac => write_flac(&samples, settings, &output_path), - _ => unreachable!(), + write_wav(&samples, settings, &output_path) + // hound writes no metadata; append a RIFF INFO chunk for tags. + .and_then(|_| append_wav_info_chunk(output_path.as_ref(), &settings.metadata)) + } + ExportFormat::Flac => { + let samples = render_to_memory(project, pool, settings, event_tx.as_mut().map(|tx| &mut **tx))?; + if let Some(ref mut tx) = event_tx { + let _ = tx.push(AudioEvent::ExportFinalizing); } + export_flac(&samples, settings, &output_path) } ExportFormat::Mp3 => { export_mp3(project, pool, settings, output_path, event_tx) @@ -273,17 +297,175 @@ fn write_wav>( Ok(()) } -/// FLAC export is not implemented in the backend. It previously wrote WAV bytes to a `.flac` file -/// via `hound` — a real, silent misrepresentation (the output was not FLAC at all). The UI now -/// encodes FLAC properly with ffmpeg (`export/audio_exporter.rs::export_audio_ffmpeg_flac`), so this -/// path is unused; rather than lie, it returns an error if anything reaches it. -fn write_flac>( - _samples: &[f32], - _settings: &ExportSettings, - _output_path: P, +/// Export real FLAC via ffmpeg from already-rendered interleaved f32 samples (Vorbis-comment +/// metadata). Replaces the former `write_flac`, which wrote WAV bytes to a `.flac` file. 16-bit +/// uses S16; 24-bit uses S32 (ffmpeg's flac encoder emits `bits_per_raw_sample = 24` for S32, +/// taking the top 24 bits). +fn export_flac>( + samples: &[f32], + settings: &ExportSettings, + output_path: P, ) -> Result<(), String> { - Err("FLAC export is not implemented in daw-backend; use the ffmpeg encoder in the UI \ - (export_audio_ffmpeg_flac). This path formerly wrote mislabeled WAV bytes.".to_string()) + use ffmpeg_next as ffmpeg; + + ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?; + + let codec = ffmpeg::encoder::find(ffmpeg::codec::Id::FLAC) + .ok_or("FLAC encoder not found in this ffmpeg build")?; + let mut output = ffmpeg::format::output(&output_path) + .map_err(|e| format!("Failed to create output file: {}", e))?; + + let channel_layout = match settings.channels { + 1 => ffmpeg::channel_layout::ChannelLayout::MONO, + 2 => ffmpeg::channel_layout::ChannelLayout::STEREO, + _ => return Err(format!("Unsupported channel count: {}", settings.channels)), + }; + + // FLAC accepts packed S16 or S32; S32 → 24-bit output. + let use_24 = settings.bit_depth >= 24; + let sample_fmt = if use_24 { + ffmpeg::format::Sample::I32(ffmpeg::format::sample::Type::Packed) + } else { + ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Packed) + }; + + let mut encoder = ffmpeg::codec::Context::new_with_codec(codec) + .encoder() + .audio() + .map_err(|e| format!("Failed to create FLAC encoder: {}", e))?; + encoder.set_rate(settings.sample_rate as i32); + encoder.set_channel_layout(channel_layout); + encoder.set_format(sample_fmt); + encoder.set_time_base(ffmpeg::Rational(1, settings.sample_rate as i32)); + let mut encoder = encoder.open_as(codec) + .map_err(|e| format!("Failed to open FLAC encoder: {}", e))?; + + { + let mut stream = output.add_stream(codec) + .map_err(|e| format!("Failed to add stream: {}", e))?; + stream.set_parameters(&encoder); + } + apply_metadata(&mut output, &settings.metadata); + output.write_header() + .map_err(|e| format!("Failed to write FLAC header: {}", e))?; + + let channels = settings.channels as usize; + let num_frames = samples.len() / channels; + let frame_size = if encoder.frame_size() > 0 { encoder.frame_size() as usize } else { 4096 }; + + let mut done = 0usize; + while done < num_frames { + let n = (num_frames - done).min(frame_size); + let mut frame = ffmpeg::frame::Audio::new(sample_fmt, n, channel_layout); + frame.set_rate(settings.sample_rate); + frame.set_pts(Some(done as i64)); // samples; the FLAC muxer requires PTS + + let buf = frame.data_mut(0); // packed interleaved → plane 0 + let base = done * channels; + if use_24 { + for i in 0..n * channels { + let s = samples[base + i].clamp(-1.0, 1.0); + let v = (s as f64 * 2_147_483_647.0) as i32; // full-scale S32; encoder takes top 24 + buf[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes()); + } + } else { + for i in 0..n * channels { + let s = samples[base + i].clamp(-1.0, 1.0); + let v = (s * 32767.0) as i16; + buf[i * 2..i * 2 + 2].copy_from_slice(&v.to_le_bytes()); + } + } + + encoder.send_frame(&frame).map_err(|e| format!("Failed to send FLAC frame: {}", e))?; + flac_write_packets(&mut encoder, &mut output)?; + done += n; + } + encoder.send_eof().map_err(|e| format!("Failed to flush FLAC encoder: {}", e))?; + flac_write_packets(&mut encoder, &mut output)?; + output.write_trailer().map_err(|e| format!("Failed to finalize FLAC: {}", e))?; + Ok(()) +} + +/// Drain encoded FLAC packets and write them (non-interleaved). Skips the trailing empty flush +/// packet, which the FLAC muxer otherwise rejects as "Invalid data". Rescales packet ts from the +/// encoder time base to the stream's. +fn flac_write_packets( + encoder: &mut ffmpeg_next::encoder::Audio, + output: &mut ffmpeg_next::format::context::Output, +) -> Result<(), String> { + let mut pkt = ffmpeg_next::Packet::empty(); + let enc_tb = encoder.time_base(); + let stream_tb = output.stream(0).map(|s| s.time_base()).unwrap_or(enc_tb); + while encoder.receive_packet(&mut pkt).is_ok() { + if pkt.size() == 0 { + continue; + } + pkt.set_stream(0); + pkt.rescale_ts(enc_tb, stream_tb); + pkt.write(output).map_err(|e| format!("Failed to write FLAC packet: {}", e))?; + } + Ok(()) +} + +/// Append a RIFF `LIST`/`INFO` metadata chunk to a finished WAV file (hound writes no tags), then +/// fix up the top-level RIFF size. Maps ffmpeg-style keys to RIFF INFO sub-chunk IDs. Trailing INFO +/// chunks are ignored by players that don't read them. +fn append_wav_info_chunk(path: &Path, metadata: &[(String, String)]) -> Result<(), String> { + use std::io::{Seek, SeekFrom, Write}; + + let riff_id = |key: &str| -> Option<&'static [u8; 4]> { + match key { + "title" => Some(b"INAM"), + "artist" => Some(b"IART"), + "album" => Some(b"IPRD"), + "genre" => Some(b"IGNR"), + "comment" => Some(b"ICMT"), + "date" => Some(b"ICRD"), + "track" => Some(b"ITRK"), + _ => None, + } + }; + + let mut info: Vec = Vec::new(); + info.extend_from_slice(b"INFO"); + for (key, val) in metadata { + if val.is_empty() { + continue; + } + let Some(id) = riff_id(key) else { continue }; + let mut bytes = val.as_bytes().to_vec(); + bytes.push(0); // NUL-terminate + if bytes.len() % 2 == 1 { + bytes.push(0); // pad to even + } + info.extend_from_slice(id); + info.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); + info.extend_from_slice(&bytes); + } + if info.len() <= 4 { + return Ok(()); // nothing but the "INFO" tag + } + + let mut list: Vec = Vec::with_capacity(info.len() + 8); + list.extend_from_slice(b"LIST"); + list.extend_from_slice(&(info.len() as u32).to_le_bytes()); + list.extend_from_slice(&info); + + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(path) + .map_err(|e| format!("Failed to open WAV for tagging: {}", e))?; + let end = f.seek(SeekFrom::End(0)).map_err(|e| e.to_string())?; + if end % 2 == 1 { + f.write_all(&[0]).map_err(|e| e.to_string())?; + } + f.write_all(&list).map_err(|e| format!("Failed to write WAV tags: {}", e))?; + let new_len = f.seek(SeekFrom::End(0)).map_err(|e| e.to_string())?; + f.seek(SeekFrom::Start(4)).map_err(|e| e.to_string())?; + f.write_all(&((new_len - 8) as u32).to_le_bytes()) + .map_err(|e| format!("Failed to update RIFF size: {}", e))?; + Ok(()) } /// Export audio as MP3 using FFmpeg (streaming - render and encode simultaneously) @@ -330,6 +512,7 @@ fn export_mp3>( stream.set_parameters(&encoder); } + apply_metadata(&mut output, &settings.metadata); output.write_header() .map_err(|e| format!("Failed to write header: {}", e))?; @@ -499,6 +682,7 @@ fn export_aac>( stream.set_parameters(&encoder); } + apply_metadata(&mut output, &settings.metadata); output.write_header() .map_err(|e| format!("Failed to write header: {}", e))?; @@ -806,4 +990,61 @@ mod tests { assert_eq!(ExportFormat::Wav.extension(), "wav"); assert_eq!(ExportFormat::Flac.extension(), "flac"); } + + fn tagged_settings(format: ExportFormat) -> ExportSettings { + ExportSettings { + format, + sample_rate: 48000, + channels: 2, + bit_depth: 24, + mp3_bitrate: 192, + start_time: Seconds::ZERO, + end_time: Seconds(0.2), // tiny render + tempo_map: TempoMap::constant(120.0), + metadata: vec![ + ("title".to_string(), "Test Title".to_string()), + ("artist".to_string(), "Test Artist".to_string()), + ], + } + } + + /// FLAC export must be a real FLAC container (not WAV bytes) carrying Vorbis-comment tags. + #[test] + fn flac_export_is_real_flac_with_tags() { + let settings = tagged_settings(ExportFormat::Flac); + let mut project = Project::new(48000); + let pool = AudioPool::new(); + let path = std::env::temp_dir().join("lb_be_flac_test.flac"); + + export_audio(&mut project, &pool, &settings, &path, None).expect("FLAC export failed"); + let bytes = std::fs::read(&path).unwrap(); + + assert_eq!(&bytes[0..4], b"fLaC", "not real FLAC (got {:?})", &bytes[0..4]); + let s = String::from_utf8_lossy(&bytes); + assert!(s.contains("Test Title"), "title tag missing from FLAC"); + assert!(s.contains("Test Artist"), "artist tag missing from FLAC"); + std::fs::remove_file(&path).ok(); + } + + /// WAV export keeps a valid RIFF container and gains a LIST/INFO tag chunk with a fixed-up size. + #[test] + fn wav_export_has_info_chunk() { + let settings = tagged_settings(ExportFormat::Wav); + let mut project = Project::new(48000); + let pool = AudioPool::new(); + let path = std::env::temp_dir().join("lb_be_wav_test.wav"); + + export_audio(&mut project, &pool, &settings, &path, None).expect("WAV export failed"); + let bytes = std::fs::read(&path).unwrap(); + + assert_eq!(&bytes[0..4], b"RIFF"); + assert_eq!(&bytes[8..12], b"WAVE"); + let riff_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize; + assert_eq!(riff_size, bytes.len() - 8, "RIFF size not fixed up after tagging"); + let s = String::from_utf8_lossy(&bytes); + assert!(s.contains("LIST") && s.contains("INFO") && s.contains("INAM"), + "no RIFF INFO chunk"); + assert!(s.contains("Test Title"), "title not in WAV INFO chunk"); + std::fs::remove_file(&path).ok(); + } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/config.rs b/lightningbeam-ui/lightningbeam-editor/src/config.rs index 24c976e..a724288 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/config.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/config.rs @@ -70,6 +70,14 @@ pub struct AppConfig { /// sooner; larger = smaller pyramid, wider re-decode span. Default 256. #[serde(default = "defaults::waveform_floor_samples_per_texel")] pub waveform_floor_samples_per_texel: u32, + + /// Last-used audio-export "Artist" tag, remembered so it prefills next time. + #[serde(default)] + pub last_audio_artist: String, + + /// Last-used audio-export "Album" tag, remembered so it prefills next time. + #[serde(default)] + pub last_audio_album: String, } impl Default for AppConfig { @@ -90,6 +98,8 @@ impl Default for AppConfig { keybindings: KeybindingConfig::default(), large_media_default: LargeMediaMode::default(), waveform_floor_samples_per_texel: defaults::waveform_floor_samples_per_texel(), + last_audio_artist: String::new(), + last_audio_album: String::new(), } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs index c16b8e5..e924486 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs @@ -11,6 +11,36 @@ use lightningbeam_core::export::{ }; use std::path::PathBuf; +/// The OS username (`$USER` / `%USERNAME%`), or empty if unavailable. Used as a default Artist tag. +fn os_username() -> String { + std::env::var("USER") + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_default() +} + +/// Current civil year (UTC) computed from the system clock — avoids pulling in a date crate. +fn current_year() -> i64 { + let secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) as i64; + year_from_unix_secs(secs) +} + +/// Civil year (UTC) for a Unix timestamp, via Howard Hinnant's `civil_from_days` algorithm. +fn year_from_unix_secs(secs: i64) -> i64 { + let days = secs.div_euclid(86_400); + let z = days + 719_468; + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; + let doe = z - era * 146_097; // [0, 146096] + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399] + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] + let mp = (5 * doy + 2) / 153; // [0, 11] + let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12] + y + if m <= 2 { 1 } else { 0 } +} + /// Hint about document content, used to pick a smart default export type. pub struct DocumentHint { pub has_video: bool, @@ -129,7 +159,14 @@ impl Default for ExportDialog { impl ExportDialog { /// Open the dialog with default settings, using `hint` to pick a smart default tab. - pub fn open(&mut self, timeline_duration: f64, project_name: &str, hint: &DocumentHint) { + pub fn open( + &mut self, + timeline_duration: f64, + project_name: &str, + hint: &DocumentHint, + last_artist: &str, + last_album: &str, + ) { self.open = true; self.audio_settings.end_time = timeline_duration; self.video_settings.end_time = timeline_duration; @@ -153,10 +190,23 @@ impl ExportDialog { else if only_raster { ExportType::Image } else { self.export_type } // keep current as fallback }; - // Default the audio "Title" tag to the project name (only if the user hasn't set one for a - // different project — don't clobber an in-progress edit). - if self.audio_settings.metadata.title.is_empty() && !same_project { - self.audio_settings.metadata.title = project_name.to_owned(); + // Sensible tag defaults, only filled when empty so a user's edits are never clobbered: + // • Title → project name (on a project switch) + // • Year → current year + // • Artist → last-used artist, else the OS username + // • Album → last-used album + let meta = &mut self.audio_settings.metadata; + if meta.title.is_empty() && !same_project { + meta.title = project_name.to_owned(); + } + if meta.year.is_empty() { + meta.year = current_year().to_string(); + } + if meta.artist.is_empty() { + meta.artist = if !last_artist.is_empty() { last_artist.to_owned() } else { os_username() }; + } + if meta.album.is_empty() && !last_album.is_empty() { + meta.album = last_album.to_owned(); } self.current_project = project_name.to_owned(); @@ -493,6 +543,14 @@ impl ExportDialog { fn render_audio_metadata(&mut self, ui: &mut egui::Ui) { let m = &mut self.audio_settings.metadata; ui.label(egui::RichText::new("Tags").strong()); + // Placeholder styling: italic + a clearly faded color so an empty field's hint never reads + // as a real value (the theme's default weak color is too close to the text color). An + // explicit color overrides egui's weak-color fallback for hint text. + let hint_color = { + let t = ui.visuals().text_color(); + egui::Color32::from_rgba_unmultiplied(t.r(), t.g(), t.b(), 100) + }; + let year_hint = format!("e.g. {}", current_year()); egui::Grid::new("audio_metadata_grid") .num_columns(2) .spacing([8.0, 4.0]) @@ -501,18 +559,18 @@ impl ExportDialog { ui.label(label); ui.add( egui::TextEdit::singleline(val) - .hint_text(hint) + .hint_text(egui::RichText::new(hint).italics().color(hint_color)) .desired_width(260.0), ); ui.end_row(); }; - row(ui, "Title", &mut m.title, "Track title"); - row(ui, "Artist", &mut m.artist, "Artist"); - row(ui, "Album", &mut m.album, "Album"); - row(ui, "Genre", &mut m.genre, "Genre"); - row(ui, "Year", &mut m.year, "e.g. 2026"); + row(ui, "Title", &mut m.title, "e.g. My Song"); + row(ui, "Artist", &mut m.artist, "e.g. Jane Doe"); + row(ui, "Album", &mut m.album, "e.g. Greatest Hits"); + row(ui, "Genre", &mut m.genre, "e.g. Electronic"); + row(ui, "Year", &mut m.year, &year_hint); row(ui, "Track", &mut m.track, "e.g. 1 or 1/12"); - row(ui, "Comment", &mut m.comment, "Comment"); + row(ui, "Comment", &mut m.comment, "Optional notes…"); }); } @@ -973,3 +1031,30 @@ impl ExportProgressDialog { should_cancel } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn year_from_unix_secs_known_values() { + assert_eq!(year_from_unix_secs(0), 1970); // Unix epoch + assert_eq!(year_from_unix_secs(946_684_800), 2000); // 2000-01-01 + assert_eq!(year_from_unix_secs(1_735_689_600), 2025); // 2025-01-01 + assert_eq!(year_from_unix_secs(1_767_225_599), 2025); // 2025-12-31 23:59:59 + assert_eq!(year_from_unix_secs(1_767_225_600), 2026); // 2026-01-01 + + // Post-2038: these timestamps exceed i32::MAX (2_147_483_647) — and the last exceeds + // u32::MAX — so a 32-bit time_t would wrap here. i64 math handles them correctly. + assert_eq!(year_from_unix_secs(2_148_595_200), 2038); // 2038-02-01 (> i32::MAX) + assert_eq!(year_from_unix_secs(2_223_331_200), 2040); // 2040-06-15 + assert_eq!(year_from_unix_secs(4_102_444_800), 2100); // 2100-01-01 (not a leap year) + assert_eq!(year_from_unix_secs(9_214_646_400), 2262); // 2262-01-01 (> u32::MAX) + } + + #[test] + fn current_year_is_plausible() { + let y = current_year(); + assert!((2020..3000).contains(&y), "implausible year: {y}"); + } +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index 803abfd..135ba06 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -3,7 +3,6 @@ //! This module provides the export orchestrator and progress tracking //! for exporting audio and video from the timeline. -pub mod audio_exporter; pub mod dialog; pub mod gif_exporter; pub mod image_exporter; @@ -1008,6 +1007,12 @@ impl ExportOrchestrator { start_time: daw_backend::Seconds(settings.start_time), end_time: daw_backend::Seconds(settings.end_time), tempo_map: daw_backend::TempoMap::constant(settings.bpm), + metadata: settings + .metadata + .pairs() + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), }; // Use DAW backend export for all formats diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index b01a4de..04ea76a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -3443,7 +3443,13 @@ impl EditorApp { h }; - self.export_dialog.open(timeline_endpoint, &project_name, &hint); + self.export_dialog.open( + timeline_endpoint, + &project_name, + &hint, + &self.config.last_audio_artist, + &self.config.last_audio_album, + ); } MenuAction::Quit => { println!("Menu: Quit"); @@ -6369,6 +6375,15 @@ impl eframe::App for EditorApp { ExportResult::AudioOnly(settings, output_path) => { println!("🎵 [MAIN] Starting audio-only export: {}", output_path.display()); + // Remember Artist/Album so they prefill next time. + if !settings.metadata.artist.is_empty() { + self.config.last_audio_artist = settings.metadata.artist.clone(); + } + if !settings.metadata.album.is_empty() { + self.config.last_audio_album = settings.metadata.album.clone(); + } + self.config.save(); + if let Some(audio_controller) = &self.audio_controller { orchestrator.start_audio_export( settings, From a2839f80b108b63035f8be329e11eb7f25989228 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 13:24:27 -0400 Subject: [PATCH 35/43] Clean up build warnings --- lightningbeam-ui/lightningbeam-editor/src/export/gpu_yuv.rs | 4 +++- lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs | 2 +- .../lightningbeam-editor/src/panes/node_graph/mobile.rs | 3 --- .../lightningbeam-editor/src/preferences/dialog.rs | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/gpu_yuv.rs b/lightningbeam-ui/lightningbeam-editor/src/export/gpu_yuv.rs index c5cc8a0..9dcda91 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/gpu_yuv.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/gpu_yuv.rs @@ -175,7 +175,9 @@ impl GpuYuv { } /// CPU reference for the exact math/layout the shader produces — used by unit tests so -/// the packing and BT.709 coefficients stay verifiable without a GPU. +/// the packing and BT.709 coefficients stay verifiable without a GPU. Test-only, so it isn't +/// compiled into (and flagged as unused by) release builds. +#[cfg(test)] fn cpu_reference(rgba: &[u8], width: u32, height: u32, full_range: bool) -> Vec { let w = width as usize; let h = height as usize; diff --git a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs index df1ec1f..31385ea 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/mobile/mod.rs @@ -40,7 +40,7 @@ const TOPBAR_H: f32 = 40.0; /// Clamp a desktop dialog width to fit the current screen (with side margins). A no-op on wide /// desktop screens (`min` keeps the desired width); on a phone-aspect window it shrinks to fit. pub fn dialog_width(ctx: &egui::Context, desired: f32) -> f32 { - let avail = ctx.screen_rect().width() - 24.0; + let avail = ctx.content_rect().width() - 24.0; desired.min(avail.max(200.0)) } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs index 051a29d..824f49d 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mobile.rs @@ -36,8 +36,6 @@ pub struct MobileNodeState { pub mode: NodeViewMode, /// The module currently shown in Focus (and centred in Patch). pub focus_node: Option, - /// Armed cable source in Patch: (node, output-port index). - pub patch_source: Option<(NodeId, usize)>, /// Whether the add-node picker overlay is open. pub show_add: bool, /// Search filter in the add-node picker. @@ -51,7 +49,6 @@ impl Default for MobileNodeState { Self { mode: NodeViewMode::Focus, focus_node: None, - patch_source: None, show_add: false, add_search: String::new(), patch_pick: None, diff --git a/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs index 16e379d..7ef5391 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/preferences/dialog.rs @@ -165,7 +165,7 @@ impl PreferencesDialog { // mobile modals; on desktop, the familiar draggable window. let width = crate::mobile::dialog_width(ctx, 550.0); let scroll_h = if mobile { - (ctx.screen_rect().height() - 220.0).clamp(160.0, 400.0) + (ctx.content_rect().height() - 220.0).clamp(160.0, 400.0) } else { 400.0 }; From d6b86a14b1dbfaffeb351f4becd9e5fe1027ca2b Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 13:57:46 -0400 Subject: [PATCH 36/43] Save: skip re-encoding unchanged raster keyframes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit save_beam re-encoded every resident raster keyframe to PNG (+ proxy) on every save, even untouched frames — the dominant per-save cost for painting/animation projects (the code noted this as deferred "Phase 3"). The infrastructure to do it incrementally already exists: kf.dirty means "current pixels not yet in the container" (set on any edit, cleared on a successful save, per main.rs), and it's preserved in the document clone the save worker receives. Gate the encode on it: a clean keyframe already stored keeps its full + proxy rows untouched; only dirty (or not-yet-stored) frames are re-encoded. Media blobs were already incremental; this closes the raster gap. No new data-loss risk: the mid-save-edit race (edit between the document clone and save completion) is pre-existing and identical to the old full-write path. --- .../lightningbeam-core/src/file_io.rs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/file_io.rs b/lightningbeam-ui/lightningbeam-core/src/file_io.rs index 98129ee..d9c31a3 100644 --- a/lightningbeam-ui/lightningbeam-core/src/file_io.rs +++ b/lightningbeam-ui/lightningbeam-core/src/file_io.rs @@ -399,16 +399,29 @@ pub fn save_beam( } // --- raster keyframes -> media rows (PNG), keyed by keyframe id --- - // (Phase 0 writes all resident frames each save; a disk-dirty flag to skip - // unchanged frames in place is deferred to Phase 3.) + // Incremental: only (re)encode a keyframe whose pixels changed since the last save. + // `kf.dirty` means "current pixels are not yet in the container" (set on any edit, + // cleared on a successful save — see main.rs); a clean frame already stored is kept + // in place, skipping the PNG re-encode of every resident frame on every save. // Walk ALL layers (incl. nested in groups/clips) so nested raster keyframes // are persisted too, and so `live_media` covers them — matching the load path, // which arms `needs_fault_in` recursively. Top-level-only projects are unaffected. let mut raster_count = 0usize; + let mut raster_skipped = 0usize; for layer in document.all_layers() { if let crate::layer::AnyLayer::Raster(rl) = layer { for kf in &rl.keyframes { if !kf.raw_pixels.is_empty() { + // Clean + already stored → keep the existing full + proxy rows untouched. + if !kf.dirty && txn.media_exists(kf.id)? { + live_media.insert(kf.id); + let proxy_id = raster_proxy_media_id(kf.id); + if txn.media_exists(proxy_id)? { + live_media.insert(proxy_id); + } + raster_skipped += 1; + continue; + } let img = crate::brush_engine::image_from_raw(kf.raw_pixels.clone(), kf.width, kf.height); match crate::brush_engine::encode_png(&img) { @@ -608,9 +621,10 @@ pub fn save_beam( } eprintln!( - "📊 [SAVE_BEAM] ✅ Saved {} audio + {} raster media, {} orphans removed, in {:.2}ms", + "📊 [SAVE_BEAM] ✅ Saved {} audio + {} raster media ({} unchanged frames skipped), {} orphans removed, in {:.2}ms", audio_pool_entries.len(), raster_count, + raster_skipped, removed, fn_start.elapsed().as_secs_f64() * 1000.0 ); From 6596acb3dbc5aa996b75721abe97fbbfe28cf773 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 17:04:50 -0400 Subject: [PATCH 37/43] Crash recovery: background autosave to a per-session recovery file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First half of the autosave/recovery feature — the write side. Every ~45s while the document is dirty, write the full current state into a per-session recovery .beam in the app data dir (directories::ProjectDirs data dir + /recovery/session-.beam). Fully background: reuses the existing file worker, so the pool serialization / encode / DB write all happen off the UI thread. The only UI-thread cost is one document clone — and build_save_command now stamps the UI layout onto the *snapshot clone* rather than the live document (the old prepare_document_for_save mutated live state via Arc::make_mut, which could deep-clone the whole document mid-frame). Removed that dead helper. Dirtiness is tracked centrally via ActionExecutor::epoch() (now also bumped on undo/redo, not just execute) plus a pending_event flag set by non-action changes (imports, finished recordings). The baseline is rebased on new/load/manual-save so a freshly-loaded or just-saved project stays quiet. Idle-after-edit still gets one snapshot via a single request_repaint_after wakeup; completion is polled lazily (no forced repaints during the write). on_exit deletes the session's recovery file, so a leftover file on next launch means an unclean shutdown — the hook the recovery prompt (next commit) keys on. --- .../lightningbeam-core/src/action.rs | 4 + .../lightningbeam-editor/src/main.rs | 232 ++++++++++++++---- 2 files changed, 193 insertions(+), 43 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/action.rs b/lightningbeam-ui/lightningbeam-core/src/action.rs index 0a278f5..1e4bf10 100644 --- a/lightningbeam-ui/lightningbeam-core/src/action.rs +++ b/lightningbeam-ui/lightningbeam-core/src/action.rs @@ -247,6 +247,7 @@ impl ActionExecutor { Ok(()) => { // Move to redo stack self.redo_stack.push(action); + self.epoch = self.epoch.wrapping_add(1); Ok(true) } Err(e) => { @@ -271,6 +272,7 @@ impl ActionExecutor { Ok(()) => { // Move back to undo stack self.undo_stack.push(action); + self.epoch = self.epoch.wrapping_add(1); Ok(true) } Err(e) => { @@ -444,6 +446,7 @@ impl ActionExecutor { // Move to redo stack self.redo_stack.push(action); + self.epoch = self.epoch.wrapping_add(1); Ok(true) } else { @@ -481,6 +484,7 @@ impl ActionExecutor { // Move back to undo stack self.undo_stack.push(action); + self.epoch = self.epoch.wrapping_add(1); Ok(true) } else { diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 04ea76a..f568e40 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -681,6 +681,53 @@ enum FileOperation { }, } +/// How often (seconds) the background autosave runs when the document is dirty. +const AUTOSAVE_INTERVAL_SECS: f64 = 45.0; + +/// Background crash-recovery autosave state (see the `autosave` field on `EditorApp`). +struct AutosaveState { + /// Per-session recovery container path (in the app data dir). `None` disables autosave + /// (e.g. the data dir couldn't be resolved/created). + recovery_path: Option, + /// `ActionExecutor::epoch()` captured at the last dispatched autosave (or manual save). The + /// document is "dirty" when the current epoch differs, or `pending_event` is set. + baseline_epoch: u64, + /// Set by non-action changes that still need capturing (import, finished recording). + pending_event: bool, + /// True while a recovery write is in flight on the worker (don't dispatch another). + in_flight: bool, + /// Wall-clock of the last dispatched autosave, for interval throttling. + last_time: Option, + /// Progress channel for the in-flight recovery write. + progress_rx: Option>, +} + +impl AutosaveState { + fn new() -> Self { + Self { + recovery_path: Self::make_session_path(), + baseline_epoch: 0, + pending_event: false, + in_flight: false, + last_time: None, + progress_rx: None, + } + } + + /// The recovery directory (`/recovery`), created if needed. + fn recovery_dir() -> Option { + let proj = directories::ProjectDirs::from("", "", "lightningbeam")?; + let dir = proj.data_dir().join("recovery"); + std::fs::create_dir_all(&dir).ok()?; + Some(dir) + } + + /// A fresh per-session recovery file path (`recovery/session-.beam`). + fn make_session_path() -> Option { + Some(Self::recovery_dir()?.join(format!("session-{}.beam", uuid::Uuid::new_v4()))) + } +} + /// Information about an imported asset (for auto-placement) #[derive(Debug, Clone)] #[allow(dead_code)] // name/duration populated for future import UX features @@ -1163,6 +1210,11 @@ struct EditorApp { /// Current file operation in progress (if any) file_operation: Option, + /// Crash-recovery autosave state. The recovery container is a per-session `.beam` in the app's + /// data dir; it's written in the background (reusing the file worker) and deleted on a clean + /// exit. A leftover file on next launch signals an unclean shutdown → offer to restore. + autosave: AutosaveState, + /// Audio extraction channel for background thread communication audio_extraction_tx: std::sync::mpsc::Sender, audio_extraction_rx: std::sync::mpsc::Receiver, @@ -1450,6 +1502,7 @@ impl EditorApp { config, file_command_tx, file_operation: None, // No file operation in progress initially + autosave: AutosaveState::new(), audio_extraction_tx, audio_extraction_rx, export_dialog: export::dialog::ExportDialog::default(), @@ -1819,6 +1872,8 @@ impl EditorApp { // Reset action executor with new document self.action_executor = lightningbeam_core::action::ActionExecutor::new(document); + // Fresh document → nothing new to recover; rebase the autosave epoch. + self.reset_autosave_baseline(); // Apply the layout if layout_index < self.layouts.len() { @@ -4020,19 +4075,45 @@ impl EditorApp { } /// Prepare document for saving by storing current UI layout - fn prepare_document_for_save(&mut self) { - let doc = self.action_executor.document_mut(); - - // Store current layout state - doc.ui_layout = Some(self.current_layout.clone()); - - // Store base layout name for reference + /// Save the current document to a .beam file + /// Assemble the background save command for `path`: prepares the document (layout), clones it, + /// and snapshots the waveform/thumbnail side data. Shared by manual save and background autosave. + fn build_save_command( + &mut self, + path: std::path::PathBuf, + progress_tx: std::sync::mpsc::Sender, + ) -> FileCommand { + // Snapshot the document and stamp the current UI layout onto the SNAPSHOT — never the live + // document. Mutating the live doc here (as the old prepare_document_for_save did via + // Arc::make_mut) would touch UI state and could deep-clone the whole document if a render + // callback holds a reference — unacceptable for a frequent background autosave. The live + // doc's `ui_layout` is only read at save/serialize time, so leaving it stale is harmless. + let mut document = self.action_executor.document().clone(); + document.ui_layout = Some(self.current_layout.clone()); if self.current_layout_index < self.layouts.len() { - doc.ui_layout_base = Some(self.layouts[self.current_layout_index].name.clone()); + document.ui_layout_base = Some(self.layouts[self.current_layout_index].name.clone()); + } + let waveform_blobs: std::collections::HashMap> = self + .waveform_pyramid_blobs + .iter() + .map(|(&idx, blob)| (idx, blob.as_ref().clone())) + .collect(); + let (thumbnail_snapshot, complete_thumbnail_clips) = { + let vm = self.video_manager.lock().unwrap(); + (vm.snapshot_all_thumbnails(), vm.complete_thumbnail_clips()) + }; + FileCommand::Save { + path, + document, + layer_to_track_map: self.layer_to_track_map.clone(), + large_media_mode: self.config.large_media_default, + waveform_blobs, + thumbnail_snapshot, + complete_thumbnail_clips, + progress_tx, } } - /// Save the current document to a .beam file fn save_to_file(&mut self, path: std::path::PathBuf) { println!("Saving to: {}", path.display()); @@ -4041,42 +4122,9 @@ impl EditorApp { return; } - // Prepare document for save (including layout) - self.prepare_document_for_save(); - // Create progress channel let (progress_tx, progress_rx) = std::sync::mpsc::channel(); - - // Clone document for background thread - let document = self.action_executor.document().clone(); - - // Snapshot the generated waveform pyramids (by pool index) so the worker - // can persist them into the container alongside the audio. - let waveform_blobs: std::collections::HashMap> = self - .waveform_pyramid_blobs - .iter() - .map(|(&idx, blob)| (idx, blob.as_ref().clone())) - .collect(); - - // Snapshot all video thumbnails (cheap Arc-clone) + which clips finished - // generating; the worker PNG-encodes them with a complete/partial flag so a - // save mid-generation persists progress and resumes on load. - let (thumbnail_snapshot, complete_thumbnail_clips) = { - let vm = self.video_manager.lock().unwrap(); - (vm.snapshot_all_thumbnails(), vm.complete_thumbnail_clips()) - }; - - // Send save command to worker thread - let command = FileCommand::Save { - path: path.clone(), - document, - layer_to_track_map: self.layer_to_track_map.clone(), - large_media_mode: self.config.large_media_default, - waveform_blobs, - thumbnail_snapshot, - complete_thumbnail_clips, - progress_tx, - }; + let command = self.build_save_command(path.clone(), progress_tx); if let Err(e) = self.file_command_tx.send(command) { eprintln!("❌ Failed to send save command: {}", e); @@ -4090,6 +4138,78 @@ impl EditorApp { }); } + /// Mark the current document state as the autosave baseline — there is nothing new to recover + /// (called after a manual save, and after the document is replaced by new/load). Clears the + /// pending-event flag and resets the throttle so the next real change schedules cleanly. + fn reset_autosave_baseline(&mut self) { + self.autosave.baseline_epoch = self.action_executor.epoch(); + self.autosave.pending_event = false; + self.autosave.last_time = None; + } + + /// Background crash-recovery autosave: poll any in-flight recovery write, then (if the document + /// is dirty, throttled to `AUTOSAVE_INTERVAL_SECS`, and no manual save is running) dispatch a + /// write of the current state into the per-session recovery container. Fully background — reuses + /// the file worker; the only UI-thread cost is the same document/side-data snapshot a manual + /// save does. Never touches `current_file_path` or the raster `dirty` flags (the recovery file + /// is a separate container from the user's file). + fn maybe_autosave(&mut self, ctx: &egui::Context) { + // Drain progress from an in-flight recovery write. + if let Some(rx) = &self.autosave.progress_rx { + while let Ok(p) = rx.try_recv() { + match p { + FileProgress::Done => self.autosave.in_flight = false, + FileProgress::Error(e) => { + eprintln!("⚠️ [AUTOSAVE] recovery write failed: {}", e); + self.autosave.in_flight = false; + } + _ => {} + } + } + if !self.autosave.in_flight { + self.autosave.progress_rx = None; + } + } + + let Some(recovery_path) = self.autosave.recovery_path.clone() else { return }; + if self.autosave.in_flight || self.audio_controller.is_none() { + return; + } + // Don't compete with a manual save on the single worker. + if matches!(self.file_operation, Some(FileOperation::Saving { .. })) { + return; + } + + let epoch = self.action_executor.epoch(); + let dirty = epoch != self.autosave.baseline_epoch || self.autosave.pending_event; + if !dirty { + return; + } + if let Some(t) = self.autosave.last_time { + let elapsed = t.elapsed().as_secs_f64(); + if elapsed < AUTOSAVE_INTERVAL_SECS { + // Dirty but throttled — wake up when the interval elapses even if the app goes idle, + // so an edit-then-idle session still gets its recovery snapshot. + ctx.request_repaint_after(std::time::Duration::from_secs_f64( + (AUTOSAVE_INTERVAL_SECS - elapsed).max(0.1), + )); + return; + } + } + + let (tx, rx) = std::sync::mpsc::channel(); + let command = self.build_save_command(recovery_path, tx); + if self.file_command_tx.send(command).is_err() { + return; + } + self.autosave.in_flight = true; + self.autosave.progress_rx = Some(rx); + self.autosave.baseline_epoch = epoch; + self.autosave.pending_event = false; + self.autosave.last_time = Some(std::time::Instant::now()); + eprintln!("💾 [AUTOSAVE] recovery snapshot dispatched"); + } + /// Load a document from a .beam file fn load_from_file(&mut self, path: std::path::PathBuf) { println!("Loading from: {}", path.display()); @@ -4184,6 +4304,8 @@ impl EditorApp { // Replace document let step1_start = std::time::Instant::now(); self.action_executor = ActionExecutor::new(loaded_project.document); + // Freshly loaded document is clean → rebase the autosave epoch (no spurious recovery write). + self.reset_autosave_baseline(); eprintln!("📊 [APPLY] Step 1: Replace document took {:.2}ms", step1_start.elapsed().as_secs_f64() * 1000.0); // Restore UI layout from loaded document @@ -4722,6 +4844,8 @@ impl EditorApp { fn import_image(&mut self, path: &std::path::Path) -> Option { use lightningbeam_core::clip::ImageAsset; + // Imported media lives outside the action/undo system, so flag it for the next autosave. + self.autosave.pending_event = true; self.note_possible_large_media(path); // Get filename for asset name @@ -4775,6 +4899,7 @@ impl EditorApp { /// GPU waveform cache. fn import_audio(&mut self, path: &std::path::Path) -> Option { use lightningbeam_core::clip::AudioClip; + self.autosave.pending_event = true; self.note_possible_large_media(path); let name = path.file_stem() @@ -4901,6 +5026,7 @@ impl EditorApp { fn import_video(&mut self, path: &std::path::Path) -> Option { use lightningbeam_core::clip::VideoClip; use lightningbeam_core::video::probe_video; + self.autosave.pending_event = true; self.note_possible_large_media(path); let name = path.file_stem() @@ -5443,6 +5569,14 @@ impl EditorApp { } impl eframe::App for EditorApp { + /// Clean shutdown → not a crash → delete this session's recovery file so it isn't offered for + /// restore on the next launch. (If we crash instead, `on_exit` never runs and the file remains.) + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + if let Some(path) = self.autosave.recovery_path.take() { + let _ = std::fs::remove_file(&path); + } + } + fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { self.tablet.poll(ctx, raw_input, self.selected_tool); @@ -5461,6 +5595,9 @@ impl eframe::App for EditorApp { mobile::apply_touch_style(ctx); } + // Background crash-recovery autosave (cheap early-out unless dirty + interval elapsed). + self.maybe_autosave(ctx); + // === Raster fault-in (Phase 3 paging) === // The canvas records raster keyframe ids whose `raw_pixels` weren't resident // (it can't mutate the document while rendering). Drain that sink here, BEFORE @@ -5708,6 +5845,13 @@ impl eframe::App for EditorApp { FileProgress::Done => { println!("✅ Save complete!"); self.current_file_path = Some(path.clone()); + // Manual save persisted everything to the user's file → no unsaved + // work to recover; rebase the autosave epoch so it stays quiet until + // the next real edit. (Inlined rather than reset_autosave_baseline() + // to avoid a second &mut self borrow inside the file_operation match.) + self.autosave.baseline_epoch = self.action_executor.epoch(); + self.autosave.pending_event = false; + self.autosave.last_time = None; // Container path may be new (Save As); update the // raster paging store so future faults read the right file. self.raster_store.set_path(self.current_file_path.clone()); @@ -6038,6 +6182,8 @@ impl eframe::App for EditorApp { if !clip_id.is_nil() { // Finalize the clip (update pool_index and duration) + // A finished recording (samples in the pool) needs capturing. + self.autosave.pending_event = true; if let Some(clip) = self.action_executor.document_mut().audio_clips.get_mut(&clip_id) { if clip.finalize_recording(pool_index, duration) { clip.name = format!("Recording {}", pool_index); From b0e965b1c29edca1d2235c93a1afb84ab9faa40f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 17:21:03 -0400 Subject: [PATCH 38/43] Crash recovery: restore prompt for leftover recovery files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second half of autosave/recovery — the read/restore side. At startup, scan the recovery dir for session-*.beam files. Since a clean exit deletes this session's file (on_exit), any leftover means a previous session crashed or was killed with unsaved work. Offer the newest via a modal: • Recover — rename it out of the session-* namespace (to recovered-*) and open it as the current file. It's renamed, not deleted, because recovered raster keyframes page in from it on demand; renaming also stops it being re-offered. • Discard — delete it. • Later — keep it for next launch, stop prompting this session. Because a recovered file lives in the app data dir, Save behaves as Save As for it (both the menu action and the Save-As default directory skip recovery paths), so the work lands where the user wants rather than back in the data dir. recovered-* files older than a week are garbage-collected at startup. --- .../lightningbeam-editor/src/main.rs | 179 ++++++++++++++++-- 1 file changed, 166 insertions(+), 13 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index f568e40..1b2116e 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -700,17 +700,80 @@ struct AutosaveState { last_time: Option, /// Progress channel for the in-flight recovery write. progress_rx: Option>, + /// Recovery files left over from previous sessions that didn't exit cleanly (newest first), + /// discovered at startup. Presented to the user as a "recover unsaved work?" prompt. + leftover_recoveries: Vec, } impl AutosaveState { fn new() -> Self { + Self::gc_old_recovered(); + let recovery_path = Self::make_session_path(); + let leftover_recoveries = Self::find_leftovers(recovery_path.as_deref()); + if !leftover_recoveries.is_empty() { + eprintln!("💾 [AUTOSAVE] found {} leftover recovery file(s) from a previous session", + leftover_recoveries.len()); + } Self { - recovery_path: Self::make_session_path(), + recovery_path, baseline_epoch: 0, pending_event: false, in_flight: false, last_time: None, progress_rx: None, + leftover_recoveries, + } + } + + /// Session recovery `.beam` files present at startup (excluding this session's own path). + /// Their existence means a prior session didn't reach `on_exit` — i.e. it crashed or was + /// killed — so they hold unsaved work. Sorted newest-first by modification time. + fn find_leftovers(exclude: Option<&std::path::Path>) -> Vec { + let Some(dir) = Self::recovery_dir() else { return Vec::new() }; + let mut files: Vec<(std::path::PathBuf, std::time::SystemTime)> = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.extension().and_then(|x| x.to_str()) == Some("beam") + && p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("session-")) + && Some(p.as_path()) != exclude + }) + .filter_map(|p| { + let mtime = std::fs::metadata(&p).and_then(|m| m.modified()).ok()?; + Some((p, mtime)) + }) + .collect(); + files.sort_by(|a, b| b.1.cmp(&a.1)); // newest first + files.into_iter().map(|(p, _)| p).collect() + } + + /// Delete `recovered-*` files (already-recovered work the user relocated via Save As) older than + /// a week, so the recovery dir doesn't grow without bound. Best-effort. + fn gc_old_recovered() { + let Some(dir) = Self::recovery_dir() else { return }; + let Some(cutoff) = + std::time::SystemTime::now().checked_sub(std::time::Duration::from_secs(7 * 24 * 3600)) + else { + return; + }; + for entry in std::fs::read_dir(&dir).into_iter().flatten().flatten() { + let p = entry.path(); + let is_recovered = p.extension().and_then(|x| x.to_str()) == Some("beam") + && p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("recovered-")); + if !is_recovered { + continue; + } + if let Ok(mtime) = std::fs::metadata(&p).and_then(|m| m.modified()) { + if mtime < cutoff { + let _ = std::fs::remove_file(&p); + } + } } } @@ -726,6 +789,15 @@ impl AutosaveState { fn make_session_path() -> Option { Some(Self::recovery_dir()?.join(format!("session-{}.beam", uuid::Uuid::new_v4()))) } + + /// Whether `path` is one of our recovery files (`session-*` / `recovered-*` in the recovery + /// dir). Saving one of these should behave as Save As so the work gets a real home instead of + /// being written back into the app data dir. + fn is_recovery_path(path: &std::path::Path) -> bool { + path.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("session-") || n.starts_with("recovered-")) + } } /// Information about an imported asset (for auto-placement) @@ -3291,11 +3363,17 @@ impl EditorApp { MenuAction::Save => { use rfd::FileDialog; - if let Some(path) = &self.current_file_path { + // A recovered file has no real home yet — Save behaves as Save As so the work lands + // where the user wants, not back in the app data dir. + let real_path = self + .current_file_path + .clone() + .filter(|p| !AutosaveState::is_recovery_path(p)); + if let Some(path) = real_path { // Save to existing path - self.save_to_file(path.clone()); + self.save_to_file(path); } else { - // No current path, fall through to Save As + // No real path (untitled or recovered): fall through to Save As if let Some(path) = FileDialog::new() .add_filter("Lightningbeam Project", &["beam"]) .set_file_name("Untitled.beam") @@ -3312,15 +3390,16 @@ impl EditorApp { .add_filter("Lightningbeam Project", &["beam"]) .set_file_name("Untitled.beam"); - // Set initial directory if we have a current file - let dialog = if let Some(current_path) = &self.current_file_path { - if let Some(parent) = current_path.parent() { - dialog.set_directory(parent) - } else { - dialog - } - } else { - dialog + // Default to the current file's directory — but not the recovery dir (a recovered + // file's parent), which the user never chose and shouldn't be steered back into. + let dialog = match self + .current_file_path + .as_ref() + .filter(|p| !AutosaveState::is_recovery_path(p)) + .and_then(|p| p.parent()) + { + Some(parent) => dialog.set_directory(parent), + None => dialog, }; if let Some(path) = dialog.save_file() { @@ -4210,6 +4289,78 @@ impl EditorApp { eprintln!("💾 [AUTOSAVE] recovery snapshot dispatched"); } + /// Show the crash-recovery prompt when a previous session left a recovery file behind. Recover + /// loads it as an untitled document; Discard deletes it; Later keeps it for the next launch. + fn render_recovery_prompt(&mut self, ctx: &egui::Context) { + // Don't prompt over an in-flight file op (including a recovery load we just started). + if self.autosave.leftover_recoveries.is_empty() || self.file_operation.is_some() { + return; + } + let path = self.autosave.leftover_recoveries[0].clone(); + + #[derive(PartialEq)] + enum Choice { Recover, Discard, Later } + let mut choice: Option = None; + + egui::Modal::new(egui::Id::new("crash_recovery_modal")).show(ctx, |ui| { + ui.set_width(crate::mobile::dialog_width(ctx, 460.0)); + ui.heading("Recover unsaved work?"); + ui.add_space(6.0); + ui.label( + "Lightningbeam didn't shut down cleanly last time. You have unsaved work from your \ + previous session — recover it?", + ); + if self.autosave.leftover_recoveries.len() > 1 { + ui.add_space(4.0); + ui.weak(format!( + "{} snapshots available; this shows the most recent first.", + self.autosave.leftover_recoveries.len() + )); + } + ui.add_space(14.0); + ui.horizontal(|ui| { + if ui.button("Recover").clicked() { + choice = Some(Choice::Recover); + } + if ui.button("Discard").clicked() { + choice = Some(Choice::Discard); + } + if ui.button("Later").clicked() { + choice = Some(Choice::Later); + } + }); + }); + + match choice { + Some(Choice::Recover) => { + self.autosave.leftover_recoveries.remove(0); + // Rename out of the `session-*` namespace before opening it, so it isn't offered + // again next launch — but keep the file, since recovered raster keyframes page in + // from it on demand (deleting it would lose paged pixels). It opens as the current + // file; the user relocates the work with Save As. Old `recovered-*` files are + // garbage-collected at startup. + let open_path = match path.file_name().and_then(|n| n.to_str()) { + Some(name) => { + let renamed = + path.with_file_name(name.replacen("session-", "recovered-", 1)); + if std::fs::rename(&path, &renamed).is_ok() { renamed } else { path } + } + None => path, + }; + self.load_from_file(open_path); + } + Some(Choice::Discard) => { + let _ = std::fs::remove_file(&path); + self.autosave.leftover_recoveries.remove(0); + } + Some(Choice::Later) => { + // Keep the files on disk but stop prompting this session. + self.autosave.leftover_recoveries.clear(); + } + None => {} + } + } + /// Load a document from a .beam file fn load_from_file(&mut self, path: std::path::PathBuf) { println!("Loading from: {}", path.display()); @@ -5597,6 +5748,8 @@ impl eframe::App for EditorApp { // Background crash-recovery autosave (cheap early-out unless dirty + interval elapsed). self.maybe_autosave(ctx); + // Offer to restore a previous session's unsaved work (if a recovery file was left behind). + self.render_recovery_prompt(ctx); // === Raster fault-in (Phase 3 paging) === // The canvas records raster keyframe ids whose `raw_pixels` weren't resident From 5f09222f3fac6a50e5bbfe61740abb1e3bfbe0f6 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 17:29:38 -0400 Subject: [PATCH 39/43] Autosave: configurable interval + startup logging for testing - LB_AUTOSAVE_SECS overrides the 45s autosave interval (e.g. =5) so the recovery flow can be exercised without a long wait. - Log the session's recovery file path at startup so it's easy to find. --- lightningbeam-ui/lightningbeam-editor/src/main.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 1b2116e..cf06d46 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -703,12 +703,16 @@ struct AutosaveState { /// Recovery files left over from previous sessions that didn't exit cleanly (newest first), /// discovered at startup. Presented to the user as a "recover unsaved work?" prompt. leftover_recoveries: Vec, + /// Seconds between autosaves while dirty. Defaults to `AUTOSAVE_INTERVAL_SECS`; override with + /// `LB_AUTOSAVE_SECS` (e.g. `LB_AUTOSAVE_SECS=5`) to make manual testing practical. + interval_secs: f64, } impl AutosaveState { fn new() -> Self { Self::gc_old_recovered(); let recovery_path = Self::make_session_path(); + eprintln!("💾 [AUTOSAVE] recovery file for this session: {:?}", recovery_path); let leftover_recoveries = Self::find_leftovers(recovery_path.as_deref()); if !leftover_recoveries.is_empty() { eprintln!("💾 [AUTOSAVE] found {} leftover recovery file(s) from a previous session", @@ -722,6 +726,11 @@ impl AutosaveState { last_time: None, progress_rx: None, leftover_recoveries, + interval_secs: std::env::var("LB_AUTOSAVE_SECS") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|s| *s > 0.0) + .unwrap_or(AUTOSAVE_INTERVAL_SECS), } } @@ -4266,11 +4275,11 @@ impl EditorApp { } if let Some(t) = self.autosave.last_time { let elapsed = t.elapsed().as_secs_f64(); - if elapsed < AUTOSAVE_INTERVAL_SECS { + if elapsed < self.autosave.interval_secs { // Dirty but throttled — wake up when the interval elapses even if the app goes idle, // so an edit-then-idle session still gets its recovery snapshot. ctx.request_repaint_after(std::time::Duration::from_secs_f64( - (AUTOSAVE_INTERVAL_SECS - elapsed).max(0.1), + (self.autosave.interval_secs - elapsed).max(0.1), )); return; } From ef2b0822bd1bee2a199c4cb270c3be9ceb7f7c00 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 17:43:35 -0400 Subject: [PATCH 40/43] Autosave: keep recovery files out of Recent Files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recovering a file loaded it through the normal load path, which added it to Recent Files — so an internal recovered-*.beam in the app data dir showed up in the Recent list (and could even be auto-reopened as "last session"). Skip add_recent_file for recovery paths on load, and clean any that older builds already leaked into Recent at startup (before the auto-reopen check reads it). --- .../lightningbeam-editor/src/main.rs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index cf06d46..f3cb65b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -1379,7 +1379,17 @@ impl EditorApp { cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false); // Load application config - let config = AppConfig::load(); + let mut config = AppConfig::load(); + // One-time cleanup: earlier builds added a Recovered file to Recent on restore. Drop any + // recovery-dir paths that leaked in (and re-save if we removed any) so they don't show in + // Recent or get auto-reopened below. + let recent_before = config.recent_files.len(); + config + .recent_files + .retain(|p| !AutosaveState::is_recovery_path(p)); + if config.recent_files.len() != recent_before { + config.save(); + } // Check if we should auto-reopen last session let pending_auto_reopen = if config.reopen_last_session { @@ -4746,9 +4756,12 @@ impl EditorApp { // Point the raster paging store at the loaded container so faulting works. self.raster_store.set_path(self.current_file_path.clone()); - // Add to recent files - self.config.add_recent_file(path.clone()); - self.update_recent_files_menu(); + // Add to recent files — but never a recovery file (it's an internal, transient container in + // the app data dir, not a project the user opened). + if !AutosaveState::is_recovery_path(&path) { + self.config.add_recent_file(path.clone()); + self.update_recent_files_menu(); + } // Set active layer if let Some(first) = self.action_executor.document().root.children.first() { From 6aece26a8b5bd2e5269e42a074d77bcfac1d18cf Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 18:06:07 -0400 Subject: [PATCH 41/43] Prompt to save unsaved changes before switching files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New File / Open / Open Recent previously discarded unsaved work with no warning (three // TODO: Prompt to save markers). They now route through request_switch, which shows a Save / Don't Save / Cancel modal when the document is modified: • Save — saves first (Save As for untitled/recovered docs), then switches once the background save completes (switch_after_save). • Don't Save — switches and discards. • Cancel — stays put. "Modified" is tracked without hooking every edit site: current epoch vs a saved_epoch baseline (rebased on manual save / new / load), plus a media_modified flag for imports/recordings that don't bump the action epoch. Recovered documents count as modified until given a real home, so switching away from unrecovered work also prompts. --- .../lightningbeam-editor/src/main.rs | 199 ++++++++++++++++-- 1 file changed, 178 insertions(+), 21 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index f3cb65b..54c66ea 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -809,6 +809,16 @@ impl AutosaveState { } } +/// A file switch deferred behind the unsaved-changes prompt: the thing to do once the user answers +/// Save / Don't Save (Cancel just drops it). +#[derive(Debug, Clone)] +enum PendingSwitch { + /// New File (return to the start screen). + NewFile, + /// Open a specific `.beam` (covers both Open… and Open Recent — the path is already resolved). + Open(std::path::PathBuf), +} + /// Information about an imported asset (for auto-placement) #[derive(Debug, Clone)] #[allow(dead_code)] // name/duration populated for future import UX features @@ -1296,6 +1306,18 @@ struct EditorApp { /// exit. A leftover file on next launch signals an unclean shutdown → offer to restore. autosave: AutosaveState, + /// `ActionExecutor::epoch()` at the last manual save (or new/load) — the document is "modified" + /// (has unsaved changes vs. the user's file) when the current epoch differs, or `media_modified` + /// is set. Distinct from the autosave baseline, which also moves on every background autosave. + saved_epoch: u64, + /// Non-action changes since the last save (imports, finished recordings) that `epoch` doesn't + /// capture. Cleared on manual save / new / load. + media_modified: bool, + /// A file switch waiting on the "save changes?" prompt (New/Open/Open Recent with unsaved work). + unsaved_prompt: Option, + /// A switch to perform once an in-flight save (triggered from that prompt) finishes. + switch_after_save: Option, + /// Audio extraction channel for background thread communication audio_extraction_tx: std::sync::mpsc::Sender, audio_extraction_rx: std::sync::mpsc::Receiver, @@ -1594,6 +1616,10 @@ impl EditorApp { file_command_tx, file_operation: None, // No file operation in progress initially autosave: AutosaveState::new(), + saved_epoch: 0, + media_modified: false, + unsaved_prompt: None, + switch_after_save: None, audio_extraction_tx, audio_extraction_rx, export_dialog: export::dialog::ExportDialog::default(), @@ -3358,22 +3384,7 @@ impl EditorApp { // File menu MenuAction::NewFile => { println!("Menu: New File"); - // TODO: Prompt to save current file if modified - - // Tear down the backend (stops old instruments/voices immediately) and clear the - // app-side track maps + backend-derived caches. - self.reset_audio_backend(); - - // Reset UI state and return to start screen - self.current_file_path = None; - self.selection.clear(); - self.editing_context = EditingContext::default(); - self.active_layer_id = None; - self.playback_time = 0.0; - self.is_playing = false; - self.pane_instances.clear(); - self.project_generation += 1; - self.app_mode = AppMode::StartScreen; + self.request_switch(PendingSwitch::NewFile); } MenuAction::NewWindow => { println!("Menu: New Window"); @@ -3428,21 +3439,19 @@ impl EditorApp { MenuAction::OpenFile => { use rfd::FileDialog; - // TODO: Prompt to save current file if modified - + // Pick the file first, then (if there are unsaved changes) prompt to save. if let Some(path) = FileDialog::new() .add_filter("Lightningbeam Project", &["beam"]) .pick_file() { - self.load_from_file(path); + self.request_switch(PendingSwitch::Open(path)); } } MenuAction::OpenRecent(index) => { let recent_files = self.config.get_recent_files(); if let Some(path) = recent_files.get(index) { - // TODO: Prompt to save current file if modified - self.load_from_file(path.clone()); + self.request_switch(PendingSwitch::Open(path.clone())); } } MenuAction::ClearRecentFiles => { @@ -4243,6 +4252,21 @@ impl EditorApp { self.autosave.baseline_epoch = self.action_executor.epoch(); self.autosave.pending_event = false; self.autosave.last_time = None; + // A fresh/loaded document also starts unmodified vs. its on-disk form. + self.saved_epoch = self.action_executor.epoch(); + self.media_modified = false; + } + + /// Whether the document has unsaved changes vs. the user's file (edits since the last manual + /// save, or an import/recording that `epoch` doesn't track). Recovered work counts as unsaved + /// until the user gives it a real home — its only copy is the transient recovery container. + fn document_modified(&self) -> bool { + self.action_executor.epoch() != self.saved_epoch + || self.media_modified + || self + .current_file_path + .as_ref() + .is_some_and(|p| AutosaveState::is_recovery_path(p)) } /// Background crash-recovery autosave: poll any in-flight recovery write, then (if the document @@ -4380,6 +4404,120 @@ impl EditorApp { } } + /// Show the "save changes?" prompt when a file switch is requested with unsaved work. Save + /// persists first (Save As for untitled/recovered files) then switches; Don't Save switches and + /// discards; Cancel stays put. + fn render_unsaved_prompt(&mut self, ctx: &egui::Context) { + let Some(pending) = self.unsaved_prompt.as_ref() else { return }; + let action_desc = match pending { + PendingSwitch::NewFile => "starting a new file", + PendingSwitch::Open(_) => "opening another file", + }; + + #[derive(PartialEq)] + enum Answer { + Save, + DontSave, + Cancel, + } + let mut answer: Option = None; + + egui::Modal::new(egui::Id::new("unsaved_changes_modal")).show(ctx, |ui| { + ui.set_width(crate::mobile::dialog_width(ctx, 440.0)); + ui.heading("Save changes?"); + ui.add_space(6.0); + ui.label(format!( + "This document has unsaved changes. Save them before {action_desc}?" + )); + ui.add_space(14.0); + ui.horizontal(|ui| { + if ui.button("Save").clicked() { + answer = Some(Answer::Save); + } + if ui.button("Don't Save").clicked() { + answer = Some(Answer::DontSave); + } + if ui.button("Cancel").clicked() { + answer = Some(Answer::Cancel); + } + }); + }); + + match answer { + Some(Answer::Cancel) => { + self.unsaved_prompt = None; + } + Some(Answer::DontSave) => { + if let Some(sw) = self.unsaved_prompt.take() { + self.do_switch(sw); + } + } + Some(Answer::Save) => { + let sw = self.unsaved_prompt.take(); + // Save to the existing file, or Save As for an untitled / recovered document. + let real_path = self + .current_file_path + .clone() + .filter(|p| !AutosaveState::is_recovery_path(p)); + let target = match real_path { + Some(p) => Some(p), + None => rfd::FileDialog::new() + .add_filter("Lightningbeam Project", &["beam"]) + .set_file_name("Untitled.beam") + .save_file(), + }; + match target { + Some(path) => { + // Run the switch once the save completes. + self.switch_after_save = sw; + self.save_to_file(path); + } + None => { + // Save As cancelled → abort the switch, keep the document (still unsaved). + } + } + } + None => {} + } + } + + /// New File: tear down the current project and return to the start screen. (The guard for + /// unsaved changes lives in the menu handler; this is the actual action.) + fn do_new_file(&mut self) { + // Tear down the backend (stops old instruments/voices immediately) and clear the app-side + // track maps + backend-derived caches. + self.reset_audio_backend(); + + // Reset UI state and return to the start screen. + self.current_file_path = None; + self.selection.clear(); + self.editing_context = EditingContext::default(); + self.active_layer_id = None; + self.playback_time = 0.0; + self.is_playing = false; + self.pane_instances.clear(); + self.project_generation += 1; + self.app_mode = AppMode::StartScreen; + } + + /// Carry out a file switch once the unsaved-changes prompt is resolved (or when there were no + /// unsaved changes to begin with). + fn do_switch(&mut self, switch: PendingSwitch) { + match switch { + PendingSwitch::NewFile => self.do_new_file(), + PendingSwitch::Open(path) => self.load_from_file(path), + } + } + + /// Begin a file switch, prompting to save first if the document has unsaved changes. + fn request_switch(&mut self, switch: PendingSwitch) { + if self.document_modified() { + self.unsaved_prompt = Some(switch); + } else { + self.do_switch(switch); + } + } + /// Load a document from a .beam file fn load_from_file(&mut self, path: std::path::PathBuf) { println!("Loading from: {}", path.display()); @@ -5019,6 +5157,7 @@ impl EditorApp { use lightningbeam_core::clip::ImageAsset; // Imported media lives outside the action/undo system, so flag it for the next autosave. self.autosave.pending_event = true; + self.media_modified = true; self.note_possible_large_media(path); // Get filename for asset name @@ -5073,6 +5212,7 @@ impl EditorApp { fn import_audio(&mut self, path: &std::path::Path) -> Option { use lightningbeam_core::clip::AudioClip; self.autosave.pending_event = true; + self.media_modified = true; self.note_possible_large_media(path); let name = path.file_stem() @@ -5200,6 +5340,7 @@ impl EditorApp { use lightningbeam_core::clip::VideoClip; use lightningbeam_core::video::probe_video; self.autosave.pending_event = true; + self.media_modified = true; self.note_possible_large_media(path); let name = path.file_stem() @@ -5772,6 +5913,8 @@ impl eframe::App for EditorApp { self.maybe_autosave(ctx); // Offer to restore a previous session's unsaved work (if a recovery file was left behind). self.render_recovery_prompt(ctx); + // Prompt to save unsaved changes before switching files (New / Open / Open Recent). + self.render_unsaved_prompt(ctx); // === Raster fault-in (Phase 3 paging) === // The canvas records raster keyframe ids whose `raw_pixels` weren't resident @@ -6012,6 +6155,9 @@ impl eframe::App for EditorApp { let mut operation_complete = false; let mut loaded_project_data: Option<(lightningbeam_core::file_io::LoadedProject, std::path::PathBuf)> = None; let mut update_recent_menu = false; // Track if we need to update recent files menu + // A file switch that was waiting on this save (from the unsaved-changes prompt), to run + // after the file_operation borrow ends. + let mut switch_after_save: Option = None; match operation { FileOperation::Saving { ref mut progress_rx, ref path } => { @@ -6027,6 +6173,11 @@ impl eframe::App for EditorApp { self.autosave.baseline_epoch = self.action_executor.epoch(); self.autosave.pending_event = false; self.autosave.last_time = None; + // The document now matches its file → no unsaved changes. + self.saved_epoch = self.action_executor.epoch(); + self.media_modified = false; + // If a file switch was waiting on this save, run it after the borrow. + switch_after_save = self.switch_after_save.take(); // Container path may be new (Save As); update the // raster paging store so future faults read the right file. self.raster_store.set_path(self.current_file_path.clone()); @@ -6144,6 +6295,11 @@ impl eframe::App for EditorApp { self.update_recent_files_menu(); } + // A file switch that was waiting on this save (New/Open after choosing "Save") now runs. + if let Some(switch) = switch_after_save { + self.do_switch(switch); + } + // Request repaint to keep updating progress ctx.request_repaint(); } @@ -6359,6 +6515,7 @@ impl eframe::App for EditorApp { // Finalize the clip (update pool_index and duration) // A finished recording (samples in the pool) needs capturing. self.autosave.pending_event = true; + self.media_modified = true; if let Some(clip) = self.action_executor.document_mut().audio_clips.get_mut(&clip_id) { if clip.finalize_recording(pool_index, duration) { clip.name = format!("Recording {}", pool_index); From 8fbb6d65c0c435c5ba5bdc8bcf552604ce940297 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 18:17:37 -0400 Subject: [PATCH 42/43] Prompt to save on quit too; unify with the switch prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closing the window with unsaved changes was a silent data-loss path — and worse, a clean exit deletes the recovery file, removing the safety net too. Fold quitting into the same unsaved-changes flow as file switches: one PendingAction enum (NewFile / Open / Quit), one modal, one do_action, one after_save handler. render_unsaved_prompt now also intercepts the window-close request (CancelClose + queue a Quit prompt); Save & Quit saves then closes, Discard & Quit closes now, Cancel keeps the window open. confirmed_close lets the final programmatic close through instead of re-intercepting it. Replaces the near-duplicate close-prompt/switch-prompt code with a single path. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lightningbeam-editor/src/main.rs | 115 +++++++++++------- 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 54c66ea..6a9caaa 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -809,14 +809,17 @@ impl AutosaveState { } } -/// A file switch deferred behind the unsaved-changes prompt: the thing to do once the user answers -/// Save / Don't Save (Cancel just drops it). +/// Something to do after the unsaved-changes prompt resolves: the action deferred behind "Save +/// changes?" (Save runs it after saving; Don't Save runs it immediately; Cancel drops it). Covers +/// both file switches and quitting so they share one prompt + one save-then-continue path. #[derive(Debug, Clone)] -enum PendingSwitch { +enum PendingAction { /// New File (return to the start screen). NewFile, /// Open a specific `.beam` (covers both Open… and Open Recent — the path is already resolved). Open(std::path::PathBuf), + /// Quit (close the window). + Quit, } /// Information about an imported asset (for auto-placement) @@ -1313,10 +1316,13 @@ struct EditorApp { /// Non-action changes since the last save (imports, finished recordings) that `epoch` doesn't /// capture. Cleared on manual save / new / load. media_modified: bool, - /// A file switch waiting on the "save changes?" prompt (New/Open/Open Recent with unsaved work). - unsaved_prompt: Option, - /// A switch to perform once an in-flight save (triggered from that prompt) finishes. - switch_after_save: Option, + /// The pending action waiting on the "save changes?" prompt — a file switch (New/Open/Open + /// Recent) or a quit, requested while the document had unsaved work. + unsaved_prompt: Option, + /// The action to run once an in-flight save (triggered from that prompt via "Save") finishes. + after_save: Option, + /// The user agreed to quit — let the next window-close request through instead of intercepting it. + confirmed_close: bool, /// Audio extraction channel for background thread communication audio_extraction_tx: std::sync::mpsc::Sender, @@ -1619,7 +1625,8 @@ impl EditorApp { saved_epoch: 0, media_modified: false, unsaved_prompt: None, - switch_after_save: None, + after_save: None, + confirmed_close: false, audio_extraction_tx, audio_extraction_rx, export_dialog: export::dialog::ExportDialog::default(), @@ -3384,7 +3391,7 @@ impl EditorApp { // File menu MenuAction::NewFile => { println!("Menu: New File"); - self.request_switch(PendingSwitch::NewFile); + self.request_switch(PendingAction::NewFile); } MenuAction::NewWindow => { println!("Menu: New Window"); @@ -3444,14 +3451,14 @@ impl EditorApp { .add_filter("Lightningbeam Project", &["beam"]) .pick_file() { - self.request_switch(PendingSwitch::Open(path)); + self.request_switch(PendingAction::Open(path)); } } MenuAction::OpenRecent(index) => { let recent_files = self.config.get_recent_files(); if let Some(path) = recent_files.get(index) { - self.request_switch(PendingSwitch::Open(path.clone())); + self.request_switch(PendingAction::Open(path.clone())); } } MenuAction::ClearRecentFiles => { @@ -4404,20 +4411,32 @@ impl EditorApp { } } - /// Show the "save changes?" prompt when a file switch is requested with unsaved work. Save - /// persists first (Save As for untitled/recovered files) then switches; Don't Save switches and + /// The single "save changes?" prompt for any unsaved-work exit point — file switches (New / + /// Open / Open Recent) and quitting. Also intercepts the window-close request. Save persists + /// first (Save As for untitled/recovered docs) then runs the action; Don't Save runs it and /// discards; Cancel stays put. fn render_unsaved_prompt(&mut self, ctx: &egui::Context) { + // Intercept a window-close request with unsaved work → veto it and queue the Quit prompt. + if ctx.input(|i| i.viewport().close_requested()) && !self.confirmed_close { + if self.document_modified() { + ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose); + self.unsaved_prompt = Some(PendingAction::Quit); + } + // Unmodified → let the close proceed. + } + let Some(pending) = self.unsaved_prompt.as_ref() else { return }; - let action_desc = match pending { - PendingSwitch::NewFile => "starting a new file", - PendingSwitch::Open(_) => "opening another file", + // The "…before X?" tail + the affirmative button label, per action. + let (desc, discard_label) = match pending { + PendingAction::NewFile => ("starting a new file", "Don't Save"), + PendingAction::Open(_) => ("opening another file", "Don't Save"), + PendingAction::Quit => ("quitting", "Discard & Quit"), }; #[derive(PartialEq)] enum Answer { Save, - DontSave, + Discard, Cancel, } let mut answer: Option = None; @@ -4427,15 +4446,15 @@ impl EditorApp { ui.heading("Save changes?"); ui.add_space(6.0); ui.label(format!( - "This document has unsaved changes. Save them before {action_desc}?" + "This document has unsaved changes. Save them before {desc}?" )); ui.add_space(14.0); ui.horizontal(|ui| { if ui.button("Save").clicked() { answer = Some(Answer::Save); } - if ui.button("Don't Save").clicked() { - answer = Some(Answer::DontSave); + if ui.button(discard_label).clicked() { + answer = Some(Answer::Discard); } if ui.button("Cancel").clicked() { answer = Some(Answer::Cancel); @@ -4447,13 +4466,13 @@ impl EditorApp { Some(Answer::Cancel) => { self.unsaved_prompt = None; } - Some(Answer::DontSave) => { - if let Some(sw) = self.unsaved_prompt.take() { - self.do_switch(sw); + Some(Answer::Discard) => { + if let Some(action) = self.unsaved_prompt.take() { + self.do_action(action, ctx); } } Some(Answer::Save) => { - let sw = self.unsaved_prompt.take(); + let action = self.unsaved_prompt.take(); // Save to the existing file, or Save As for an untitled / recovered document. let real_path = self .current_file_path @@ -4468,12 +4487,12 @@ impl EditorApp { }; match target { Some(path) => { - // Run the switch once the save completes. - self.switch_after_save = sw; + // Run the action once the save completes. + self.after_save = action; self.save_to_file(path); } None => { - // Save As cancelled → abort the switch, keep the document (still unsaved). + // Save As cancelled → abort, keep the document (still unsaved). } } } @@ -4500,21 +4519,31 @@ impl EditorApp { self.app_mode = AppMode::StartScreen; } - /// Carry out a file switch once the unsaved-changes prompt is resolved (or when there were no - /// unsaved changes to begin with). - fn do_switch(&mut self, switch: PendingSwitch) { - match switch { - PendingSwitch::NewFile => self.do_new_file(), - PendingSwitch::Open(path) => self.load_from_file(path), + /// Carry out a deferred action once the unsaved-changes prompt is resolved (or when there was + /// nothing unsaved to begin with). + fn do_action(&mut self, action: PendingAction, ctx: &egui::Context) { + match action { + PendingAction::NewFile => self.do_new_file(), + PendingAction::Open(path) => self.load_from_file(path), + PendingAction::Quit => { + self.confirmed_close = true; + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } } } - /// Begin a file switch, prompting to save first if the document has unsaved changes. - fn request_switch(&mut self, switch: PendingSwitch) { + /// Begin a file switch (New / Open / Open Recent), prompting to save first if the document has + /// unsaved changes. Only ever called with `NewFile`/`Open` from the menus, so the immediate path + /// needs no `ctx` (only `Quit`, driven by the close interceptor, does). + fn request_switch(&mut self, action: PendingAction) { if self.document_modified() { - self.unsaved_prompt = Some(switch); + self.unsaved_prompt = Some(action); } else { - self.do_switch(switch); + match action { + PendingAction::NewFile => self.do_new_file(), + PendingAction::Open(path) => self.load_from_file(path), + PendingAction::Quit => {} + } } } @@ -6155,9 +6184,9 @@ impl eframe::App for EditorApp { let mut operation_complete = false; let mut loaded_project_data: Option<(lightningbeam_core::file_io::LoadedProject, std::path::PathBuf)> = None; let mut update_recent_menu = false; // Track if we need to update recent files menu - // A file switch that was waiting on this save (from the unsaved-changes prompt), to run + // An action that was waiting on this save (from the unsaved-changes prompt), to run // after the file_operation borrow ends. - let mut switch_after_save: Option = None; + let mut after_save: Option = None; match operation { FileOperation::Saving { ref mut progress_rx, ref path } => { @@ -6177,7 +6206,7 @@ impl eframe::App for EditorApp { self.saved_epoch = self.action_executor.epoch(); self.media_modified = false; // If a file switch was waiting on this save, run it after the borrow. - switch_after_save = self.switch_after_save.take(); + after_save = self.after_save.take(); // Container path may be new (Save As); update the // raster paging store so future faults read the right file. self.raster_store.set_path(self.current_file_path.clone()); @@ -6295,9 +6324,9 @@ impl eframe::App for EditorApp { self.update_recent_files_menu(); } - // A file switch that was waiting on this save (New/Open after choosing "Save") now runs. - if let Some(switch) = switch_after_save { - self.do_switch(switch); + // An action that was waiting on this save ("Save" in the prompt → New/Open/Quit) runs now. + if let Some(action) = after_save { + self.do_action(action, ctx); } // Request repaint to keep updating progress From b6f43d2e72b5c869362fa0a2e93d5ff0d8f7a8af Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 9 Jul 2026 18:26:37 -0400 Subject: [PATCH 43/43] Bump version to 1.0.8-alpha --- Changelog.md | 20 +++++++++++++++++++ .../lightningbeam-editor/Cargo.toml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 9d4eaf4..d331668 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,23 @@ +# 1.0.8-alpha: +Changes: +- Mobile/touch UI (experimental, testing only — not built or packaged for mobile yet; enabled on desktop with the LB_MOBILE_UI environment variable): work-in-progress phone-friendly interface with a vertical sliding-window pane stack you drag to reveal panes, a new-file intent picker, a selection inspector sheet, a keyboard-primary music surface, a Focus/Patch node editor, long-press context menus, a command palette, and landscape/orientation support +- Text layers: add and edit text with a chosen font; non-bundled fonts are embedded in the project so it renders on machines that lack them +- Animated GIF export (parallel palette encoding) +- Audio tag metadata (title, artist, album, genre, year, track, comment) is written into exports — ID3v2 for MP3, iTunes/MP4 atoms for AAC, Vorbis comments for FLAC, RIFF INFO for WAV — with sensible defaults (year, artist/album remembered between exports) +- Lossy WebP image export, so the quality control now actually applies +- SVG export now includes text layers, as real font-independent glyph outlines +- Crash recovery: the editor autosaves your work to a recovery file in the background and offers to restore it after an unclean shutdown +- Prompt to save unsaved changes before starting a new file, opening another, or quitting +- Faster saves on painting projects: unchanged raster frames are no longer re-encoded every save + +Bugfixes: +- FLAC export previously wrote a WAV file with a .flac extension; it now encodes real (compressed) FLAC +- ProRes 422 export always failed; it now encodes 10-bit 4:2:2 correctly +- Fix VP8 video export with audio (muxed into WebM instead of an incompatible container) +- The WebP "Quality" slider had no effect +- Starting a new file now fully resets the audio engine, so instruments and voices from the previous project no longer linger +- Fix oscillator/synth phase drift over long playback + # 1.0.7-alpha: Changes: - HDR video support: PQ/HLG/BT.2020 video is now read correctly (decoded to scene-linear), with a per-document output mode (clip vs highlight rolloff) and 10-bit HDR export (HEVC Main10, PQ or HLG) diff --git a/lightningbeam-ui/lightningbeam-editor/Cargo.toml b/lightningbeam-ui/lightningbeam-editor/Cargo.toml index 77019b4..2ec49a3 100644 --- a/lightningbeam-ui/lightningbeam-editor/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-editor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightningbeam-editor" -version = "1.0.7-alpha" +version = "1.0.8-alpha" edition = "2021" description = "Multimedia editor for audio, video and 2D animation" license = "GPL-3.0-or-later"

RL&LHE$ zK%pty!S3-oB)(#Q`E{{Fd@Xi?ACH~h_r-4R?bspxMC=TA5%z|=+PMomgum;2f_>0K z?1H|YJHpi0#jfW*%5~Lb_yK-gY~#@e^k29B;45%-QO~s>S!d)r3)fj>W9trC_k{H- z);r0@*86Dv@#~+v{+l+oLDdG=Y`E2iSK9N3tz-S+_gR0Vg(g?H>E@fBzUkYWjo<8& z&A!-t@6E3^sRK8EbBkeHOxWV^Ew0?+$&r;Kw;$Oy^8AsDMt-s7DD%76a{Dc3+H$r$ z+4`ek_+7VS+s~|@)aaPw)_g>N3HlE4F&p5{8R7f5b0%YF{u=DdKEd4+UAwP)Fx+X_ zQ{BNX>T|JU`Z4Ya2%qep4);ZSO*dOkpVmjP50gFo5vHM84$6{CR zQ?LW~`3PSkE{A)SxDM`3*gg9$?2i2)!jFlk;l3zdhx@Mh2<{gi?rQ8|JqYelZv(g^ zyv^Z`@?yBP*m1f6yGu_(cyI4OxKq6rxLCgncOh(hI@kmHB<%Nmws#@ImwH#gz1q7T z?#0N9h0Oz3 zbQ@Xo`!EuSr_5RV_PvSlVN(BkyeaLcrsPXa9S$?~X*KC{SvM_ljzp=VrSCPkQ)C_7 z?fO24+aV8eTyM~llMp^q?hf~8eFusC<-u^9U{?>Wk=vx7Ownm%P3?OfVeGOCx25kP zxE-eE)A}$niNlTDgH1ULWGzw-HT6Hj$X%e*{X{>7;$VLg+;-EV;S%=U#8!Q;!ad#J z8LqAEE%Hu;PwRUI?yq4p0(J0r)vl@A6Y>u9^Wwfwk#deH?cyc6%&q0^a2rjXPm=c{ ze5UE2=j9@V{Uv+Boov$2(tYKAp~GHa`mM>IfL~{uaHFsLWHxMF;93~zM;htvrUz#D z6Oq2xlD*+JnzPPZvIoM4`FJkf@0#A|u_fsI?h~*9ivH<)3$kv59SpS5*ZtFD?wJMt zb_h3`tKaI6K^XTg+*!Ib0qbON=a`buS)$8X;P0r@wG?l-{0YJf`|i=Mudn8IeOIEV zec0L90cSSA`o#vYWpM)RYFv)L5`GJ@Km9#;nih$_IT7}sf4~`lUFM&2D)lb&&Oq!i z|C%!x`^qnNhF}-?O<>VuL)5JuM-PsLIF7<`91h5HPQ{_E>0OB9QXE&{xEfNf$8j?b zJXf%hAz&Rtz!rvpl?{PC@dd_f7gjA?*xL{orCkTDaeYU)6+qZc<&}=RH0z|KPmn+`)^zhMMyHH?B?hFaKQXn?(iNwBSOpl+c&6Yfys-fdi)VqAN+X3+LV-Fs8UyiTmL$ zG3l3x(^1-0?qABTvnKYhuf(49YhbT>vqL@lW9fdkdbeBbbqh(Z{{dXKOV0_; z5Kk+t+SI~&O@q5DY}4!w`!iEvYo;9*Wfr!pqID=5jYI zw>%6hEziOt%bT#i@(F6_!>Y;>@K5SE54m%Xr!RExRurIL) zwj>^d-G~=qBjR1ygZKirA41r97z VfN_6l^ur!Y)GtY%olMy@dl|TcHJZ6nbDY z;V9TgI0?28&W7ECOJU>SYS=SC@8M3$wW~0?yNh+$U2MXOOn8wFi(w|uFcaRxg!eGv z!nwtAL@`e>T*&V}Ys!Dt$STMc`4mzj5H8k-;TDs>SpQ<*nrr6{T3QG9(iO_T-}JM|!!l3NdE7@#x(P4ScGDKG(v%aL zJUbQ2Jj#UaHMRHBiecOT#cOTvNn5vKUu|yskoTUc73*oj=6Mn34s`AFV(uJa-M7uT zwho0bua|A3y~lKT_0eFBeoG_T&d5W?zQ|*+D59;2yo@pNbIdBNfpId&p>4qJiDO?J z2jiHAV3Mh&B2cA4tS3X=Hr0-^p6pyCA`1?M`6vb%FoFl6NWU7 zuoas1yJ$Dxm-C#_p$!q%N`!p8M>*J0~won80*3a-^oR!m<-ihHRpf$m}xwr(MP zB~najU?5BD&|t#W)%U3jo}gWQFVb~y&|&L}?M%9Hy<1KC5|dIWN6!-A+&1lc$C^A^ zUn!St%U?yiR#j)#%AOUP(yUwP|5f#~yBhuDni^VnnL6BWWUaRAVs50r-wNSke~Q9= zQk3iMXwJ327W;pYPIs+4%7l%pXLumXxbAu;-MW{W@MPmID!4Y?xXYYt>dBfH%PjO8 z``=!>g1gMMPK_*c7C5J_n{`h&Ve6VJXxp+(KNRX&l(o!#UZ_9QCmV@N+q?SX_y(9S z{u^vW741Sjw-QTG@52Vv7yc4!$pBja>WttHM{sN7{u-Qf1CCp8+=1g>9J*f~!SN)H z=P}~H3jb{!y6-+i$LYRX_6ZqkQdV?}Ig7vA^kRw)ub6*TDRH3=%a-Gzg2Qki6=R9k z3WmTH{zt$*%r-BhS80vvoA`CmSK%gRs0B)d{(>@5pJht?x^q`9@oVa6^gYLDdalv* zHu_K3aU~LU9d|Wtb}{#^TfGL{_5X!CvwUB!yboNbVr6BaxBt^}2e0BuD%baawFWEc zS=af0z6Pu6dx2TQ;+?)GuEtkC9bf%y>Ap;H=%-QNO%CQ6zOKYo-7oeT)=%UAppGlK z9{*+?S5i{BFLWJuh0WikBgR??|2xml*WW8E?=4#c%xdv3wqzzRX!pAA~LUv;IqF1%qbkn!{`W-KSEw%G=Yp7nl~>HyjuKci+GM z2ByZxni|{r9uvkqx4sAM48r3kk4Y(}dm~J`bvsv{N0|8-d)7+a>&;mOiQa)ag|)K3 zrY29(wKXw)U93OLFV@QDG-Z0GG|YVWmtPFqa*D3Esy?x^R<^HfFI{MASd?36(JFhO zkY~kKi{iDgIc;wg-K7#&|Na%Qllv&})Nv|sfXlmf=J0Uc2ELX1FTZC!twFioc;k-H zuK2`+FEQaYwdIe%`E>tklX9$f#fv7q(1eY4 zyi7S~n_nweaBZEfToW$VnI()cXW9A~jnK}h6kD-O*p^vz$D0yNImPmYo`H0253n~( z%Bp&!*aOA3+xl3YeVJC9I$J5cF1Dq%$8I*#jcX+u_g}eZAMjsX=AOON8HsEBXWYY@ zpA5r0wx!HWb7KLU&@aOOI?cd!)-l$deb{ymVbgiATo?8qABW{fZRJthczg@i9p8sV z$IrFJLrC+S;aC}<-+r&7TZN-2&-Jl}1$Wsi`uG3kk{ADnOV&02&(!(N|5(Xy|Hn#x z=iew<--#RJyz;!pvUi5Qe*bF!3f^nw64yo<%INvl!nwUCtow#}7VN$G)%hPsDX#zK zYUj7*uYP`8{#&bE{sX2ytDpaGCjZ;3UA}%AUH_fc&TpUL)z5G1v-e)`YEV^7ysOr|?``g3V*gT;bdmb9xV((yVJsFlYG_3U%l-;i7AEniST* zP{Wl-Db!7xl{&WkV%U~bbXVCY*PHs7UZQlHQjlWL{hGR&Rx^(|*Vd|-vP?NP&yj`R zSSs;f;TCo#yPpq3=fSn7Yjrrip69=K4q~~t2b$?E`##|-QruC7o7p!tmo5!|bqU3@ zzB=dsMrE`iVGB!Eq*z^Koc4e>sk;a9oGuCLFioxC_T391r4n49C+rv^MlQ zj(2f<1Wvx4=EJMyuD9AbSHDb?6K~p32fSPBcd6H2@BEiP$3_QlSixPL^&s{vlhZSE zCFRv0rnJ>5VZ}U!GXE!~eNBDL`~%A`hF7f5YNudzZU5}m*GT^t%Pg+})8zxL``tqS ztp0QS)o+6I?BaGfcEXY4n1Dl%aC*++U>wtM%*4@&Lu>Pg<2VM#38`@>&-)_>yH;#!mcMER>K^M9$; ztN#rB7h|E-X{KVaL~B znM!==f_~E9NuBSkk8c6SVHDpR>py1V8-OD*QlE=2ey#$ydfa&t87thO_`YQ%R@iTk z?+z!qr|NQ|Zd|FwCe+g(>g^A8A*6L|KYY1A^h$r|)&9_HCWPn8sSH0y_}%dJ{@>o{ z54~B)^_C9f>sf(0P5tfeAZIAPk`VqYQ5)QM!ngJBM)Er*r1K1M)`@0fRnQ0iyO=H2 zQr_*)^yg>NU;mh0nL@yxw5)XejZLMiyf5w66!j;xSt;Ts7TGT-*!!B@$+BUidU zYFFHCLr}sf)O{!9($@s@+vw95;OaR|Kl}nA@M(m;><{4^Ri@!PFq3atxhia z%$YMY_gq($h_vL7B`KMiIhQW{*P%V)%&0;dJ2YcR=2ruchKr*`jfgsLX!gi4C+b>! zFOD`TqPoo+Iwp6J^MSY4izDo)h$my@n4Z1miFdZ4TTS`G%deVJk{frZi%2)Oh;!BD z*OqxN`DFO7BI&C{jEd!Qtuv;SmPm|vsLxON@ENnNpI&c{!(MTe zSg}dP)20+Ple;!yKb`!3#R!_OZ2ki2GSZ!juPU22fA0tFu^l6lbo|O`SI?F~?FWgq z0R2|uXU(~MO7zOWMv;MD@=snhWnM}1r_6(t#~<;|o^sW+PkUb-5bFWb%_>Ufl$N!P zyLz%%kE1_xNy*jIO3pqrbePD191(M{=)N=BJm7si_w&hN=|4#v&E&Jw_sfGi{Oraa z(Wjp{dr!y}u8&B%#js0I9j(2n{xTUHvi0;6zupjXMNqB_*`JVfEeT0iqa{ZQ#7W6; z=}AnCb%w>YQes+bgj6E)@037yo7uobR{=6|W>aMRsd#W579HFg}uTR;hR(+SOi$ zImSwsS}4WF2DuqR&Gr&+ZImL)RFj@S_>wMN~b9r?xBxm)`T;pVFnVwDEIq%BwApKkjC>d@g>okpj9Fuqj1DR-(j1MiZ* zi1;GvuOLsObrs$tM0I zTnE|4QE8{kJ2y&NkYV)4ZS;%o_y2&A9%Ek%eNCExzaCg>3<$hWNc*IPzCODD{|LS= zMK;vF7W#TL!nM#gsedUx>Hhu?7=MH4um6x!X9z5${tpP+lhq``z{;ROc`d-G8--7aE?e;X-sS4Wr(~$nq_sH143bCil zEe+bGt%2$WB$W1o?svQI3mAuS$XP}-eP@?3AO*f3mGp{xYu-lR59Kvg5fQ}Fb#hu z;>&=)3b{@_Xh)n5O=$>qn(_}*-|hd`(7xZf=cuPU_37(t-}l7FVEf{xp}vo?-OL#w zQ4XKo?+XY+kcCj+1ZceWEu1&lI-|%NZI#*AJKyP$N!C8ev%U_zfxO1Pru7oy3KczH zEW-Z|)L%uNdd`RtUYp zo0rKJ?$xWY`$7ows)uXnE|RhCn;Yg?bB!$0GS^bl-MF^FdxmlNA!F^%#u3z^wiQSQ_21pp=f?8xOv;^Tct%j)IiH)eS;|kMFkHAL2uGUDo50YF0A7$J%~%b>!L4OkHmV`FcKi*YQom{f-Uc{?@v7eds9A zbFQ9O^z-vVxHKr!a(fPUd@JJ^cUjQa(OmjkVKQ9z={dc5+)IYItYZ?5Og|TFxNl-$ ztnsmA7!OnTYzZTMhjowK#xwZNAQe0_AE8WzHOnqH);WfIX&`k^x1Y=1@VSu+JwL%x z@LIX(YnYp8C-+0(b;4VXckDHSo?rE|ysfoZa;(C}XSz7i53`bZrt1F70mdTC(sp1% zFdS!JFCX^9WSFJfc%|H7_Mp#L(wP^~4&8V9UgbV_JuhQjKJL3_+#g${t2sxyI%*`& z87u7_moO)U$s+SBX=A0y<&K-Bi^hzQDH@d-_*Q){Gaw0~jqjw${5WvJSP}TiUc<4r zqu*|ZYhe*>{~3NY-j+U)%Kfj`Wal7HUuRIUgsJc*+#5^}Ca%{Nhv=V8HjVZ+$Ro%b zY0tg!8R<2kZLlT@d`lQj_zj_HtPGq`gILp4OR`ZZmiEOAm&!ka?$jXKFYMaun_CDq zJA*7C1Z_KT*se>jQ8(D_)or$+|2sfy(&vkuJtIR`I$%U02`% zxCFHB%MIo0A0m7K@IlvU!(QZ^&)BNh=|^b8(}dWp|Az995mJ93gYZ$pM7SRv_Dc0z zP>Lj6hinZYZ4bOih<<%@;zOY?ar~)Y8{`-=+IBXYkiD60U-y@`tJ6?_gbhp$0@)2x3remzXa4B*g>e<`3ay?zZ2MF)wdzyt?yyVzpQ2Cg*5Ix*q4phd0NNT=Q*wCRpQ;9Q{@`M9?oZFg?>I$-lgSM zrHnOK@Jue1E4UBuwDg>U%zDtCgIs!Tq4Uj!_MCdfIlYFpTnpCrdQB(hy>|Xs$L*{S zmdFD0Q_{yN+m$^FHXSLFYo$sn$5vU$JUE6n?zZdF^9}i0MxCX^J**oBxHd?v4y_v{ zo4nE19?58Y*3iDIsdGGSt76Wouma3q>C)TToM*^Ga=FVZPtYd|9Mjm->1SGztV2J9 z_RiK=b@uC%-kkX=1@t}by)Y>YaHgLz`MFW^G?auP){yBI_|n$Vp-P>wsp|6%lRa-@w(gByhfhTy4pYcnq$-1O0GrL!|^@xT&Z+n z(^XF0xzfXh4|ZKOGE@6QxkB1Ej`q*9cs>~4)4r$dwcj|$cyXCe@l2VA49s$_`BT!& zq5I%U(AQ8uvx56obAu$%R)~tHzcswX~l?JXa#@`Wv3l zA$I+(?0bjjpxnt?=ajxCE+6Y-T`y~5+YjxN-albGWGmyMUc%^)R@Px@<$8d7^#o+*(Zgx&+D8_AK&`hICEabx+7FvjiGxL zdx~VqrQiEm-$*ylLYd{Pm2r*r+I=saJ-DP9a{=>O{dVRC<|xhC513DxkIr1FucPiq z`ro2o^x8#_tp(<{jOR;qUjzcQKguyto@6h*L|=b>4fGhWuLI-8Zu@VcMLNWBOuAab zBndyNbbq)!T-Tnm)Dy!s|C#l@lliEdBn15&r^ktXt=MnTKQ4T*Lp^5nb#u*N-M3IW z>#;+*A;Itl!u9AbbsnV79Oj>3sKdl3oK)@1L&0 zgZ(qk-rq1j8pFoA&h^Va)_JUZ&h>AwpBv|mKZfYd)Z# z)o7W9=eHfc#k%Qs;sw?%>SR#nXPoF~s;z6fu_?u@)qr%Dg!OO&Ww~*>;<1O z?vqqUhCJgKAnSD6%9JiVN3yL8CDU;g@|&fQ_5+dF31UXcN7e`Ok$H+|(`jjApY!-w)9UB;OwPb&@@)HF7V7&>&qL-E zNg;2M*^l!HJrC*mr(w-}q4hcU*XQh)K9`H>(!v)4KK^9Azv6wo@XRSQR*v3BMUYUr!iMHN8_EqTHIAy$g z4WGhff;mXKX+7iaz>lU^EVC77uy>1LI?=sd&Np5W3wzTP`v&yML$4LiAOxG%Y1>(|2@u(wIb98s_5i8)}er| z|Jr9ce+((3&nNF}2<@{m(t!_x=?C>QOOJ~`hQWI5vqp6PBW#?{|0?{G{^KmC!A_H~ zOxHs?2mOh9JpQjxKj&$$eU|wrp&NVuw=+$?ztH2J`s}drx%9t6^zHRTld$2e<`3te z_^HpD#?W`1=d-?S_^v{qjq9_5MUJ&Re}5ZujbpEA*z}yLsr*0B)BoDee;s?&uk&?1 zf2xPyqwrOKC$L?Qp})G;L3!d|<~N?h{*UmF*UlL{gZ*D2{{MC!-FPnDB((Q{dLQ@~ z=$NM>#YWO zZruLgpby-xXDAbi{xgANdVl+0nM^&CDRT^c&0UQIzIELd__pynH1=m>Khef;-R^V# z2mNZStFhc)h5ze1HrDf3;UBJxzBWMupK)D2YxLtU!~bKu|IGhJ-1jxDFoW+3_~_cBnaxHO(}hr?!4W zng19MoP+4IkVZLv>hBr5JHL_Pt{C>i+y~V4T?6vYpY@SzniM!6VhnU4oGd-B(kzkrtZ`?V!{tWf5NoGG&a1DIyI2=bGe^s{#umBEY$wxM zTTVAKSU0jJFwd7VqlWeDD7nE*=KX|Uh^6XCmiuyCsi%+s&S$kng>-p<^(-kZ>hzpl>dqPaoyAi|{{&jWYA3 zq*;fUfA5p;S+72*&&nJ7k~4!;WaC`>)G9}fiZ*(&9xg&(fB*cH6yZw|@qu=FpFBj_ zB5R+~(b}gRj;8`YJ4EewJY_7gNBaQGzMs*&mJzXsvY4zBMG^7OS0C*GT} z(}sI3mi^0*jHR}WBfYlBX0Ev*7+;FqUaJPzNWC1a3)nZh+#@(IebIiWqr3Gm=Rz}B z2XofSJ$5no&n2*h@%T>-bv>YA{?dDJ8|0N5^kb2|WR|n%Sl9r%?KOITjm|cE&&>YX z-m3@q=uP+H_8wdBuWy#|_Fh}>w;N%9ugm@UK0olHabMt_hCP1Z#oz7m+25br+c)m( z|71^ZpAB$+VEe<~pL()6Yx|b=pCvrQFld+~?RSR)hv@J5aH}z1u7E6K74xNLp>#JY z1K*lG#cN~*j!=vF){m5I)gh^DJjQdS6!mHu7Jn^9Gns{9(cb!t;Z5pR-51D$r-Hx51Vj>M`(| zHIB2q-*YMH(V#<3CTBL8gnfgIB$dfSdOb-GfPsx5*Ku8s_OY?Pe#mvYX)ho(BDc=y)1(9S(&dO+l{Hw*=9?YtBcm!xeu7`JeU2AKNh!J>8cn(AZ|(-|dm> ze%Ae@uSesx>4aRzvp`=rUDm#)$aPxx#ic>)>yKQ^es^6MGyTDyKlJ!%!2YiBIpVx; z!8yub>=7E}teakgeeiNH)ai3Hu8;mIL|3oFGfu8%?*mooxl)JtwTpX#`?8+< zvA!0x-}*k#YZNWh;eL3Yd+2CGXzRR5yb)cdX})f+o=eZcocWH%x-f91jXs;y=Zwrf zjo9au%zKpU46KXl3-ott%nuyZ*u;t=Ce~6?Iu^@n2i}f$Gpb8$y{VE zHXkwHb+mM}cf9HN&T+E++Z|SSc(}taajoLo#|??QG;Unn#JIo3ZH?Oz_d(pJamV6L z#?^bny&b)sy(!*4-iy2$-bvm$-s`=0dRKa%^S=NBm{+Iq{R?7sTHi zUlqT%qqAdN$8H_7I!;S462cSW6OF{!#7>FHiG32&6K5sfllVyD#>CBu+Y@&r?n?YP z@te-+oriXwlYCY3bv;@?8UN(s)5htM^$CG90cJGu^4)ovd4stC@0Xg7nYE6#junnt z$MN>kz8>!@<0LL7&Ks8%mmQZMH#x36Zd=?NaUaHg7Iz}pclh(e2Sg6IH= zAn(P)j~u@5@ZE=(9A0>M{^8QYgAezlZ{%?3H~;+RyKfGD^U60{zj^eV>$FVd8&<%F z3J#4t zk3FK!rT$lB`F~Nk=}+^@Wd481wQLPio=f)UQQ--8?z24@qol-es!HDJOgLlNO*?#bN6?rH576WVD)2v+?Tp9BX6XeXFlPmpmel*OfZ(? z&UJIoxyQNl-Q(T73E?hqPjpWuyqq`7?2mg|Fyz^)Kkga9u-LsQ7~|=zKkmiBa7i#+ z>R#qv?q0$7gW`U`z0KBP=5SZLd1rv|MfXeY?QZ)Dx&Q9o=l;)A>43FutJPzVcaCzMP^>{+~+sqT{Y3^y^3G;+| zB0Q0vmYyh2D^IkiwI{~YhV?Q!>>Ta?bnM(m*;`7TG|tKXJkl`cOpeGM%v&y&TOJ9K zW)dpRrGgPmx~ITT-Qu^p$?nU(#d%@5@}k40$2%u?!R+E8ameSTbaYWXe$1U&G`Q z87?E_Qn^erA_9$v?(;~4p}I7%3X4gtYRcT zCXdQG#_{9wq^xHjy+Jn0)AB6qVZUsWE%Kafm2FZj&of)UBrnU~T}S0=DUxY2)Adi8 zCyz*pT*-L7R<3a8vnqSgox>i2u_rU+I`;@h>}BTF?r}1UUG4;)TrbKka=FZQk5M`9 zJolw?mHdstqDPMF2f5z$ zjq5wt&$2)k%Wbkm7RfSME-PfI+$;Cd!j;^m56NoINh)}QZLaa4vD#Q8HyCS;hm1RY zLnn;S&&$mjGkR3^$jdGrG5nHYSwl02WDFj3v2Wl-7p7lu{(!Xp{rdJw>Di-ua#EMh ziJcNU#>d4(MTCd7Xx=Qu<90b6mT4$S_WD)f5Wkt|jmVsmFeG70R`+D@keK3(?#V+E zG7J6QDPBK;)j1)H97G9I{N6&Zzcaynakw{@ob~ z-fA^|R37mq843Ab{||QjQaf&SwiC@s#K%*@_DXx}9pcZtws_HyLVQ%4nuQEb7(6Yc zd$Mc_X-2deF@I7*$tIO_k+LI3(vb5v8RBlPtEHtwrWETsD_*e$dup=e4$p5L58*+u4%MT-{tBf9y! zCS>@#-f%F6KAPrFPRJPI@20H|AAPQ#;lEX>{Eo!%1n;7sgytmtaFlj5kxU6noQdH- ziO%#JgZ*lBUcCOa%jBvoT9ldK&0JKtXi9aUd}@L>JYmtM(9lICL+EbF&hsneFW%M8 zpSd{SA6{6j&c{`|>6xR4`&*8hkmolNGrh%A=nVcFm=K@VE)9>eZxrgBn#P<~xjLu3G3Q+I!UV4E@G*Id{8r+y zqJ$yz_+3-{VBwj7eQ& z5r4QOEq-nYerQJpog*T>0}?Q!?F>m6QW*U2+Ts{$^wQB;-5Lf$PM+VFL67;S1P95G zO({JoF{O|THd9~MQF;EJ2_^ohgh36X2!VEM$jmW$c990@5P#HQKfibimh$%;LRp6X zkVSf=X&1Dugi(1fNNS*NQy*`;Eqdb2&p_;t9?U4~JY-Q`(R6=YVY?#kyXoG%cJY2+ zK36zDA#Yl~9(MFl*E*`>L$eqwIeEj!Bn%%lK98#!pKs^r3~kOz9P$S<33=@r%rF4` z?nJjY&uC}nqZp3Bn@J>LP&$A8u0%J4)8V#M58OfN-aOS#8nto5-_<*0T1HS$r+;UZ zgqb)vtI?WMC$Td)t6e;|xc%v#Y>@2@R!niX?$j)ur?Z%*$aXV62WweFU&iP`@6Ah? zmXM!N?DhMy^K=*K?y-A0*co=8H(ZtBIeAS}O*)Hil6bQ1o(p!SKeOApKGdD-A8Mx? z`CG{|92;xNDHA?!FQI+T`=; zo4NRWM&qJ{VMU7)#^j~j+RP_^Yj=aLJ5q+L;W>l4Co{DT+LWLwMs4z`iZSE!USL`3 zt;osSY$#)J;h_9Yoyg03!7JjkrG}Pjp;qzggtj!Agxl6{_kvHP+|IRZft|j*T8S-d zP(r5U@@k_&7H;cp>TFl!V+C?~wbhX0YqVe?b2rG!ZCSqlY!cn0z7U7c?eq9TjpjzX zO?ZuGNM2;6;!(0CR5e%aHc<*65m&47O&(vn290tweEAI?=Mn^~&K;k(h4qYTXaA+5 zLHfgRi7DpJ;CVB|Tciid{QTlYh57nslV~mt|0%ypxQNS?a1o9;L;WEM(+2sQB@EK? zfm%MWLGILYSHd8_idGaa=f2DKEB@w8$cyKe^tOGg-J_M8Xj%J4rc8L4>tIW8tuaxhr z%;>l;>oVi^)O}HRtj_e+rS{9L%Z!Qp=MRCnAJmcDqgmhQe%D)azddp!_i&cvej|an z&o2BT_X}m_etw~ud%z6Dh3$*mXV`yyZ^yLD-1C}xxh6gC)$Go3+y9;v7kEKsSC>?m zSDV#=8egC~vUg_O^8=qB`TU&cZ+?F5^Upr-irJ=0Hdk);Z#Kg=tL4ur|8pwrIpq%9 zGH}bWEoQlYx!>^nYy7o-v*)t|pEW9<_CIaZJYD;=(R1U#jmFxiRLzFk4aUgz%hnq` z*Uwr1%KE^1Yt8CTaoMZYocq)(_o@3b+sECzvUOb8%D9y`uUxh=u+ovT!neXGU!h8t zl`k`vFH<$kYL^)!7f)V1XR&!lW*~0u?P|eoz2eGB2ga4skU6u{<7Q{}jcco7a@(ZF zz)1rf*bYQs$m)H7t`oG?s_Lvzh9P%ww5Gd8Uf)-6}Uih2@6#4$Ea#mdidXE-q|f z*yOOA!>q8do?#=y=7cQ^`ywn5<{F6XSeQA7V{B37(aNE!)$&a_W4d)4UhN9-gm-06 z@T&@c;u!tcH)_1!S>c!5@e}ekDYZ2J_9aXB_3Lnd?=gA)!uI*Y{Y6B4I#Ny~y#1zV z8I)gIR$6vVH~mo!5h?4|t+Z5&6o}5Tqr_CV(r%*lDMX^|8Y%5oTBb@%%cQgnd8wL2 zyp&%hA}>`GhEntzj5J!Jf=L+SPgz41rKOZB#ZGCkx|m7g*yY$If8*Dvt#~(7|NY&^ zI-h%q-FbksqNacA$Nf(2|8;`xn*MBMC-JOQ+VRFe6{2_I8yk~$_=>z9{BCbG>-@j| zyuiMt;j>a6V1M&hpDXPB!aC~z?Ni9v_x0?BHnPikQbcuBsnmL9P~lT~>o?V=G4-W- zOYUYzb0vSD=kIC}<3{5IzjhrhvyG3;TiDawNgLOyne0zQ70Nm_fqmGmK`WEkL(OSw zk@zC^F^|g)oQs|NIBp62D9z7o!Sg$@bDzwVxm?MxGaUjaqz`ZDHs>9tSIjtke}>(f z{@l`7*p+2oVQe#uv-cob!H#SNzgzhjKbM#nH|qR<5jt<-&DbdG9X*l)AMhKF+vHQO z(2Mj2XO#9wpKCP1$ipknHO*SS`QnVjnZ=37EU4sk0abm5;pUXYQv#f{cuw-Vdnd?& z{UXg$V3$ZJHkv2H5q|o&k)I5O!f}53PMHV{MHB;ik(|Ipa+=YSdZTvmTlQHZ(HXq% zhED4Wei}!)80u={gM%V%k+nnCZXPgt+GD3Zb~`NQr<37;58eeL@oPjn;#&go1oAs) zi*%u$u9WLa`n+@~0sKk!0{O|5=}x)_b@a>BT160eXJ@DqK? z^z)0PRqexi&fm$`sgzGA??Mak=^`J$$=@Y%F$^vg8G;W(u`v`MhK&%pBnoPw zj>~|~rN}PJ;wMpVpw3Z6BBN_~Rf%*?x=1d0x#*7#g;-u4&4UWzhym~~KbfB^c!6>g zVqiY35h+Ll@+T5b+z7jPt(W{sX~IwMK!?S!3U3{yhS0&n$v9 zV&DE(CJ`3EV%QGHMP_+n1k4A&$W`lLuW+CNX;2KSV3)`o^h&6=qzuq2q29UpG8ft0 zBO+I0>*{P+09yfBX%vvpzuO?Cl~5}}aVblILRbzpa8l%&PS8i>+9bfwYw`11@~%BD za-A2l0Y9&+f&(J+!ode6Pzkld#0M#m2b8~lI~*6ep+@A!m9PU&iOi>t`RL5AfNH?r z-`tQUa#I1|0oTK{2d>T_U&7=irL=-jM?fU@IIHSr`K&L@K-@cT(ogB3KF3bLSBz)L0;Y5#<+c zgaaaXg#x;F6~JQH4%k|Ze~Wj)DUl`kvos6lK^5#5Sw^{K=}-dLUv^Stc@kh}`5M>> zXP9VH06$jX#|mUCuy;3lcjp0i@1~u1Q{O$~BG~75(Q@;DE@s1tQPqiB!h`Ixl34yci19`C_feOHr^9w!;y=79zdf2b9@f z0u_MncFJx?Umr8S+y{za2b>c5dlD4Da=_qGm-??F<$oJSeO8hAKKTz*KqW~Mn%0!MAi2RGZ ze{B^xLEewyK!2aC;b)K)B0r}@*8tBfl63hbn$S%&ad%Ch;krv)QECcigIPZ5x(LHD93Q@djp(56aiX<=cps1F`fSoApw3-jc zc?}Ia(fdWU&I7-w82pSmC8`bO+O8DU&I=nw#iqaxQSIl6>X5^h2%B+7MS06a#U}&$ z@zl`~+X=`zrHe{T64iN@s4iPYC1JbkDpBXvi0Vc@UxE(Dqz2;zR7^?ew6Pw0tx`#e)!ct1?B_w^{*3^ z76n-_52!1R@&iI48_JYYk>9)J0j|ma7cssK)Fkh4<|i*B@iD${)j$M41Q6UT95_hPz}dL zU6urSPyw`aB<&fAjgcjQUn7r-%I*ZTAsf5ddqs_k0?Lglgza!j)aVgF{^%MYJ%+q7 z_&(-{sGJm-2M2i>6Z^S+fOIala?xdtsK#aib&kc}*ps5hQD)pKpiCZldH9^S5%!D9 zPlqy~uJIP6!F;F&>YI=Qlw&Qa3U-N_h|XlnPxixcHdojyB(HEMoDnr88Z((sW@m|-lLwTWGY_hHtu7r#Kmp7W zHMb9B0QJnJ?A%(Y6LmG|tFd?WPS`K1lyoWS(lY+}F8nMj7j+G`uk8ec^T?mK0Jg$G zQP;-+vKx{`-DtrYQS-A!{VfU3h`NdN&G^82PL)@Sx`n)3PKmmWa6u7J&+T5=FX|53 zxDdUC#4BhEYcX{vHWv9s-9;YjE44TlM!Z+V)i71&%s*}K^`;}NI!;+$7)2aqwKl^a1`+2@lYri^@Ii3d4jwrPKtUG`IA1N zY*is(XFYoBb%<>JUN|Uf19qPxexVD0eep$g)^d_LH-Q-&(?_YZx^+R z{7nULO4R0Z!1iWzHlwq-4vvf3A`lL1L~UIFD@8p=8=s@S+hPIT=d%ExtFiGyk*F7a zK%Fn15%m&%uze$tewp$wQ~vL}M7^>@R1M{7=81YW32H>WmILU$M!DCEfxOo%;0WO3 zKMG(L>=*S$HW0p9CTb_;cVhFc`J&z?{SN7ONb~4byZVUQ-3hS$E@j>=gc7KNYEgSO zimELa^&WZe>2NC?6!kuO9~1-i?yVHHFGJLa>7qVL18nafA?jl{_<4mDd!JIzfha)# z^DTC4BJ|gO%SJWZ$4%LZbU7`*n|2CP|T`BWj73>%F z{VX^x>SzUE?}t8s?1y!L&0{`M$BRV$s}o@J#BxzTqVpqVPE!6SFiTZa8 z)QUPq-Y@Vg^3z8}omnaBY&x72RZm+3IbujC_{C6Juu}|U1k{LO_JLDkSlh*L>=46A z=&BaO4W3vihy7xNpcg{ zmPf^i!e$h8w~7JEMb8tXHNLg35+f!9YWVm3e6UN5w%KB|i-N6yZ|zQs5t{@BuoBh* zd9g>tXpg=2g%)|j);-yg*-7jquY6-7+r`bp_5c4M%NLr6ZVU79yYsC zU$Re(?ruQdqg;%h*zZ{`$9l;avR{l$>dD+L#!%`QS_s(8>I1vrj2OeRVTTx(_y9Y@ zDK~tT7$YnoeQ77a@5|BvJ0nK`cC(Y=h!~^h1N}5=BVcC~Hb>{cPBF%miIIaW7kjx? zVvLP~YB9#8h{5*2V2xtrZ3T4lv7b+w@z@_<0JGqr7!!)cDCi`{#3ZN_V-k9kR*Eq> z6xNARNV;$pREaSKohj%{B|Vk&)N0r%#^ohq6lIAqjrynMKoQgey3@I@#JFyy81snF+bhQP>%_QW9*5i5xRE;NQ|El-e=CLqV%$^+)neQn z4%@{jPXoUgx1<0zZ}q_$F>b@R1)ZQ)jN9?=j!?kX9ep4Ru)i=FmWxqA`c4aC#8~7O z<1WI*lwCrZrA1;agXOct;H<@1Q76XT_;b%zF;Un5aYfzV%(2Et6~AW4^aLA z^d8(L#%jM9YpCZTe0vx>Yq7s}z8IBdVm#7Ej7Rb7(Vb#EhTggoF&?*IqZm)nwkJo3 zQH9R>aG>rD2gG=)R*a40Kb-;x#dxMdjAya!PXg-Plr6?){NR~wY}qfy)^Y~#%+z>haq0d3hy zdMEiimjm{9(uTK?z2yUZdut`s0P?rJK-stF!8)h~>UyUWWW#)*{&)6@u?xR;A=|Y8 zcEU+9cE>^%l)*+gD8{={kO9cvT?c!`*keHo6u?TL{GK{EB}Q#0pl!AIP+JN6#dr_9 z?;&FiVZ4Xk_mI7hjrXzfejY3UKO7L_12-W50NWo_Ks6i@V{Z&(K?$saU2sN>eMyi5 z*xrZjef!1u5ZfQ70d_y!F2+acfd3!ufKy`Z$Ikxwfc}2`{x}>m0G*HV`D1Ku@_gDN;6 z#<$^+1?5l;N5%NA6Xd~i*a2t6_&x=&^}QcXiE$L2qa%QFN0A++&L6@d1Lgs;AC8Fe z&sfNY3fL~jF$+?lN{r)fpk2ocfc)dM>-a%2{uKk*{uj3YRSn0*I6-|U@?bIS5aY*C z$N|dzh+Wo(#z}NeqH_|RlgLgU72_vt{geSEum)B+6yB9U!vCmdeLXZY@GxJK-t#l zwnjH51*jut6(EZ_C1xAyXp;^_;0I)FGhnNj?WnsQ<=a&O{=`NBb;Xtgb;V*M_NbWc zk+)v}C&lc5jSh8U#)Shu#S!-s_fpPFIWPI%y+A$jML@jcET{zH3HXwL?F8bT+>iz( zfUJ|2Q7#dm6MaCL&H{ZP2j;_8I4EWpbh@C^g*v)az($~a5;l|a{{H~mNe9I2+6nNd zYdI_jWalM887zQWF}qQBw_?DTWc*8BEM|9P-K(HR%pNIlRLq{~unKm-aWPY{lTrfJ zuv5&`3P3;gpqPCu!0$fT;JnxDOS*3#K(}8gkk^lT`eCbo3>3jeI3s3S4q$5lWd;_(h>G*dcp%)w=1 zW}q{a_70`~EXoceyu>Z$aBL1gAm)g2F)#CpInpa;cAl7{vcw#nBIcNEF>{h&JJgDq zOWkArVveg2GY|49KOVc|Y0m`g6kwxZ7OaA5*bk?~oEQUp#hirAN%%PFh?tYnos7-G zJV0kkp_o&VU5?Gmv0Y?AAIN}mF{fpVIi2#;i(!|T#eOlb5ZEf_l|HBxbCwqfucGW# z$gWx^=4|4#(*VC_qdWVIm~+URLmhMQxg;6N0srPApSue1?`rDf`)ae4{L6@K zm^anHaWQZ9LN=5G`nPNp^Y$V!D-MczXE-%+;YV9}bAQ2K_aw;Eb3L;nTxeVy?9S-AZ&T>%@FyjhK%f7n8k)xh@6p`SD_) z%oDRReZagaH)9FwF*nB1ya$qq~ z?%8n2gXOSOOh5KE#Xt&B-=>v7A8y(TJ76yygp*=!rVX2;pcBjk+OxS@%q_^b(8ewJ zxCQwZ zU?XgYU1HXtS5pM&*3YVg-`eE=2jP^M?9I&A zyg zaWUV_fR%7W%$+{K_DA3|y9UsIFB?|DK{4OQ&ilmQr|bvBKd2IOuK+rGv9p(Y_ho@! z%nxIr1Zu?mC>)AmrkG=K#JOSO;gs{45*r z>$6j0ex3u!KS$1<$NT~tb)8@qY!~y(D8R;-2gLj;9jKFijrnyfl*2(W4`u;=AEeGh zX+Zslu>DN|Y!&mc8}eYIm`8{oDFN#F)(gbHCH`F!lmYSYlK~swpA_?`4=Mop56FKY z{sVRX6JP$h0BXfNhQG&y9!{72#^iJv5X za=(~Ar2z4tkpG+n^MQKVLzt(sU@?&Q3-Mp7#Qaqt5By@Db_4A_P5g`n$j=Zzi~MXM zREt?33dHNTiWxv2D1sWXgx>+nJlG|c@mgRAKl?qd;g{d4q%duvH zWtew+)B=yX+M}j=RE|ewcvNqX>gZ8X9%Xsd36J{9qxO2#4v*UGQR_TvWutykqkbQc z>eQ$|H&|s)1$8Us9_#;p+|N1s5p-b^QeGF9rLJ9J?b5gddZ`n@~DSA zYKcev&7a0io;8CA>)b2+8)gE=1t$(FQP4K8oZTnq4 zs*SDR|FlPa|N8`{gME~2qPu1OOcKa=M+`Aok0_n+U?g6d6l?PwGA|Dr1cdv=qyG2!n{ zMoABdh*Wz(s01_B?3>z3%~%<;*z z6dL1jb%=|$T07nCV_R8Ko@On=BCVDotz(>ysFt3Pkd`qHt8Hj#`?$7NytkR#Wra5H z(7uIg9v91RVk6o_w~CAiZ?0;atC11QBGyKj10zO87|kP^M>t|yN3}FVJp6mNJyTOt z2SVfkeq=Ur?!>t?JvB9acW*vTqmiohfQU$-W9^ya4-t4>&&ib;|6_=UU)GwP=U#wPX zW~sTe8e;Xuv+9?rxy*kO9k|*W?Kmo(B$;3Ae&}0l8r?%9+qUi6H6fu>Sg&3#SEuM! ziE(kQI)xgJK7HHt4(nZ-zo)BusH<{RbXD_Q`(4JHZ9i&jY-_6)wtcd#@lh}JL@#w+ zFV#J?S6f%txJ0-7EqY0`(KcE|M~3@aw$2Log@q}P8Rlyio)vbUQ=Fyw2_09O>rA7> zEH@2@*~-i{r8#~nw zd!;0HGW&N>ts{D{Z|m2e0Z78tLv@MhV77{Kx>_ZuK00-gQc+sXGMx2eGdHwZyL8jS zF^OO9ng7zR^PY*i;hHV?=k+-8`FvH`XX?@=_n$53efgaFGf|N$H}Sf89(6o9b3*a3 z)z5lvyUlZdW&EU>&FTlAmzg(j)Z;swJ!-^UoYOzCKG{=doV;LkTC94!g&wC485TI{ z-0N7vudl-xt6jJT<9vN%rK7V&SZMR4mOa|EY1urCKI+>iy4SY+@aXCZ;oS(W7H0EE zPu#YA(^EacOeAb61Nj|=#0(s$!~f_G##59tp<`z{Ahmb2N>HhZy`yP-gGlx1oY2wP zDk?g)cfY1_=iWyjUbFUrC)TYxeZgw;!G}(NvG&2W4?n#2LC3^PCrlVMazftd)5k}Q zpOl?lFm9CE{L$erKRfuvp|d59TSDtT|M*IdA{oY7oPuW&bGPduNZi5Vad=QY4tOT#ugS`eA|r!FL~qoqpR;2 zx-|6Wi!=9BtLXbL+K_qQ5nou*XJ1zJ9hCq{*JzI z{(18d(+c6y92OqZj{9lgz$RV513Emif2z}n2#;)?(AkK%`MK@Sy!7nTFF*70RwGKq zt6lHZ)+g70SO0x|j}LaK-6{_Iq1f;Cn|;%27Q)I!+vlL_kM>o#;Y#SotZ8&fjgE{k zx>3RQXP$jYR}fzRMSY)l-dFFb*8K1N_jc8%)_YCX)oI@s5l2aMA+n}@D9lo@7BHjGfG!5YtcI^Xl5OZ9gxbQvzk z7M@!ZC#LENDwP2_`F9V)h=hpvzVUb!-^y5fPko-Mx<^$Rh4ndVC=XOouF!skwyu3Uz{(3Gh({zrTn#Ok1~2HrG&?oo;L>2M=krtx0;|%s@lp*eBhC) zZo&ssTKhs6MYy3>PhfVo)%8j*l{xjL*FO!`!>?tG0gMq-T0U<$SPY9WI}MCbOvk-a zTB%f}RzF-nGs;o-t5-k!vja!11jl{!YimjJMMXM8#VIjuJYl8z9+%mwG~a9!ysvHd z^y8At7;YqV)I&GEccj@EPwgFPCH(T^k0*an^2-m;FM0Hd757xGU1_{pzqWp{x>{YX zu2fgl-(CNJ>ZKyX8%ou82#v@8z$YKiEH?F8U5Y{-;(5V-vF1hU2Ln4p&}gF^V_QR%2|LFMM z{b)!x)t86OA6mq-xP!9VociX&K+KDsR9MC8O;1jry7IBxZ@ccE(C28{-tX^U{;*%& z{`!Znz8vxE9ScftS#!(Pb8ow0PK&2s`-gww`VLmaX4+=(R1vGceLW+kpRa9%BhoOK zHdM+kOuu1=_d^{=Xdi_L9kMl3b2b53e`_UI*1cK?p1f7|0jtW5ItjcmyxF-lsx zoG~rw<>;19t9^&I4Dq&Y&8Vo>W%*I;qe}BVaY3ap%@K z_Usm^r?e>HF-F(^)}yQ&StaUcz16<{-{1e^gm>G3qbt@uzIfP81N}YC__MdQyXKkN zf2()C2*}3ATD|w|1Glg1(ck#_f%=Qb^Iez}2ggP<|I$mguUq>FXK1t5!vCd*nb0Ys zZB*N9@}r_mkEcaxepu+TP$R?<%B|}C?N-&(o&C_&KL2icrtjUThI?01y)9j=JPejz zcgX?>3E`9yb^Y`6x)9OcVx^<=ce@J@|_^7J1fBZhT zOuw04GBZgg$s{obNJt@4B%^{xPzi{@fJkr=0Sl2Ph?vlnDxjznf;h(_LX*bXAg@-}jt5cL>P7@B8`x14${he!^#w6$L4F1cxeEZPqDCn@MJCpY9yv z4X~vv(KnmuFLZebq*-?S;ko21ecXS zTrOOb=u&3NQ^VdOkh_3YHJK{92CN0=6`VIU=`0|xsh~h|xuR2(F0<5mYDb#J7S@=q zbMG3?wsz!EuuY`ue1m24>J#U}&XU7F{q3vvM`li*|F3rs{p;>2_dNK8_Dl2pg|nLH z7p_^la21u_znm7n{>P8sSoDfdj-+Nh`u4sjXQYC1;6+hAKK=F?%~RW2=gwa?OZ$8& z+eeKUqX%OQVvK_mU2;9ZU!bqaBNY`p@}2oue`lUk%#xf=$?uO$PWm}r2%0I4z8#R4 zk1>M_#x8SZaM$By?3lP6-9<^l z3w?tO?F$eCN(P(&Bgn~*8U0veoC8qRi<6CstS`U*`s>n72M@MBaPT0T7k)0IE~|jM zj1t+ZAXseBf@C@EwlyQ{PW^;lqo2@>BhAoqNF$k3+Kr0SC-v(G*YKnIptuGN5MpXq z$@TIU9E80RC6WmpHCvQAQ?qHA2@01Au}N$ZS@s^Mj9WP5b@D;59zu5c$SNi$>!LDa zcDyy@x03$`>Fd^w(r~fod-T{UoPR5D0EoyPI!wMy`Ujtny2N?72}PDIR>s0Ol{{sD zGD;86oMN3g5!sCnii&uSvDDUB3B$QT8ji7iz3O3_NVl%i zu7gGjV;PLSUc%4W0%1_1Q+_OzZLxX~U>nPp3*(-s(^_W*mRe(Zu{9&|93fJrP&Dcv zm5j>z$qwWMsS-l2H}T2+>us0?`j1nfu(#Pbv%1BrDrKBvez~O6ppgyrm(n(|r1w<~ zSN1Aey=2LTtes(!KR$vy4a;7aE&ZpB>C>}%a-Y8A8tS@tzjRp7N%NN6;dt}I!>2BU z0z?!Z)&6Y06+BZNGGN`6xHMuez2GvZIgtnkL-~TxHRQZN8PFi-e3X=Bhoj%eN$({e zCxc$6BhMk)C5PAHt?t#U=wq1c#0v7r71h<9KTcLN4ozT`m?Izrpins@H0~;Jja;J6 zU?oF#{U4{!Tr8|D)TN_p4~~&wRc)07@u#Y?H$){7$)se_@AJr2m2n75a$!LnzMmc; zzY^4fQKKxu6wcVv3a-Nuy**U-^@C6UQqn^OWNTL!-ict zbLN5PH&A%vj`f;pL-M*s-{1c`@;~sgcFQlXX`epQe`wRt5xZ&hUnoTH2<^{nJH8q_ ztNF{h(-(ZO7XBd6)C$TVoXcjxC5%XPQz6xel1YI|7x-_MJZ^_LD(P@=rKV@QhZ;O| z!b5vJwA@3D9;)zAxrcQ9!)><-b(M8h-7^O>Q}RIG#Q&}p(9%c{w5?<|nenl>L|*o2 z+g*>nDb{@|c5549&F-2KozF+3v_>1lbffaWqF2w+y3+6a4h2q20wiVm8}w6!T$?M5 zOZ2dNE#62(&a&Vnw#ZUm% zrXAXMYgTHz>7s`p8mK*{t)a==*3;5gey_~dw#?d+<9m^QnRLB&@#MDlpEa40VbC`4 z7Rn5MZeEw@F1dmMi`5!%N#Teyh;11Rdc6Y1BnuXoC1Ghm!oKyELrA-4aY*=fn6Nh9 zScFEVIdX>4I&aQ#yCcs`1%lhfn2ZdDG6ZF+A1U;W)=JL5(s>W88LGW;=p$|a<8(7! z{3Ug{?D?*r%D-rbwO_P0?F}lrYR9f;>GCgWAlD@2=4iJ()l zL7C(8o3chFO_Jio%zI*f-p4fcH)J&MKKk^+Nk!0S)T7+6MSJ@|+rQ`o8cXx{FkVLc z7xh^E?Oo!*KWZ;-#biFLJx?a`o_clxq$$iH!wn7X+i@yaD&T8uk{ObQs#r!Pmnrm; zLNkv8lPX)^mmX4P zuhDv~()^H?(tTLL=b&362N}st7SSZx?7S&b%x0{jS;9QnS##_NJd=Y3rtuaa216q( zd0M~QE&W~obKA*B+TK7zI z1S;53$qIAAEG{=61^;Z8X=grZ6TA< zbpm%IHPMydP<+6SUWeD71pn#ze$nxM+6eKQChg^2pKE(Ii*M0Tn*PYvZqskoKGjZY zKWOg`uGiLU;YqW$Q~&gwDNLth7bYcwW=WDQV5()iLp6iuG??iq7^j`>UnaVEFP~vv zXcnDjvY5?QW?y0JwnwGyDy>(kMWrT{8dYjgDTeh-e;MQm%C9m*oIg$3v7(S^u(tS? zN4J@F?-qaAEiP@FtjunES{(dKvp(mqqN8`BkCLz?F`CQ!z#LCPp$Y~1C{P!uTB!R8 z1u5%jy+Tb2H7e8qHi5su^F78Z{h`ZvzD=R!XTryn(_K&78VnPIZeL(}11@bt0!FW2 z-OUIka4&{iug0N|$b_6YUlOfZS$0u!dmMI92k;W3k_m|+%;<4bZo}=zVOkcyeV^*A>rSe*24F`)$w1c z<7-)+h~Tz3NI=ksS#3C?RhyY$%)$`@`y3DtGA2_*W^j$Mmic7ntHDkPc~IMXJFy&3 ze%ZE3#J?qN$FL^7#l3B7TE8~-S~=QMpu2&J3zkGP7DZy)S*Pu6mcX;>)0FSuv|D^z znSCmjqvv&flGk(uYw>@seYVdVRdo zeZ0oSyvSf)ogAgwV^MmS7g)((^odeDN_Jl0!zjIMRMejrKt-pbgd+QRT`f@((SNj` zh*BNzzYrzE@HL9>55t;uCWBz?mWnKz(L1x+k7V1-tPiz(4)wNiQLMo>` zrZK1@m=JnXE%XM;5Np>GZROqCvJ3JhdCMuf-B)C?V6~fmfMd(@-`&ufkoIi3b>ge7 zgO%B>GO`UqAuVN&5pR>VLHT)ER_|pTV3zvLRn>uSCp=6UYvM$>Vk6uyZ_A03tS%UK1Zl2Vq}4w7VVDW+I4n<*w} z7-}<5IF%ey#S z-a!53j-+F}LvlK(aA*68M0Y&8v~Wn_*g{DyB!}!Ml(Ms9qmmPID49e3b3jOQ$g1RI z%M$pbMiUJ(Q6Cfb8MM|=-}9qFSKs3^Hza}O#QDnpID?{tbI#8kBh_8R5@v9W=T`<8 z%gGIFpbfc-{F8Q2`?jrZ(2KD{zk6|i-K0kvpL}KvL|YP1XjQM|ZC$nbxxRDuUNn2! zjaQU0T_=q#YQDXBX5S$f6bFj>jksgL^ZV|9HoxhHrim9_)3eN(SJq?jB+N?}91AI| zp_wWYpW^s|2GI(i4h6eGk;ns=0o|zpo)D)D--gxPQ337(;wUQ(uu=tgfVlC9mEcaW zo%<5#FPyM&T1kJtjk_5%aaRc4$KW`Bwa)O5sN;?YNOo9@7l*90+)6F{U4xa%kzDuc*6_nhXAMNr!{{} zo60y*ZjAAZhjwXE`EL2ory}y7*Q9ySM?eEhF|S_X@b#$9LC>jneGaWv{(`@GWUf>x!p-nyd3?Fg+<(qCXbc6pGC-P$)1CDi$~(#B2>V zvBK79TW;H9J7H7GZDbaqz|mO{^yfCi=RiLRAe~YWN0pTIX4`A~>F&XUY2JREMX{fM zmX4w^tdt;_??zkM)S76APKD`zd}cRUT;z5^S~iniGOISLRh50&N+i5ELXkapHrIEL z(D@O{j*u-vKS$`B2z?r%cOtYiLJK1gM>a>K=@A+qp&pTbk>L@kG(zDBIU+Q!Ekgf@ z(B~045TREi^i+h_L!b z`e%ebi_m+K#6)^ILeE5Kb%d5iXeMu!Fk><#{$nMCINPEO2M930y$yUjys)QTqIJ?y!+`4yY`H9iVTFyJ0rdDtv=<7ealJMNFOHg?-%PWxs0SFj1xa z_1MW!mvFrOm^S*Yw$8<&x*uQo;ewboQb3lsq~^h&PFd3m?MCbDP0!s*vUFqX{hvHg zIC+J{%rrX9Qp$i|cCsYiu)u=`=54#A$Tq9pq&gyMe^p$pzN~($O0w!xtJOZLJYldp zMxCdwR8#6c^_Y56we(aeu997)lPZ0t(ms`*QfZ}1GgTU+QU!l2@VAHf+x7e{ecB-KljLn5e+ z$TQ7G*V|~YjV`rOHyg!m6tH0g^u3MfsErQUXrGO?+i1OwR@$h^Mq_N0uu(M!_1Q?U z(fE@#I%a^TY_!2f%Wc$RqbW9Ov{8eN`q(IDBS6bG0`O5AeQ4AR;FTP54DUt4Misnj zfkW=&Rn6l7gLyr1qn_@1EgNiv*LuGIQOyg>yzns_?MYPGC}mWlzxVNO;N^!ldYShY zrNu_xFo>kn#%cALQo?N=Hdu%CJH34Dld+l7zdOKT7mn&6J1{FFQ@t9}gPR$6@vWad ze7Pm-uC}i}pl9f*55)SmonpPzy=}C(j_;F;aJD~=ePR{*CCV(KWVV`RQMTEkRM4sHO$WCr+oSGQ;$Heeq8zny7h(2 z+RdbMH*>Y8hhLXp!!=JcxTdBoo|DhFHd0c39rDn64_u^(JF2?A1nG67m-d z{+!1D{0m^=G!U?`()#;1E%-tzbxIzSf493Tk7WtyVmX&xcv8V8S@!|Ot3sk|#|NpydhOvrU5}kYYt`{vH z%OuNlmc5n_ET*Q5ErTp`EcaO+vncGv^>`(B7c7kyaSVaTUpEqxF0$@|OzGbpKVJvD zVeY(!y&1=VRb(84GOKrToNc$GLBY(oGJAb?c zT!DUE_x_N8Zh7)%ZTuXjNG04Q`>g0ujzOI_Lb!Cmt`eX}9^df!kN<9(KI2yVD_v;bf%nhv8P4x>*_aU~Q{NYc|7KP4jpn(R zj`BVI;HH#G?m2JLpy6(czx=G$rD34C$u+*|&KvI;zV_;*ELMyeIINMajVR2+{N!LS z1u;V-6ScWYR+buq#1}78+#-y(4N1RC6>NSnzac3Gwii)d5iKvGrXtEKqV^&>T10z_ z7)9Z9Pv7`yiJIxQ40)Qd)2@67u!Xue5@T-7Rm{TgVKSd#w1!{8q?wObA?N&7Q%aUA zpSa__*Iu1|&ux zKXG*9XVDk4CXHX#A{PAN(72l&YhHrB#Y=9=d-|cQ;S+Z0YcT}M*nftJb{`1m1*VT7! z7oeyMlY?^GUJEUHYV9`NCko7z*h_f_dBv9{#;IP|Ttv|?`{iK3<^-a^EN8+9ye234 z^Md8U0l`th=HRm6x}ez^ti#7`!QH_l!Q(-5&r$d!>hB~cK)3PdN-!~GOnop>(y2Zc ztOzy+r9=>#sWNEm83~45$~}>E8q&RzYfiw5GshH&sPF^l-Vs4R+ebMB_kr=L-~M*a z-TkXO7xwLaSbCxLa_NP+cdVS_SZKNQ+R;p46onz$Q0chbUx-0Nur4tsKWMe)$no_Gg32q%SwSZ%=n(&4VSG@UJ-4T9;TyV$~;^j zrlzpYd$D-{bEso8_&sjUNN-XMmyt!B1cmPOu<(iL0?c$_kH=?ifAabB8?R}8IF*9B zZ1#lTYPQ8h|2k(P#)BEHS{*!nbZpbo7oZI7cnsLsWBYMXUdT6(YmU&3<(8S{&I>i5| z^H95ows~kB|8pX@z{7q6!#hkkM?AFML+iOUet?GnT+Yj1h%(3d>w_F(xrbUj)a0Q& z4>@^_D2Z3QdDVD3fImFSTiw$Em~qVbN)w{fqtEd684|_b@uz881-xeGAY<~`j%AaB zwJaQq{uL^yybt_U?OuK3BsZUSwTtsvjpm!OjFi)_eqvVIA$l(!`D6vTZOW z33kTf%c>(!ty4wzW0cyi%1%}P*sLnu)x_09>KoM-b-lVrJ)|l};LxDzf6IdEf{>a> zzdWj*P_1T>%r@ELgk!EBWDfBHb;0h?F91zBBdC)c_%XnT0V~Ud{|<&KsY1JNUMfYO z9o8u6Ec%G(~&$3VR~E>24fgq@19*@7Q7!J`VnM_acHu2ZOm+X3?w zf`uAyGq2Wj6X5`b+If*fyhsLEUzuMo4Mw3)yl(zllu{1-QqAXd0esa8aYmKWUC>{Z z3PQd$KjyJmW7e|sqo8?FSI8~+{W9Zy9L{{J;2%>){mZD1-Eu*BW#lZQZ_DUN8NFOa zPnFT)GMZUN6U(S4e`_zJ33&Z3f4z;rZZ4w{Wi+6SB4u={jE?id8Gw~#q&HB;0pv0| zSw^23Rio@pWmL^yIm@U!%H!S3WwgGGP;v^dy1&siJ8$4y-uwpMuMxcO5nkz`G7|Uj zlFQ4ek@X1TU_}{~ml5vY;1rrqCsR`V3fFbC=I@P{?004}WcGql7+Q}?gBMI6Uiv=J z(FsW8)_Ffh>Po6|#USShLD)}IK+YSwC1FU@4J5)ZFu0h zAx+cfid)xC-@d+WsdV+L=PRAM512gs`kQWPeC}OF8P`tV_GlYS1i(YAyYwB_JuHk) z^zyKq6~nf0BLn_IJjAxjo#1bvJVTpyz4A=2$GjUj3XQALQ3zyOn8r3}-Q5ta-;`TVoF z?_a+A=lAsIpT2o@{@O>DE`H###bT~@4Bqhh<|+2+!l*fuof4&WxuG)4i2X!A{KDjp_uB5X?Za<6;oa@wHMRTV%oz@U#z3X zX=va}W->XoVX{L>p?)SZMDceJ9SpBBCOiBxM3Gabtpxo?#Ur!de)H8k=HE16+{^*@I_;?skbOLkD`0u+DgM;t~cj10! zDPHJ`I9wL5A}HXz5w;61TnHrM6pPcrIBky;cyW9d;^2WeCq5uf>*LfCr*b%~$LU0z z4)G7?=7DsX5uPx==GPU*!E$Cj&Fd7pnG0UpxmSl}4$3J;NU=`?`t0lcrr&+9X^TZJYZ@CbiuY=oCiR}QXlK#lv9>2{ zucg`!@O{@Ea?K9#n&%5+61~i(0)I533PQwhlFK@&1yU%4Fs*3FCE36PNC8(T+&_uP zIXc>j_H?4gPSnzg>N*jg>!`wUg~u0wEoQR@Trui3_NHtKj_qNaDBw_cVngH>Q#M#^_R{Si+feSg@$auPGc{3I=f+Cr)mC zMyJw@#)-ej+?hd_e@35ebZz1%XyB|i*se@A$?mXo(fM!_nmOqs<1L4(2WRm`zi|Ie*)r;Ty7u*C)OLw@l0ug0y# zFCNl<|J)1axcjTuUhR_i-gdEVbLY)%6uqi={B9)$k&gzQOw2X>eja z^Ix+;z0NFfl47>n71c3MrD-blf%$8UD#1@Wu2O&-%6?Som`WdWW7-atHZZum)dy8+ zj7laopcbo_szX%eMic+d-n^-Pto}u{Jgk1IiU1nIs-~+f_0ZCK1SL9&BuoqK_7cKCF z>eq9dol9*{n;;M^S51(ENH$BN#pD!3h(c*MJ!WkkHL~pVqD?}cgfhNX#mzm&v$PTPMBB->57^nFfG=sq zngIC{?Ou!1?SXAwLY{(9(BgG^gKjHgOTdyu?hDbJ5KRfum=FyL(ZwOE4pBTro)C#4 zIvJv4A^IRhdqb28(S{IpgdfrYKEUB`)C+vvp}>P38u)dAj_|ZSL|{x;hG-sl2m)g| zI7EFoZyF1cFGO;PPK4-ah&~L_zR>>(zV>J+F+2^QsXSA2rrJ!M9brVBF9whhqCE!7 zjEChRa`A^5FA}zT=Q5C=xiQnsh_U*-fU(hy-~U4i{rK0n@0oPk3*7*W0Cg~=dm$5H z8za1IcfZQwE}O1*Ygg|%res|qUHZ>mTH_^CmTE)o_n7`uCfBrW$tw9meM5Zq)SJ(2 zzMAi4{FGJ!Z;&G;(JDfJM6g&PINJo%Rzp&Dl9_4s1pkM2+GnSfoi^BMrJd&4X^Nf3 zfC6x9VYQuNcJkQ?G~uM3j@s!%JMH-|@O|tA_&EH%?R3njc0I4Qr9-tDFjNc3TGR_@ z)Vq&&Vjge44}aJ3RjNZ5$Jhw~aSnq%iHCTnw%ZAO zoy$c#V@=b)GhqA!7LEnV@SmB518Z!UP5oDkBir7PTy2kulNUPxsfZcQf z^dN=c&3XiKbM2D5Kr+kmqHK%VV*VlN#EtS|xn;NIpha?8%JJ86EElx?PQmHHYbYwt zux_UP)uE$dY;d|SoXkLDS_9d-qy~XG?&@wm$FB!Bs^AwuGUA_k^)JLIeh2mUS^Pxif6iO+yL7{mZW{g5Hg?upeDm3n- zLdW>)CWY`~uu*sqcNX8E&`P6NPld`1H*--}9qD?YW`!mip5_(2ZWnhgzu=(ZaNfi_ z2DOsF)k;UXgL(sZP1g$?=ej1q?^b9V*EO|pO;kdma)nUPshr_rmN}Huh;ha)HwZ@u z^y>INjZ#86S57h0c$NEwjoMY_yMBW4lLUq%_ZT>04Mtx@|3CCLyK0=X5+Y4ER`Fi63EgM6IP2i7!usiB$=fFCcDwRSGS5E1zRoU9v^V3|Hv2*QaeKSnY)@F#dONJgJZ{6a zuPppIg3I@%lOE{S0;V-G*RV+EAd^vQF(T=TzAG-DJ?xSx7ft+ab@QCO>7D0H>M>O; z?G)}@TE1*>xQiqI+6T@r7TViEJNcIj)+&4uh3~Z&X(#9?bm_D=(av}wnDecIDPcCP z8DVysB&4o^+u|IakE+p1gRFG1k&ns)H%cQP)gdeG<@u-{v(kMAyq=HhEchG0?~uSM zWu=W)dXOiun#L1ep@BYD(i2z(tYos%k5>AC*UlU?b&dTzD^0Nys!wni&KP&36s&|) zSI2m6EFE$suUCg0%olDH=`85X<^-rv&ceWWSSqNau6qQ6Aa9b_k9-(5o^eWi zrq!X$W*Mj0THn+DrSuE@L2`s*K`253lxmNW90=amB~rIqB!WTAr@xF~+C=bUFc1{O zZyh-B)}6~zkr%VN7j>ta^AoNe-d*?8%rRAoJ~h`Z^iU2_8|ArIuXb)+oHe|rz|s1a zBft7)$MPLno8;SF{YQQIhj9Zv*f%|(?{9z~bCIwpF}T1k`@B)Ffb0qmi$yL%lwxs7 zuGL{3n2fM6&uz?~xy~`s(d<}li_WiiBqFhThoivd3QmR*&#V;41yhnm zEWSN$Ow^Sph;L;%ly1wYATUeJ2?@tYTG!7EaIhNZ<1D$5ppXE~IBnt~eFK@dvIpn36-rsls+XY+w6EB<8#Ij3WdnJ9nHHT^+d~{Wp^)r^wrqJvAXWY^~ z(SPzR?da^gZhxtCJDdC4gfHR0G@0i-x;T*^&6b1$w>?Y9I73h^&*BMiVUVE!+OKOAYY_K+HMrsUm&|Ja{FXock;##gc z(+%DWuNpJ%H;6$Bw;f)yBz_Ha>aVZJWfW7fl*-;|+rv zMvrftdJjI2zu~He@#Du%S~B(V$EHo)@HoRAYS*xK(||hxXb%U7CX3VV4+KI!E20)H zZq;fH1-%|m5P5N&a5b?z>;sc>IbS#|S!w5b!Qpbm9P8nW?h~A@Z7$Jr!bL8Z#qW`r z+b2&+0q>lus`nv>R5B)ntBsfsjjm8<GG{($;5CWwGxkJy z;t;1%oVkv5XSu1~zUK_e!?bA5Sfj?5#iO^FO(Lunf%% za)Bj;)rnE@U{FTlcWc+KWw|*yU3~5yW$vWQE@n3mC>vo{S`|l0{-Qp17ix|3ow~ zG#4Bm#``Ot`6%*eH$uOIyWvw}x`3sx*xaq-ND_C7UpeB;E! z^yT91x6JN2arW(Cm*zM79vpC4#iiAGg;!4-_?rhq6|+aI+&q7A-ta-g26ex(M^WcB zP05wHvAc&XUfud`{kT%oguz`e@0Ks#!01kA`E_x)GLzfN2-}iRX zAFU3+ftX3F2PS^LwI_};XM;!mx9YE9vcabF#H z_w(Q9_wDm2oBOc17dr?ZokZ!Rf`2Bbija`Sn|l35|AzdkEw%c{?U z6N4u^F2s)|b8`hD#x75=f<;5hq+?0X7kkQSc{#O|Q&TxLmQzDHCCUwdi?qeq zz(ukNI~Q|twXjre;QI1h|eUE=^AO&mxmF ztt^4-DLhCLnH`e++mI46VRddxf)&yE;mk7X^TDXIp6swgik)ua&dt-NZGLk4^e1n* zv48&?$6wxWoIK<9jhm)U-L&!cE5=Re*KfjjwudjmLdWIT(XJDIinya&m{67$3bXR^8ZlZofWFJ$m=QypQFkc{6Xlf6?%3J0rI$2@$bGoP&N93G)&I zLfP3Sr9k!M(yZv^%;y%kJ_F`gu>1|BB= zE&G}{XZ5|>SFr`24eMXMuzSO`{VwY>*mRG&wt3FVA;U*ZkRnrBzE&5kTzE_UMZNl7 zdXeLn>9=3s|5l%91$to2^>D~O!Tq`O6Fvyh|J|3H`A2ziuGW8gP}?Ct>!Z9J?HgXl z68IY20vS9Pk*JkHo6T*{6-9!EFE3~cn;MegfDgWyKE$jbXX?=y9g5MO7;TSHON?cE z(D@d|M5h%aXBac}l?HO@QbGEI;f2jv7x$9qZ@RtZzEu4D?3{}#MvPLV39nspOP)4V z`MPb;qQzUG>}x%AMPJKo-*sd5mKJL)e_(_5lk@}R0Vm5JsF<80wuKEl0s(T!;w_nQ z;8)eXR|y~SG^x^;1gOsb={qvtFHLFs^tMfV-*`r>QX)TpZYuowb7|L$TQ=_=%XAh0 zrd_l{T;(=d23-Ib;TMEa!bEt;OCxJAZRo@8(A9m7PadIvq71fQmka4J!Qnx$v?p9l zmEv-VS)zGKGA1^NjmROjUECvLtMY6JU{cG=!s-0oX{2EkL-D~MgDI(NstXIuwN)%9 z%-7m?-SfK^-9Nu9>!O=VuDh=7dHH(Nz$?WaW2Aw4UmCC{XJI~Uh>DI`EfzCUSh1vv zg5px(Z?CZ2ihlx!Pscj!N|sxZi*}q*=E5EKqH~*TYB68_hQsfCF!K|!>L&5#Mbj6y z<=n*D60y$~i-*BiFu$4066YI!Gm;N=OEIZI0_T8lYu2%(^^4<%`wVOW3{yIA3&wGG zCY_9RbXpe=Z{N1*+2@|35f6+Qck{I)ZkTwj?0Mzg{kvY>uidd~?mZ8#SPCyb^y#eg zU9vdbGO562L|}P6ELqI)5m`>9T$!je>s0Za=X)W`c~ynTVB>@M{w5lvZQn?PT0h^y zK5e1_OLLG4Yf7HBv`)J}^AY%RLHnOfdzIO)Vqxar1Xqb!6Mq&2m)T;HcD5a4@8KIa z_3ZaM>|I_Hl;L)mHbWI(XLBR%MyfS*Y{`Kv0d> z%tfxQftUR{SC`iM@sW01Ci!~he@)dTzYfh2#)>Va3^JYY6eH#zREU4=Cd?GW8= zrC9VJeM`#Ym(4|G?izu8pQ}tBeTFi=ghOOy+*i1LMuCp-TBeN{H3B|>jQbuB3IqkD zsm`=u^vP_>z!%bWX?WhLuoy1>HN5G$4159I4ES)uDe+NA%<0{$m*9h_G1@SlGRlZ+ zXd(RU$`Uz}SS(3muv?J&2I=bHb_;a4q~H*s3&gT}b0 z=!L(5?6#_^PqCZfB@8cEe?U(*pvYDSayeL$#Rxf$6c^Ip@}yQCK|P)NByFByCt!!9 zlbj=j)XEdDLtbSHBj$$4a&j>}iT+K6W~BAp-}a|B-x9aVFSc35m)rVEvDW;DqS{CF zg`S!#o%BfluC8qk=3$(670X|Rd4S#!`{fRTHb`*1K)F3~!!9ddcedup;UQ}rPtHr@ zNgd!qJCIyKDQ0HrEh*I{*v`O{v&-bs5tIR*5G&(2&*3x*=y;;TGd$7Th|%qO8>$L; z;NtO=$6*vnJJ%g@GR#Ub-7A5SNY$7Ib# zXd*^~#3aR}>}-E6rC0>f7!BazM=y5k0H1fMJ`x5m_k~U`4Gi3Bhw)n+XJX^f>yF_8 zupL>Y0BjTB2%V{!?XiPao`b@&2!xB+`b-Ww54{uXc^tS{;AGa3Z9DW0=S1*bST zQRdGIW|_RfAR>oN_MB)yWjN(^3KWB!85He7WbE*WNOZz7Zl7slF=(}+g5=nRqY5l* zOmOQNz6y)<{9X*zD!X~IR_WQj^h{r?SYS(6O;k2N_-@en76aNJ(ZWE7c&-{#c+Hn z=Rv=07vSqC+mM<}5NsCY>xNF#jO3EHb&wdbaIk+^DTwS6R=St2*Vd1t>*%`i+GEr> z7OA7xP(S3FZM;!iN2A7T8?^P~X|%Q${g8yq+TT@9DlfqoFpATlZ=x_1%kld?LXO2Q z*o%wug5d&|0mhvr%ShB`mc21}!7{x(PXJ@MG;%y*Y&xe=dfJ>4b75^U0uu9~)2=cH zr0Nm^6DE)^NYJ^Z6X^W7Y5K0Wx3c^%|9Nu8p8Ze1etlcdJ5orX)9SoIeErVnkWfcY zrPH+HUwf!OO8|50ec$iWUOl?J^{ElmnY@2~K)Pm__6LSzJkJHk8E^*tTw}h}*x^1O zP|Uxx{ckCMNOqoqGiR5{qkp0dqgSkqugDiP3g|ea!!w-G+lbL`0nc{2-;V;&P5O)i z9A|WRw*ESN0X+hEPODh2L<;R}*HWQ?Ug$Kf>WsZ!%E!`+k0p-%1N{WYVn9nrm8kD9x0S-Cj%?ca8NdE0=3+CddNspP>Y zx^BohZ^(ee*dE>H4Y_bk=Ze9VSB_l&SSyrTU;pLN1j~HB;sh;fD_W>anx^XsXAh=PWbRC?iY|zC{^M zyVz(Xv1VucJE@u~z1V5o|My zfHPK?L(ZIuIS|uwa$+&K!N*+A3IuNkYzUJ%Zl)HHP_qqpZ6HXtLqOjdfzX){zYJHF z379Y#b!;L-GXYN(;9YG{%#TUfL}w{w5iAYU9wIx7^x$ZogBDkIFvGv}w)o(ZbeN~M zd+dM@?t60cci*3wIODF{R0K}n`^#^p9iz`%8(yEk^x`oi$38q%{1IWG&G#b=)cyP} zJr0zXOnv&nt=na}=RFe}u0CywMTlh04gH3WPOcuZ@2Ry=rFQ@e`A9b;|4 z%y8Zho0Ic?=jDb~og+8!IK=U|K8?qp0}ePA4g<#`JKK+?BDoItukg5InQ7F7GVJ_i zWx|8QIYxogcpOp~9`iPU#|$T#j1Y;SK+F;g8wEHmWq1`*7(VKKnjriHc+T^)UP)rE zoEz0k=tza}BI1>v!kN!jO>dQ`tnwtKx!6Cx8V4M_XNJWXR}TYcPKZSRA7SL`19AUjkD zCa{S`aLhu~n2^3`hMxlx_BB(=;*exzmYHEuSQ4uV1;wGdV z#N~91>=3*C?WdoPFTC!SS=!-0-ZKC94a2FD4nMW^=Uz+KtX;8u#iI}SNc{Int>>qc z=1;nA=wa;2Cb~W0)ZP z0(g$wtN^%0Ig+iHNE1v6y6hv+Wlsechl_HMSLJ-kZio3^HpwQ3YO!uj3dlYzs**)c zu%zfDY)uZJbtIAk;|L7%XCy(#l4leUP}y+JHUIs^rnZ-*uiIYQB<*_P1?{o>?$erI zcmaLGo#5?%lvDDx(AR|#g)}R1RUkKlsGJDIop@o6KNNEy_{iySNudTL6%HV>Xgv=< zIvOWE^k}>Bq=n}PXhcj>oD%VLd=itpGhfU@=3HE-As6#7G0t+;@i= zahxW3*e~C`K5fjCJFlic@1H#4$!7+>8q{+7EPQNn+wVg3)g?6xEC1rSHq4}-@BhGDfXC#h3AyASkV~@FGjQ$f zGSg@%$}pzG6i77$O_&4MXz1Zoqn)_?>@m$OI&NPao`v$Bds6@h6k3Jam4SePz7DosE&L`v} zds|*q_IiDOyU$)w=#9uui>iVOsR0XC9F}BuVN*u%)Pv<2WnuroZ$jEM@@gP%EFCk) z@2g@g8f1lTi&C;6=M^+6U824p5T26Y)h>*iN>EmMap^s|yE5>;?;m-97F zO0Pdg2T;FG`_VwGIA&<5pc}zD>-Dp}!t2LgVRBODtcclS23MoQ>-GQ3s9#(y7EsaY z{+EQK=j#7~O#Q2=lQCY_e|I?gKldNEkfr;Bd*6yFS{yAf8aQYF7YZL|;L$+T6)i9t z;P9K64v6cSO4xq2=F?cf1K+qb8Q9tWR4Ncvetp)pD+i!Qzl@iON-qih1#iMGmI-3n z)n#Q)T_&z$TO4zm)-%B(dmkR?alN--&*vg4a&n?`p379^hMLnKGzH5#h5bS9B<6Fv z@?7OEDTqsAJU}&zWH}E+fH5Objn*R*3?gf#0D>czAvhAD$eb|fVqK`NJNI^A%5gmf zfzDYo7cuZ4XC{JS3C(dn@j%1>B_WLVA$3a4oA=+-!aOzi{`=>)mY$UvhRp%y8M;qK z2hL-220CyD=s*YE=RZ@>#hj`8WcxIvFj`Qrm&x$6I@64-GH39!dK-)m@HRjP7V@>! z^{dXE?cb)H!E?63Xo23wDkR<2`4HYlj+j$;jy4#+^ESlQv>*5$cwSe>vg68;){_cl z>HUN;FnrhRUrnHEoG;=$yckY1*nsA^vPrv!A7A4I z!XMBEqumu1-~(%fnsRrv1H4{8TVuWcBKlE>r@TDWUr#NtlN`TFa*AE9g zC=i6o3(iqLTVGy3=vhsgp0VolO7ilJ>h<-~`_I-_uYWZ`)5O=8RbMWYcQvZl;ctYk z0QVOd^;^T@Re*=Bps-7!F*9tx@On)#&=_8?HHV<@Vsl?x%j#u33+|oE%#4mZpk*;W z_hsq1XE}`6-0RAJ!Q+rKI}B~y3#Risb8qyezYZ^a4-Z4&L^v#BtXz?r?J*g%kGgc7 z`HQgCc;ROw|}aVi4}Rs+C=7An@B$_*J>!X&C*g;@-V*_-DFMo`95bJNU0N zJy51Ii#hTj>27(9jIo?@^4K=oy=v8NvHhioAAYHg*sTUKt@;bTR-I~>I)BL0bwj^q zV{-sn90MgT%*l4UajUO6?2+;<1&M-&g2nF9gOm>ME)PxPIh9U=5xI|&Q{oE3J>a7zzs91sAivOJJI2} ze5lLaj{rWJ@W6C}6BAYtL{_E?-3IPhKlicVVpmSjSsS3|?Vsv=UE2I3;`*tKg$B6B z;W;hW+gSXm(I>P4H?t1V441k6=VlYz=lU3mzA@mj4RMzT0yIzKr2((7eXiHP>IVZJ z^_!8B_)K`=TJ0KKio)nDuU}k?4q_K#w5lYrn$hPxz|7*b`wBLX;#$m$J7EucNpuTt zxN1mBB+y2-E@t6MdJ}UR zX19b}CR)g4sj!I16e`fjni_6uMqTWFkoP!k1bqgd7o%E~VYHFi&ndVoh^xCZ7$vqp zj99F4Sk5+ZXEyMZv=gCW`- zqGchf3$f_ZJRV&-@py<1@ki|;+7_a99AYBR*OD;8VviVcu**X#3R)4jdSFIPu!=t>(Jsp74GtSllU&aD;h&k6^=IxbMu3h= z{0trdc#}2Hdli?SWSqcq|1obQs56SVcidN*uu~RD)1pkJhAe}5g^rf(Jyx&TiYr;bJ(MPV z1DsUuT<1LEl$x9^%tuHd%T9`&Zj8GK-Nx6P&C%&L!9#!4wv16<(x`8vcO#NI=K;;P zMhsu`$Ti~rwkKAy_8~RkJYl)oE=N9Qv)wMk!0b@L4fd+OjANii%hYWX6#r)jRdad8=UUbJDsXOl!bNf8f2jRuT>epS?c1Lkm^a;IRg9C2380veLD4oLd50$oE_x;dNtw0=wC|<_UR`bhW4Z%w!S0&V0HbaNs++ z>jF0ZFDX~R=yqBkr}vx5BDxNHA$V0@hU*D)m@JZ0XmnjK!(*}<`1vvLdf=-0&+W}9 z<4;jsB6t^kl8VP|IzP;?n}jED-c&0OKvQ*LB4*3ahAPg-?&&Ma&Jl8Obxe*Nkh~st znUoCvmxsw9S)SoyYxtWp9lw6}E)T$lrWEQo@Td|?p2Y5m^dJK&Vp5+OwV`M8@Clb+ zQ@r-s+YimyE$=kT_2hi~$3I=U#n-RjhJCF2g)rH_lFu0yOh{4EJ#F7ITEbkCBmh#@~cFwWZrA;QLH(V;#X;N-x9B z55XIQ=|;d&b4NIKx6+U1jmi?xvKaG;%vA&%t^{{EZ~x;SM}y1v#v`mWEyamC-}IDT40YeS$}0}*7K+FooG1y@%RtM zP~sm3uKVyZB{;V#mzeU?`GJGo@|h)BPe#aYOeWosEO*JI3)cQQ<4*0{?@qOSPuWxN zA{qAMx<%_AyPwYAuv{{ql|}f47g)wT3R3p7&%$b4^4Xz}KGv>2M2U1tVc-UM1Z5nj z*}xT?p?Qq21&<&G;lL7PZco9moMuKDkFWse;6{#1*D!ctn!tRwn2y*7;l|g`7nIVj zUOMhEnmvxwY`y+4@~$(Q&CJYSrXs;~-5gJ6Yge%xaKLR=w+Z?PPTgVTyDk;-rR&Y~ znbB;$&WxTk$T|%ne{(83&**fTW!_)eYBKv$t_nbCmWfAD9xhbq(m zJ7^=|asE}FS83GD`;BuFIq{5og*&*v3E;||_eWz4!SKi&h`_MoAaA&oQ$_U0N&Q~92(k53sTws^z7B(?B(@C z59SnLCx%xl{KKq{EKvbTb*P}sxHy|#zhSj1D!cO3O)fVws_70`jOpaMEgr3=N2-BJ z!Rd;#oIGHg#n<8XDazLVx=Z^z1@&8~aM>yg%U(tMjJv3e+=EeJY9e8ZcvRIMj6}l7 zd**f8?K!#5T(S$f*Dnh1#LHY`wT&AJCTK8$2sa}JcwTZV(y;+{4wm((0!k6GuC|*86TpLp@_D2 z90Ppx^Ul!*<3sc|R$-|)j`B8gnNXJ-KRpVL*ZMp$ovl8OHMCoJSExypK;wbz@lC=1 zN7{G5M^Uct&Nnl=n=RSiH`(lF(*p!j2qloENKxq^grY&PfG8b7q>Iu7L==#YAVo#g zC@4xfHn1yp&tI_}>rsz-jtbeyeco?oHyhM*@BepI6vOP!SKs=)&%4m~qfh5UEBN3w zK~@lKAM6#Bh6JZWuB%8&L;*PX9N`?< z+70sZUT%|g;p56-JMbCOy)-V<#U_x5Yf%e+4u`34A|v6TmPQ?W^hhs^3fN$-ftJ z9%qHG4PQ`;!vZpR^^5u1sN<%!RKIt*um#Wa^~bm^N1A?VusD-Lr}c-dr#OTIJs>iL zaq(h61sJ--{9t3R+t_Q=ZuO8&TtnSfr`SZBjh(gqXcO^sY~_i#H#fgeE$_sNKSAID zEVmQ}AQ%V9R2kI8FwxnM`TyaxE*oYuiAT46{FR(96S?$fcK6gT_1 zPN!_h%tUFK7gC+UDL4_&3dj1`e9S8u;RhOxvMyQsls-qkiCW5xGG=L-noo^8d+BKO z9~9-Y#5U=0B7<%Cj_C-GzK|{3y@hQ(`YE+{@AKw~Ig1p?oaTN(0r+x#;`w%6Bx11z zgBWu`p-YD}UN(7dnf4)dylW7So2Z!-&CX~t%BGHsR=mX#qTwYES9+agdUGK0tHjIy zwLj2y<+lqP1mPTslp9)~CwUd+pQ2Bs+A5`8#VZmYC9fhAMj<|SH8=)*LZ^6^$L$6WsnO;ScrrX0 zP6*8_>YOeMf@2m=G?0=ZD$$L05CJ|Zr?5jTA{8PQ7U3ZX-Xd3g;%g=0EW%wCGEg^UIcN#9pL z5GQj$yePwHjDNxizGOvrZ5ggS_xjt)>3i8EOv1Hi`AzM+cZl@v z`dGX7<#Wmz<(!gGo@4o#$HT1eCu|U#ztp#4n{BJJcfYS|<}Oy}L)e#9Itj5z%m<;8 z9KSOxWQH>f@;w0Hd+bi2hH~^dCc7OlAiG`dTBQYlcxea3++WOxi|6#lRcKG1)Hm4- za{;#Vf*w|PPc$x>RaTi-+P(TOdRg_EbBpC}d+&a4QcEl7Wh-R%Rq!p7m?y|^y$|xu z1C3P^4||1>4(D7aNYMyl^3EVS>eSacB^|<9$aM|4RZc1M6@8OwRNbg z)BDtlx};oItL#o_)peaVe>-1%jcV%BB{ItFsJ`y}!&F=+3WFdgHfpjM)#`eZ!wv_U z8+d&Z1cfA-3?@4j0TGPB(6Sh?`S_58!}YAps{lr!3cqsmhueL zL?yl$eDal>_LR$V;$`tG{Xw*6tGIWrGVSR(%Qmc7vFWzi!25yHcjUDry0MJy)D+CP zFlSty*!GhbUwr56o6phsfo0)+ES%W;uIS4SM_33L9Ii}5W+duF?9UmpWkKS>Y$1u) zJn-a*Nz#!r!;qsy>CdWYiF#d-3Cyf00r}JMbJ=yri-^6yaH6rQDFEVw)wUe}t&=u1zABYBu9I7D zVc(MHer^HQy2=$O%4)m3FTqso2 z)XW@fl?&oJdb?rWaDarLAAY9=3eecyv!7R{P*Rnz)eOBb$Y_I^+90?*F#IGcB1 zzn$3cFvW3fHe$PXG3)_DS8F@egqdW z@*s`gYBjlB;-ESkKn4%FB$r7j<4p)tW_oh*=z2gf5+>iqDhx3x=89EUfVl=zUD{pI zxhC!ko(a6IG$P!Yxu{qJHi<V>zaFVyN>2g8Hw3yK0sU8|tf|E?2)xxB7&PfB{{7^4n{07`FF~_s_-Pf>xzqsRe zaeHFy?TLDRhAFpFtGfd}V;>a0m|PM%%t>IAa5@FF|MYl4W?*;84)sRM0y&2&EfDvW zpk0I+v8lX zj2Hiw*wXl!x;DVAe1I_xi7Zx{*9o{vU7C7piE)fd+90I>@5pwB+OzRtc3&uWqNZ%TN1ObJI?BXy zj1RY;?P<9emQ30U>d8ZUK>>^Zz8AMlnbL6m^%v{~Y(R;yl`dd#qDGU$Zr7uHPBfb& zw?}VsqJfBvBwwZrDqk|;9TjRFH5MJSOKMZT+YlTErna2NOo_#{u)?k|g^GKVg`aax#t9-uG`C75tl&jlZmsl*l_;in17rk<4M;s5#74cL3BFKO! za3Xm*et*a%+Kqa>JtW0)!%i_r4mi+X58@t-i{3P~g)JcTJa5;Sl4x`yb-5sxjRe5N z3erp5e14N40{BmSF-QdY{?|v(epwm!RNHYQZk+t|_#1zH+H}BBez$w?UiO`|zww%d z_w>GJgLJyF_r2R#59#pvtJe)%H)t3iZ`12wnTlY%UZGvwpCQWzfZ4qUuis~br)m?8 z8BW;H0-#GcC`prm?gzI$D;O?4t_=0JyA>AFSd z`#d{Bf@LBf8oT%WJ8L(XcUiac^;)J}ru9v$h=WHBozpT+K@*w^c}=OtX&wm5G#~^8 z9g@5Dzv_ghTqckB6CA<$rSK+7set5bGKX2gWdvK-?Z+R`FUE*JOn_=2fK3!u%UytoXvs0z}CV7OibTvknQX-o_6 zE6?3WJPW_DOZ$wtXv8zq=LMfpe*X~!3(Wja5H6@R3x`<0kBDU9-Dg`(?7Mg3+{0Za z-iV{zN_h|3>R077l5)HrL?>{@mNT2_ILy52LwetJx~qY-$adgrI<= z341a}a3oU>Hs-+Bo$DvN$n*HrKdikr4O^P2++NOB8@|2$MfR;Do|!%6=%2NHJ9Vnt zJ$olEI?=XwbMrT>y)r_7Dc0VF3KqmK0tO+ByB#)2IT44`iaVWYmdv<3W?6z31y^O7 z-Gr7ZNyanvkn&=bBvYvmv5c6bj1yXLBcWP;{QBcJzO9_vb06#U*1a#WZKVyLHjH2X zopO#Dzg%(Eu*#}O*rosTch>uX-tpf~>#rAeZIri^-<98#Q%KRNzQ?mzTL<_!y2o=p zM)bOIID$qgJIrigKeOu1X0+up+x1jb3AYrYuH|r3l87%Wh6e{J*_jQ)WvWS9UM&^K zo~Ju*8bn?62H#b|dX^?`qDFf04y+QFxVCYBVJl=)&`hs1r`tKFZW?vd^Ga>V_7X`3 z>+a(BJe$U8b%oGQ&j*$=mlVzk8DMESLJp%hX!e@x>ktAY_1H3^*-Uszc2>EJgcfI? zNcWVPy_h+o5otGqRLc3YV4?78;Y(?*a)gK|fc#zb(Tk_POMIB_AYaBDiMRMT)q2Gb zu$sk$w96LqJQ%3aAxNbqvKQ6tC{YdCGhK)QG9w5N|0)KUlA#!Ds#uW+zH^udmaNC| z-`f1GThqQUzeCNuzwGpy@uAZ1#s(t8!A;18Z#){6OpF3pShG%5dkP z>}0bX44~sW5P}8>SaPdn+)XE!aQv>N6YkP|!w-o7xgGeYsS<&6eUQiimvsTwAAl11 z8fBicw6|0-pTeTKkwZN(#ktG>`_jN-z$T3i|%>**w%Nl8XM(1<}RWbArr2I zpVf#vRsw8ScD~UQrj~rIO7o0iXHik)in<~fpg}kaPVx(RnHTq`z{#AlU@#_EWmmDf zD=N+rb1;C-vLb?G0meV-DIbNqT#cAkG?I5^*V)ImY`YE3dgEJKVJ{{_%~AqR00=`&hr3!+I7K_r0dy z&)pZ#&;RuG*D(G8h*>;?zJK{N{w%8y%>V;uL182#%Mpt~hQ+)@#W}DJxBS149}Jur zKX+*{eqtN}|0azetsTG*ky|ie(7H_p9BvdvUCxUnaq1-AVB(i4?CW z^Z@?*cir1KA$f$Z%P}|uut_Zd17>*#)n#SbB%5hSoh>Bg4yuz_vv1oX62Nz$*~(T) zC9AjqX~9C@WAf#<-qN{K`^t;DExg^!hjx31bkYk@wDtzl=AR*GUJGUTw&DqC|Rs%`B@*%8*mU=y(%$;^aG z(?DKR((vamPqkod5$7eZgW^opS~JE~w{~yep-pXv^p=b_Y`n+1&)xTuiOJ54z>2`{ zRM(+2?ltM`BJ5qKD-)3kpt9ip- zH_uy8m>o8Iz(SASndYz*KuxbLioRx1Z-8e@RrA{iTL(*&;&0Wg9qqU@+u!MfMMqb1Yc5iwY-n1T-%9XLnSM8m+{bZSPUyE*)G>73$-|Ftg8pjZcEeLrm`mkT` zkJ^mD3FqheEx?Y#G#jC|J5i4w!i_I` zrC5mn#eMWTJ$6A%C~qix-hJbq_P>bz9zFW~dmnuNWP{l6m-hF({+_bO10-g_X*P7a zdCS==l+TWQr6^w=X4&;;x0t6Y_rIuopgat@#{C~%0XT?5LhHB>^kbQR5DI5|5YF~M z(WM`a^gBTZ0zo-gY&0?_4xq%5Cpe_egIX-DuO48h z96U|ihibm0oWM#z@#B)J!ZOvbjdc|fbfYE+1~g9jL*q7966rT?(Beshd-fSyq4XSc z^uiWy(ulf=ZO1I^y8QgmAD*Ofc7Z(K*pJY z40g&0$blNZh5f4C7S-H>n8%QT=)tif&P)1*)@5Bq6C2X_gY=EEldVxq$|Py|`E6{6 zGDd2{4sThhbXc1BdCM9RnRc)i!>2!tJM18RlOgIcB%7p%Bk!c)AYh;lXJDPxqc)oR z%i)ly`LofKv6ak{xKrw&`~<`K3grv&XyaU#rF<$*V7*o@SB@-I_O4vcVKa1&;52T< z`UHW)D?t3#YXpsmh_J0u$_nWr_8fXW&KBa(nP_AQvrbBvwG>M2NPEgvAUPylmcW#w z6#`j~8a^+q2IUmm3FnnEwu}AJ_@Q)8q5aKXqnx|-V3yJoS#W3MWSfA&(vmek0h20n+~A|H9<*&L_iw=+Fhwz?_ZS0aSu&p zfmyqTDBrxMG{NJ{XL`jToooD%{UT2M^1YR(Cox-}$M0j^nEO1d$Y$T{+@iqV#aPDx zKb0qUhOLiS35tRZ-hgO@wJdt!Jq6@Jbyip#p%5F0jzmr~(l{+Vy+2u3xIbzLAHyXe zHhEHNtRDlGeo0)y77X5UY|!#SD>phH@io1B`+GCr0|Z@~E*(_ntv#}F^!-=PUTisf zV$tU-PbLEF`bX3??TTHCB;6}>LN>q(;2h+L5XiG zlB5E-(HJhboM~-aEG`Z~>MZCeKuv>8alzxp(CMIA-<5g9{p;V} z{$lR!%6HvX&Kn;TKW-eAKWXXh%0Th^i&hPov2fm0$Np2NpIEnkS*IyeX4Gz4F#PUi zutezW)WLr#z&Q^IJ>z+xQuRAr2E86`Vs@6#MFk9gfLILJ4L8!M^>vb#LAdaktNQ@C z??(IuRWn_QwJ;_P(IxIfMij+w}0uQH=cU*s~?G7lRS9enAMtNWCP zPQ9V^L|-^Dug`V8_AFd-%cD<7hp!vjv$Zp~q`b%A>WzDOL-c_o+SQiixJtJ!pSOgK z>aPrkeMekIm&qZyA8O%(LWg*k&!p2~$Mr%e%WOr@B7lQDRu?pb9o5@uQ5K9jiBrP) zq!^rz9BHl9JVI}9UOkSkq8z$Qc~SYkVV_BEwRg&MXO1-qYu29na*8-*xANn=l3{ z+u#y#{E=h=%?q-Mb}W}&%yf!`!&9Sqs$Z7wovHpAUPTHMFQk)Czci@$G`8v>Tdho^ zUk8;LY?U%Y*0bl8PMegMH!1aO$0k+@ANL8Y#R87WK(+<{k6ejqqM;i2+CpMz7rN+P z5C}`XT>mp0FPIu6qSVmp{qd}EtnN(e*)geS`(WMhETM(9XQ}2#;m`Kw-#bPguYRxm zh5Bsk)~RRr;n^=a#ut_a)p*OO(W9{`!}Sr4#aDmcPyUSm9PuZ~nXC!L^ZnS{>hn$C z8BtS;@u8-WzK6!Aeh(rzRIjT3yr1^-Ww8Ht@}C3kLEl4T#Pe7gaJdtnG>^s%Jn1-$ z8CYADXB}Kp!9U@WB0OnNb&i~)20R^{I}}_}9}<^TD2YI&cQn0&Gd79819ecSxryQj zgy!Yk)&}7I7-t|zVuqWR-alVnqS}>&o%h6b@udi zQnBeAU#()sm!n?`$Lez4HjL{hj4Rk&wNLw};^RwG?-?q7kI#hT4j|$Q%8ma7%mJVJ zCXEc7W5o$$qrVQH!@F=7Cel7qt)lR6hV};Wd}(_Dvlm1P+7`qM;LaD3{M+_whke4DEb#;_~SbC z5T^X^#sE1_=huh4fD;x#w>a>Z0gKDvusA|NCsdRZ6)`q|k!)T)x~-!$re%~;jnky( zxio_cZURtAVl3jQ;>~BI zE2u;;DT54jhE*9fRD>3zGb~GB$J7rkP6#tNou6UIB7TN{($4UYSf6Oz zE7#U+uMzDvZENB+Yir~hiL)q^7^o_vQ~D?G9wAcPfb(Wjp$xh@=zabR`aoRpUSRv% zG#%o1=kMx!r3KZjwt9QDXs>Qt9j}(EMdB@{_u{ov`Fs7;1A^D*EK|oUeT4cP$RMwm z7e%rj4R~)$u5r$9%sFMCo#=fc?k-7EsY@5!$F8BSwa&tnL{afA5!loP!o+w-mY?s- zKuey2u)|Scl8OqApvbGQ1Bn8>=m7$=$)GEOI*>EnZW!5qzgx}(jww?!RH>63>_9@3 zbG}ztUl+RCsxL{-LKSqjC+2B}x+GCe%}5yOQns5gE(;f?qg=jWc>^AA0fQOM9=~pf z^6$lU%@~($2ivCuU>sNjHScvT;_t;md(;PXXy=tJB~nQiJcYbG$tzWNkcC3thBVLO zin}UNDiAkX`v4squ?5-;tP9wAd-$|yF)VY9;FPx?O^eF#BnY=n&^=Ex(W&2PbybWU zJaE=ipvYH&e$y2&Ryk7umTh2$88_q2f{K+$d=P%?VTb|?_=X7M?8~>sq zoiR+cF$iU@nw+RZl7}tX|PnM4$VFfGU!&g@0G*BC{4QocY+Nr(gamp+A0HJa?R} zxO4HM^#t*H`1O|`VX*{&B8dlRvnYo;T;2p(YK5=YDUJd(_lx8|1gRY#N&*P{ zX#ju-=#W(SJLNv{@E^QA!9XMM@Z=uvKuB~M?FFa2=@VtiTgQ*o9-I8}_s^`op&>hxnbv23R0H`6hOV$7LWn)J z5%$F=5R^_OA!QLGEH#0~yb}625Z6gz ziiF&U5S@fRYaX4nt#cbO`f!X@5!bsx64mtjq5OS!9r!fza45Z*{+&_tZ#Ym-sNQ%OP8=t_Q!k& z`F?TxfpZ{ny9NCX+)cyyy&1i)@o@8GQWM56+np$Up_B5XZ@B6~*C-2c@1UMD>^ZAgm`F-$HtdWe*qvypp#4FiEf%lFe;VKaY!;yx7 zH{CR&yTI3$zXxkepJQk79{#x+nu*x)NYl*?9owres_L@P=hXM*rQUZ&dnAR7oDk}z>PmwmC8R~pQ zoOQf`xsNpMYH&LkSDVe_qWPrtM8JH=@8t8z&!_o-q4DX4+#CyE?dH#^@1yys?>mFq z`iU4H!^^0=F45q#sqaeXZKe72;p00acEWpT{&gSsGCpVY&2R`!AhMX+ba!9R%_CA`A`o1$# zM4bU++JICp%1@D?%GlzTzH0qno-T0S4#Cw_k>CFhfyOY_0F zuEMxUo1DCpPf?QQvcht=0AiZ(%tZIX?9b+IiFB9CUv=X!ChQoe!cAh}!XS zy@qjVPBi!NXn!V9oP*D2gf^dVlJhAjp!r~5AH}|^VU^T6Xz!!>sP8)?{;ci~MS<=M1QJNggn~gcVS@rvUv?n&s`7mfR`e4m*qIQSV(IDDR-LoY43? zw&YI1bAI)?GP$fuGe_yU3%E`2-0kXf7Qy1wsiKRXBW@GmXJGTGoF74fjEJ4_6}BiF zA=;uj(XyyCPyKIgbU<`+bV2lZ^tI^6(I28sQH%NqSMVRNiEh{a$eetho_DPb*(5L)ax1g))D*bWj zNXJu?Dh#}hvQt^{=;%I~NcjjT4^;cbWb=JYqguCY)KRT^Ee&99bol%?Hj<{l0 z@7^=BukE(J=8Ze9zv{A4(JSY6*!S4B1;yKOUg%z@b1X;MT{tgkzFy-W{kGL)TXJ7d zjVa z+o$7!5S- z1H{#(T&A0WyhRk~jE=%J@#;3-O1r%%D>IU-*M}osxpSwih(mG(gVop<)K3fGAIm7E z))cOGh#(tRKSOUP+B1qB0O^l1A*R7$ zfU3}|5M~4aiBwg-=cZRqvek2Umy2S9*sW&asy=fseqzAZ8|F+(d@%dAO)FMxST;v` zzGJ6?u94ndDr*m3JCeDXUu-|}$~o-hdDnK|Z8r{?+V$yUGxyhjz@~im>{}ndd+K!K zu(sW@-2*T02tS87m~MohQwY6udAuCSESD^WeZDsKJV`1t*-aIddRMeGT3TNhMCDsF z7!875qg_1(iVEvfD3(;P2swNT8#UiO4msd;CTbOt0llvXx|^E^!@RmmO^0>C;`|SPTRs1doys@=Nhm)YxMlgGFaExK$)45gw%xIUbvv?i%j1viWrKBh>z>L1F+#giA#UD>zqpyl&c z-nV(fhI{(1lqN2lH)wurYR`jH795PoIj7gW{pDA0t$hK%J-Ben!JboN^9RkpZOPu{ zTX*i-GJGZCF1Tymni#M5bfGFaM=vm)4ZI;DT4Z|!y^D(3<1Xg~z>(VE%NKew*F>@I0s~)8s0#R?8Aogl(DbHyNJ6MeGQ;zi+F71CiaTz`{C#4VSu0a(S8m) z0b}AhDINpI|09+(REppoY7CqVHpIXqazrx<(_-Mr!_@hL0S-Iwc+hy(H?Faj)$c%e|d}eKH>d}c`OpJWsGwryGrBV;ygWQ zj+$z~$;YX39u2`eM{E1{qB<{?^C(~oq#7yVgo#c2mCMA_&|MB8oBAl>DZ2$$gOuahZF%v}-Wr09mQE@n2RA7{=+UFGoq)={d zIg(DfIl0?&rA#T;0cs7vCOmeFEz}Aqs}QOJQ9naNPr^`O3SOvKNv(2I;I9Z>FPNTA zO>e+e0@|IKU6zN%0St}jp)O6XY?c854_D@vM zbcT-X2))ttsxq8)fy}nxURzB7J?b*eHVHMkl9n^kP8E=|YUQ1ee-%4*+xl_awg5eeNdSPsN(T-clc7n!B zQZ2BQ>4gPSPXrwj`XBsiX?3Z`{10|lI*CkS%oA&2cd3U&1tv@7E$prot<10E#of(j zm;76jT&AVfl}UmqMbalJy!`UVpFI0M!eoNnbeC?h?i=ur zd2xTTAU~=lpFP9i^4p-eY%V+4DNJTiy6I)$GC@rd2w1Ivq5F&m)VCQ7F5pQ~)GAP` z;)>@X`p5)DYf6%R=|u-TJWR3!{{WO8m7*+G9b*+fSA|imaJ50+Vw+)WUW>WS!Q8ST7Z$|_STi#{UZW{1J1R-pf+Npt@@5nkgak)S z4hSB*C+->OS?k&1dEIl?lVOoOo~%GT060~^WzRFCQ=`qwGC^tycLg*ZdH^C{Ri_9s z$v0mJ`t7e)P|B7jn>-2HoZ*Y0;}GXWrViVHaZ`(Jjra$>5E)f~qW2-Y?8Y03+h^CJ z{<~(})a9|9+QiFKRwkK$j;n|1OQST{-nn z&&|54I2X{rH%pHesA~a!0_i)*09uQy;vF-LnHG!38y2&&vqd3fkD|w}(L}3ZkH~QC zY3e9l40vr(uo&o&Y}7$=X6S%b&}N-J<(Bis4wH(YhT>pS!3Imo=_YZxg-8x)jUIp4 z_~Y5WHIJ-5y(sd8IafJWJ*a+>?Qzf92WE_TZZ$KuW^Hp@tyx&NmQ{iHIL8hYT9?cA zS$nogw256i$+G3ie5IH4(xI!w1Eyn`C_~1G^Z(;K*T!??jEqdCHyh1Hs|{URsPY;U*uZVpl;VYsCE+Q7{VdebC>(K(^4h{=o*s@0_;l2kw8_c}6*MYI zkAL}kwAVACi$b~Wv+Fxt)8U@^xA*PQ&$hJV(wWN_jJR^q<|P-OdGXORS-WgYZn$Yi z+siksncugFm2SGv^k%j{D>}7v$vt}p4;||pKBCLeq49yi+@gLr3|zl?{z`YBeivWT zrlur6ugk?_G5>a$|L@SH*{HM&2L)fiobAg-)hmduLN*{4GJqNN`M?w8gZZP)8FGt@ zat(w-XSqC#I(GsHCxPX}6oiTz?wXzNI;eZk1KH(~${LzWyZ#q-+L7P0{}$ClTInep zcsiEVb86>y;SRk|wu-Ej@>T+6$Ak);RjPIN0@0eSb9!N?S_G%$_emb0mL;G#Z5j1- z=pT}-V{<~XHtQI=WKjo%h|0aeX?Y@4R$y?jrzddZt_(T6B1<9IRLoqWiFUVOhBM0GZQ9A zrkyi3s7IC}W*3f!yWl8Mp^8xQSoIjqm22+3e!=mqCxY+Z`^ty+ZeWMj&Rx3bS?k>o zzWL0W7YmfdcJI6XePv~GW@ZyPUkeoyl^&{&1@?w%!qjcye`En2p(Gu zy!5dcvI{}f`Z>j1u++D#PtjD7cDGZf7gtODhleF`%cB|#qgK?M4>CsJtnoK;l;mqu zoo)2CBnKlWX1q<#;8cvGmECOmLpz%37(bBqY8;FpW@N*9|9*7oQz<4!k`=|MQ`?4l zdQlzJ5821;Y%hpygF#b<#chd1v&>$ihCmk;0C(*^MC4q0nigl+BE_mTE=$IENJep6 z1K?fyJpp(L4um>7M0pBOXUVAAG796Wc;<1*dTi!QRMRz3h22c?;bUhSe|lCb7Ds?# z1iv=TM=>1=?barKeeH$m3s0WHxiG@F>WA@W;Ed(~hA8M@C?R5CGEO$JwkB3)LZ36H zlQNvxMB)@78tO4>HkdCoq(lYh)nGBP&+}sKY2~J&LzSCP>lU3`Af5gF_r?g$p9Gvj z1U(#`kf1(fyw}HvSu*S_5cGHvu+Zz=e!u9#@`*Y-K>iL?lsTLkkfyqLhGc1K1>%iW^Q4G;rB_o9SFM12*L?$A0JoRt}S_WWV?E!kf^l@X|Aco}of#knsLmd7vlc)f+0%${Y-C^j2F zNo&lF#bEghDA>;gTT&>L3ssz(TjKH9{3W)5btNVGAhS#P`F=lCw%=O}?mpDy8ug^C z`RONdCJ@z-CP3&QIaFV-g`JX`EQyv7(kUqg?!bJ#ysidi(S;IoQM}X!CGN^EC^YnZ z@453|Uw-jpt6w>;jM!b-edg_fZDW;v^#hNyOIKFZF4?3P`z>NOXVzS!j9{PL#;#-* z7WT6BO64+TgI`(e23oO6@BtcB%-TC2+O*=S@hq}p!=SHJ*+l)dh`-cNIt(4E{fJ|E zy#UzROz_nG0`?YBtxTCJvFzXnNUv4XuyoB19T)0+Tj7(#5=CWw5<>@V#_1w4Ph1(c z8^uijVz1nG+easrdwLvo&cEW?*~&Som_4;6v2Wu)U-_0z%bBrw&5D2FU~-@B0qi4i zR zH8=aHu3*&ULnSlORduMYDC^XJ$KKW+W^nd(I`IVWc59(pv zW!-ScsZs1!HjJoD`gJ%jUn4y@UU^sfS^1ZOFdmJcIQ|YoH${+-?i(+NT3I9#^b~>` zv`y=5)X`*zmA1%P>3l|bzpu=Z%jiFHqR_cX~jJ;sY({x zZ(Bev*xViM0UUhL(4o4ZZTZDFFCyP-4m|F5i>@2R0HqS!55FpWr>^sNjgtoTur(^jr|^n}s+GPCLu=fsG?WFK5bGA-l*sSo#bG89@wA2&fTB zsh$--DXILLdXP1i2-I8x?nDO!3^Sy8SxSG&tCV$0-{brCi5pa+h_%0gC!#ZZWy2n^ zZ{o1n_ik|)=ZQ${l)g*yM7Xi|=-&aq@S=FkV+sU(b{$$z*@aNh3K9;h-OnS(9yow# zRiZIDIXkA#vX%ox^oy2UBiO2{EKHW)a2jff&iCq@ZqgsfkV~grotK|i^Q!l;!|WgH zo}aejNdC&JO?ynrYwHvk_H-6_tdFmso2tJ=s0WwRZPq!cbk!lj;E-TvqhL({6@VUw zEjgwfwbrp&by8%Td=`HH@qX3JuBrg>69hbSMdds0{8@SLnDYKl>o&Z3r&xaHn~5by zm1XkgbxJLj`?8bk(CjlYmI34^s{PAhV+J9^!EVRUcgAn`Idnm%*%S2S#R4`TSSfry zx8!i6P!R5x+nwP>19o^+%_5+qx^NDv)K|kQTvVlxQ?i11hchVDG70-ELG!pOSS^eC z!lE>1^4>3c%(%Vo;_`wv`>u3+yX)FzCl<5CGmj~@UcHnrOLf*u#V^(+9(-{B5c6&N zUrWXQ>)0^mem1Oe#d9xCwibRwb79EO0PhJ&(N-89Z&Mt}$Z%V2wl)%qQ>1p~+5SPu znieB`7A-Co96(1pAjlkE0XGi^Lyd-Tg-W&|5%qCPn%0_*s(KDkXpPfpqfAUvrDz0l zhAN7y`4ATI<)Q>u<>x36-??}97vKLpW%k_bO;5E^ZhraIlFq?s&)(xl>Gkm^#*DkC z?zsg^dR-y+f8g#1AC~3Lw@e#4!lC^A=)>&IfrAWF>=UNUzV5b>TZh!iV%u?pN7U0i z5!;r2z&yR!GsJW=h3ss*PjA=f#ate%jkm-G+YCBk!%;|aBJuPZLSHFq$d7f)U>9zm zN_Lu_S-c>d_}5LY%NqAw*x*_e=3`H&o1}y25YTyeqLjw!5LNB04pE~>pqC-WIS zos1cZxy^j=XVz?T+~>Kp&w@>SVxKk#3YpLaXXFrPcQpzQOQ7#y=`w$Ft0}-r8 z%TA3{mLGWvpQjxSj9e?-&=wt+H|ZOTzbslkVVd=c?DzMc{qcv-?*GK|xNTAWird88 zPoJJR(z@XRa0>sR{8Rbl$t{*i*S?|3zJ5)+rROjn;^iM7@8H!LL5`Z2l~v*w4Mu&d z*8UQM2y-I{VkAWMgE>JFj8vddx1j38Vwd4l;F#bPacj^|fDIn2qkt7+5NR3oxN;{Ni8b^1W&Ke^U01S0%Il$~X%vVr4B7{=c7uwwD50nqQp* z?!vpAGp2+X4cz+td;yXsvoz4k2bvfkbLwp-TM>fFb`zMkO1(>V>dDto3@p^GSEtT%5+%ube9W(J+BEjEBHKRC`ovAm z)4*PETqMq&6PCaw@qCxlskfNT0YYR45wWlZ5IzY6jFJ(~h|w#dwkgFBt)4LLplNr5 zpDq$cE*iO7(N)^Za4smbzssDm!djy=vt)Nj$S)rdm|CJGiK4kD@y(*Um4h z(yGfQAG!a9rv^|QsZY~CrCFF`zA!D`%_Er|IvpgC*_;y*A_aw#+vkS#6msl2^*NI9 zAvd$RUvq!#mdsL)8%}(V1Rj0l>SEpuD{0@PK|!(!)llTIPgRqWNslBsV>OrqBc9M6 zWJ3^73h9K-8Z88QEIdS6&}ZSCzn?y|>qXC_IS-DRICb>M`k9k&oba~$Vf$N;pFG2& zlUKNVJhF1fzD@R*M-J;h`u6(aBgTDfeQ@6%nqL*>H;&Kmrg*QIJ>-c-EkZC9G9XM* zh@ua4Vu?9aKhIz{)Eh8KM6VpLIX-q^k_HEYS+JJFVYn?}yM&l)x^eM`NfOc?hE&pe z!ese8dE6DMgv=+%U&JBR-=u}z2$eSOtrLgO*dKPkJ!!_28TBK_Oqp=`gE^0SUflKY zslU%z$f9fZ?0eAq@#SNN*WW(6|KKq%+c)mtwfvDD?iK1hbq36{LYN=#-Oe70dCJPP ziRy(CfZ^Jyvn*&eq^>L)4Tf7zvmh8OaDe{!zfP2IC#L79P?1X)9OD1Sx%$-O4ayIG zzV-ig<}04eU7lqtE6?weiz7a2VKCb87b|hqXg`j*&v9qvoGFW|u_mH$3HJUqtceGa znxXO5LfGQ9XJr|p-e_S_*n)~TNOH?-mX9siX^SNiP_Eog2lh^EF5jw*ez%vxWU-2N;8|cIxfoQZ zC$b`*=}ppXCexcFVyJzv8f(;?5~6jh`mkZ+#>HA!p)*UQ!;O8Htk-%A%>|P~Qn$Yi zy;unR^3ZszA_OE56Hdis^=``U=>)PppB$wFuM}jR4_-TgWv~$O;7%CJ+TF21GT-Oy-zzcamg85Q zI{VSz#vkSQ9pvABKwn0^KzJrb9R1w?$l|_rR)%P^T7zhSkR9ff+h9T0Y2=|gh>BD= zgPX@{m!XuVLdFV5C(5b+kVNG3R6A{O2MeW=Vw^p6$-6iGyGc0zg=U*=TDEcfl52h! zvz5zxUV_X9nx?Z{Zjin5=0~sg_>lI=fH70<#DVB!H~8Ajt9PSp)#}Wk{y-of&dQeb zpuRRUGYl|uNT$=GMyg@HCljD>V0k_*U!^lA*AIG#IB$HFg__mQ)+#riKsKf8ab?ni zdDl&x=ga@~(n|{S@(b^lb|<1BhZ*zH%8yomcuA6Eh~dsEwhDjMZ?}qF!ngW28O|^=b9^3 z6#|?#aD<3(J_@Z8@j-HOutL%)2Hcb69N>nNp0zPG+mR#I!od=cn>@vqpgXg7A0}S9 zw!^e&ZEdWzZ%M^qn%Tg1OMI8x3`kd>f`8ufY*{8gpk!xiD!5yXu!m)g#|=N{VoSV{db;%S%V zC$F!6=vDEJ#NcU@D-Ycz&OcYl7B5iGFA|@qY5M@aNOc?)l?cZh41OnY=DdXr6Oa(_ z@G)qT4`WD00yNu-ydnUI;e&Epg?^W`WV1nH&boBEcV22VEbF4aRo&+sr9~6RJ@jg# zOgzZPlvpf3!iwyBT&wF>)T(PToR6yzwOx5GlPF{%!yU7l?TJcbAp_1PW^qA->a zXXQ-_W*mVQBZM#^d8~Sv7$t!?h`JIE=pRPwVLglzAFT3a#js9YW~vLQ4OzZ;MJNsNm85X0_?#Q%I!z%+IYy&BGEgC(bw{%!wPD({hN)8#8fI@OWlNPT_PJC;QDKJo#}DiU+-;6A z5==Ui6&RxoX$Zmys;bfs3@rkgP9BwroS8aSM_ZM*22~ErAJogd!^DcD7{0G326s2U z&rG97PDn@W?o7(Gm<<_78)kr%Q-V4q4<{Mwz5ryJ52h#tPl)9x#$I?Waq!hw#lf$w zTD3}wtz5}vCN0!ftWl7n*w$dsjJkfeJIla5a+eF8iVOycyX0u&r-dWc><*G0=$%;?5MtGGd1A&%VNpc}t*(ba~A{mQ+D ziq1EPb&0)V-ID9Z&1H8bBJb5ypkR#l_*T?3qvvdLk4>36D+RW&8=9}&j3XmCklN!; ztYdX`*+s3DFB=e|x9zgAB7Hx6-xB!b1Mq!06dBCc>3l{#a&h(?LC69}WdtG>0TQH$ zLps1Tp{_%)AV4R`)V%=9KPQ>v5ka1TO7mG#TSAZvs3R4kubQ|##=vv!jwKD^FUp#y z-&^qIx~IM_e7O3vvFissy>&2Kka$t|vU0;C%6h+Ya{1?rHs0-d{EEF}Hy&Bb7B>!| zwO9@eTprG6G3`)pj-M4d3Z6#TgM>76e3=m!l%RnJHHsl7alJq(Y-3 zr0o!o9H^xHX-uFgLQplbfIpWWxNOSwTOZXZK|9?&Y3`aVsl)cEyYG7BvMIBc5IyMD z*@t$B(;FXDIYFn7yz;v1>mPZEMDNzw5A77E^Zi_oJWd#Mpe#-deJxBZH!BdxHGv(} zY4OG~f;KP0AzrTy7tQ81W0Ovk&}f~;$bJQJ@_pqHG)v4NU>vX=xN#|CAW1`X|1u~f4&nW zxyvt_Egq!xPTWraJ+vcnyELTbj&LEq+qs_hqiHAXqwg`lT*B7dZH7P~EauvB^I{>y z+29hI(PZpxU4UnZ5P zVrDJSGxs`Zh&cDy1Njv`#xB5h2qCA_3o|<==Y?`yIq|UG8W1w~%no!VIJ{ddH zhN&Y?Y*h&~E5H^*m%g9bwN<>c^5q{V|P?GBPNoIJrs=JinoF9y^RwAdBVKRJ7sI;8yxEXv z*@Js9_v0wc&PaZnD;q%FpKi_cl(38*9i%fY``QSSx zv0(O1^O*Qg@SXOjvxGZF{c>%XNj5MFt?d@K(Pl$&gbzV?iM`t)mT)$X^R zQ6_wF?X;;A5|3Urand+o$nWtn7lq(7~Y8u2BD?>y*nU;jI z6Z7>4(A6NfcGN|b%bhYy?$r3V*|$ucbnWG(Nadbv5Gzyf)4lYE_X+>s@6)|>zFqqJ zr04kiU<>^R^YaVfi}#t$j5z}OpvC65+1%jT_ZsCPbw*7Bs**Y>0Gm}MM?3_uGRb%c z&6ZFOoqH<l!UEjB zH?b1m7r}l8Av^f@L0{P>=Y_{cW8v6C zyYP)yKe_Pyev-4$LvmZ3kxYyUy*tb%qeYMQSpxDjCOrd+*_dgATa9+xfSVxWWHU3a znKVh1X2f${KsF;r0OnS}{Sh$$EeeYTShSo{W~_Syd-ceBcdcOKK2RD?G3SRfu9G|M zzGKmWfKtm2vvbPoqoYUBUbY-p_jnYzUQvRNFL*p=gW2a5AY9-ZnnjC^n;V*#C$FC; z29g>Rq69<2l2qVtHI@kM8gvv|U_>3P`zdzm2g=qJcfEgDDQ{rbNz*@6z89CU!%A)7 z{)Kn!#yuV|YV=VBtAzFM2-}Bn#T3D7jc$IGA*;0#eS7tJd2K4C_SNNx-Ia%m^&!bo zSP11K6ncsB7LGJ@IbARs8b$R}Q30-Xl1kJ?4hhA!{|Z6OBQS-$Q-_C6WPrWrTf6YD zvBO`!{@~=BI+o<-l|s*E4^$&-iv1zq{d8~9lrmro zih)cgdFFy{h|1y4bt?InrB0S`B|DFIS7;ZC7IL)t%h9t*)HLb>P6p&Du~;u97}_ ziOl~T+ zN=18MRcNyzTv@}WwU=Ddyd;>2{)3I5Up>5)e}@b2 z1&#{1?n|&XA;jVb#Y+%Cg(sAmky#jW6^XflOc^1QJlNi5;Fuxr?6}%8oFiEskNtT% zcI1Srrx2q{Efbd-Wt1!C_0f<;sjw6n=Da>=T{@cNb%=+14IOm6@7%sC7sY$BJatDW z%}LDaR@Sv||Bjv7E0>M9>ZvYMht68&mrL1|>i({&-@l?m`zr>t>xO%j)6@ix2&%{L z7S9E-lhy1-hbxcXtIHTvr_))jW>BG9ML;EB3SruOHMOiCjUISG4P1jm*S?~hbKn_b z)Rjw>d^vSv(%Px$=K25K_!bqQNxJ^ER({S>eO}z8D+qLsVxLU6?De3)2VG6fMzl4d zw`+UZvbPIgE-Y3R0nfDJIIbwPJ|E;j1|I}@n}t0j zWG84EM4b-(24f-gOeiYM%?Jcx!|Iql7<5ZnHa9YZZZDcrs)iDFtSk-GrRhvng78r_ zqoLE>bIMVmvzCU64l~w?{n{{TQo4_I-F0mJ;=2`N$!O|o$U|p|=4Ksp0j+H8^5ixT z^^>DJAPmJk+G8GNKy(-9g&Y}0;8bbtu}H0@c5QPJRLHf3JVhBY!Xxl;5qE|lH(t>!=C(4gQE25>PgKw_8 zzDLi!YgX^;(es88HxDi?jZ_X*esVM6tyQ!7#ItfrR$epwiWxJ;Y(I8v`0T0JEzs;7PXsV1A{^vNXu;<199G*EV zL1pmxZUBUJC-L2Wf%@baeSW_Ta7@7D_gMcAYwrOVMYYC{&Y7~k%(iScbwe5?frNx4 zgt}A%D7}T=rAn2KgpMLb1r$NWiejOKVxuS)ydc=Spn?SyvEGZYv+wtv*-Zhx_y2$I zMcFcY=FBPI`FionsBUefl9+|95yTf$#PJywdE)%78GP*0q3F*xN>nG3Kda!K9SI4Z z^qr?z_U;|X_?7?4kwdwXF>B9G2%R_GeCOc%#T^*JTj}?uz8h{Ne(-F)tX*Mlvq0)( z%=1^}8*mw@Phx?V*JXByvOmrtSfKk*036YxPee>&W)v*1&J{UX^|64&`?YX4x!F_O`|4`}UvjvbHiJ)pR>-XwOoAke zf&ni5vI0#79j~IA=h8BOJ3uj%BKYi7X*a)xY*58-Xam^+ak1uR7sK!BS1* zFw;B~v^{M@I{iscV!Xe7UY$cS6%i zTWzIslrO2PU5#;BT)R3$nyxK6dV1!^cYdt-cYE$%dHW;#AMMF*54VwB6eYXs6~CFH$H$UYxwsoL1WO7loXFDce#1- zzBp*YSbB<0#>jyZDZ0=Sumn(`7J&_ED0D{Dh1}?Ly*T#>&_m;@rIK1e!m?}8=co^a zz6YphgwIwF__AJN&uF zI`x{Z^=vPKI%-n-#Z&;R}2(&hAd|9e#?F zvf2GGp2ICripNA~TLfeV&_n5js9>X;6|awz;!Fg|CVKHa_(Ac&t%;3V-gW1SyQ98= zXa78TL`-qN@zG~5=w5*rw4Z;|POI8&G!}>l(wlrN&_7x&e$;u95wisoAj+nAy@JsQ z*3fDNTIFY&^Ex`qQ>{#;vrTcS*BOmn?7}N=|NK?dTD)%Is=He@VvAiY_#3c*j~j~j zp84RIJN&hN_RGjul8>b*l;DyXO1R1xiC>700Z~rae*#kC{bL@^x|b(V8x$MlmsIT^ zNg`4`qe~3F^7bDwll48zX%Nu!k9TQje$y^#=lL*R{Pew}Zm|sO(GNV~Ti`_#g|bkh z11>oRuP@P;l2-nDY= zozXbeZ+;e2uq3)O)Fq7;|NlX-cCczX;O-f8k zw8~Zp5~v~@5BKC)&_c}0AC+S1=z<-nTS3-cyKHPGtbNKyPiaR#ij|K2h_%>Or)F&U z$ye)^jeS~s;mf+^Vz++=vp>d6`q=M)f2f9KnlJ^riUcV^RgvsM@>(30dg%oaWDi6R zhw7)Kmpjw*(@&>MX33Bcp8)T0X95(F2?_L$3r>0*;N-<&pS(W1|eXzi+gykp9&wP!xAoHS;R z-0aRJgXjB_hPSThsy`_ypiBk6gt6&hxFm(!-EPMLW;qVmS&A} zCdDPy&rUJh%;0XEW=sKM9f7sdk|^gZGy8d#^6+Zl@c=!{X5 z(x^n?mee&5$6U{*Pny2C)yscedFkWJlPA6R;x~Vad2gSLI-U(0I_&iga0Nc|)IFa! zs5iC$nr+%Q`gw!@i2Nvus%2BeK_Yn(s)o3e?jB2fr90D7K405b`| zLjakyYR`n6>agZGlRVZJXaTMoTeiJ?<#kCzwP`)^(qe*5`J;$_hQGb#e zKa-8)%}ZwHqpVfiQZYPWyzW(Udu?x%@;OUw8`T?bdY5q7a=;w%n^xEsQy8+6i3nM; zC^mb@-rYXTE`{usc2PiBoMb1+^LYH_3=&d4!=D9q0)t-Un`DB)xhxl;HFcT}S~MZf z%W-i4${3i6IMZLHnXACym$0|4yv}~oJb1b!Fz7WJ0FO~k-yk3)H`(a(1#RFXP`{^M zT1s-SiWI?)MD-wFP6HloFbFVi14Lq?E=GW-bF;3l+|czedPo;mwU$Yj0yW8tn%Bdm zl_sk_>-W(9yB~PDYURpq>zDK$wEw0yJ$|o3Uh)OEEs{RJnz1T##FmOR6DGvBC~Mlr z&|Hu4f?`0sMViakuT?0;!mJ{#AJ}KXs#xtdqbOpiW?Cok(XC&HF@>YvI_|EtTtI+~ z{JOA2>1OsmyJcxrm6rJ)lt6`8D)%I|=&vTyL*d`pJ0cmG`M!x^JXaBrt3<{)j0Vxk zps**^7~%SAO&qEN!?-Jk!?*8Zuj6o%a!dE~{uF*BNw5LH&pX-T_s69piF^Y!$Dhcp z$;=c`kQWbfpemrUR&BwFe7yNHC~6K!lKunWD%HNxb&CF`yC~wTItMXu2|F7L=!zBn zt)eimcVi)4$e*FdbFpBw5nZ$f`G8%beW^VAj^Kz-N@B?ga7{>d zz#&FRrBr6%;*fF%P~${7vDPy70>u$!S28^B^jftf+UrpSmvO`CMQ(hYMt#pi-NG4YBuMHEqc z+wCIn)nFRZhbbteX-GuayfZO1v2`LL{zPc`9K2|h+o8KQkh2AkKhYQ`)Ol+FsV>IyQi{+_Y)ayjwU!qO!OsCjf>-`x6u4_B~@I=0Q1u>I)-7 z#R(RFR+h&Tl-${I@o{cA1m(nK`w%MOgNOp5)@TFPw5dku7{ERvK>|RHK$!M~16g!N zWFD=a3busr2^W%p+`0WcmfwXelO(&;amz>fKdId{e)(0>v~XYXpUnbiqJGz?VV zPJQyaa70(4I~s*d{Q%-smo@6fMd^z{+&Szgu`pX*vjN}5{~0nRUeT5v{A%9ktF(3f zA4|V;^73a7Jkpab3%~r&u~f8TyR~}*5#h+PZ{}z7Fr8SLN{a^JVixLXG6h}QdU;aTkLL`kl^+@3`(etr%8GfXkgQQDa}A?ef|=d zCS6!FS8OejRs;58tZqzGQUg7L9^c3L;5wibJKxn1^>+)XyEj1l9hE!*bhV>CGF{x- ztn*3NM4d^~!@ZxaBbMe##+sZ;0JgFUQPJf=bWSrej^!u%(PPk<&mLCDc-XnR90rw!P~7ROaT zF@-s%o^ZUHi{NTruoZ)AFb*w1%*sGf*wqVB#3LYLN`9|FLQpnMp6!k*^!r!v1^A%a z2C*osdUNycCF;0Yd5U#-_|>7)Qb7yo20=GPw3aac=ZR>Vjp7IrcECHecxr> z^dPo=qdO8`kc?cocPnoJqaa$ZFc{2Chj(&flG|){B;}?lMNJC98WcJkH0oZ_K*&h% z0a*jYLP_*lAv9SN6>tlba4C8o$a$#xGY&b?oU`;8#t1gc*Z2P z*fM+fwcnH+ybk-(AOJj!#k=wBFA>LY`5hu69B$ySY*4{^3N6 zm~aQ=lD}{U8lkpmgN9Iuq`2b!=Im_03wfCeQp{dttAO8ETtfjFJ0O#LRi>*aq6eL` z%z6@AT{)l=6}=u}oU9|Ze>Mst>q%C&?~bajLS*gw2mUFMkoMQ7cSL$A$FKD4(Z64h z*h#)FeNd~rpS_{QWSST1PeNz<4d!J6t%T;ssz6_A0j@K_#Z^J*8!$Z_9Su-!)#e+n z;S~sJ0dS*Sfs5TyB~^yoir-uvDDEZyMWXN0j;miv_4qRb0+T53qzzn5$56H{;1m_N zAtBL;NOBaL^%yOpWM(i-dpri!=kVE}IftIRHT^*fDSA6vm4jz6A$S4CNVh_JT$IS@ zf2qzFW!c*G%MQ(7zG>d(Wpj_#{vw&h^zawrtKm1rru6Tx;=12yeMW-m`T%1kJvzK| zQI=A%LyQwlc9ZIISbJ44#ex3}W;=Y5dQrX(9lLX8mz-d0i6Z=L!~c~ewd#M;2hW8s z?2xAEuhKczT06qNgGV}l=dhOzUF22d^$Sw2r=(yo)n{}XQz5kpaxz?RL7tJ^Oomz_ zs%VK)m|9A9+MrC%#F2@THfg;U}Notz$*AobF4u?bQ9f_pa;S zXUE%2T|TvW)DX01W;Wvb>w?q~Jk9&BEoJiPn%19e7y^h@ol=%}yk z{>}T4|LwyVI_1n|=f!6Ys9YrFF`NbbvFJ4m^je`%y2+RX4^NlhCsN@6~@6s zM3QB<+2y=ow4SXnGT~eWJI`TI*F};9$tepYU^yr_{tdCQ4MTD8vjH}O#my0p0 z*P|E=vikym+7B{4iNKhP>7^&wB5ga=p4;=ndsf6Cpa zTGnx!Vz*>vWhWJ7Jsap+9+#AoaZ@+>+|`|B`If1scfRi4p?VOaFbeDNImTdzC6e$8 zlaLnNYlQ=hYP1@Caf0V;)cNIt)qA$WiWP{$SdH@wKQgo+T)$C-goM@JFCIAKpx*PB zE5Ba<%iuBhEYMz`d*I&nFG@REv3Arwd(fEa;+62|Ma#6m;Op=!y8s>4IcOlT{>aiK z`;joz?04H`$>ejIF9;?)9geD8xe|>vh~hGs4N(ILm>2fMQcsbG8x>_i*woP zx97}9#Ou9F7Ny$Vi)OP6?8ofe3Oa?q7ykVyAfM=Y$$REN%@kxxs zB{UBC?SO2n+hc~0s{I0V{TPOf6Bn!OUPeLi`rP?YO2INBU2G}C7%&j*%8bV8Wlaa? zYM(FJ+%Es&#oDuwwh<3!aJ%BLst3p}`B$RE0AJ9$=FR5L+9jWD92x2aC^slhTP5P^`|@=u{pm1 z1~D&#U9#9M4mb)MEA1H1+fY2!ctm=11$8l3VWDs~~pRR{xd-{wT)rUHjXOHdMZ%m)k#*Ld|i0HEk z&dFNLHIA~G7;5Dbv?6t|?<|?7oKExE3g@>RA#r>}{v%NfNQ~E_Gh3}%s+GeJY-vi) zxQc#bZ};8i-7w|B$L?GT#5clz&x$q6mRZJ5KfLepqt^dm97Pz%8a@tiNh%1f6>Jhh zOXEH4EVK>?Wx`I{fq`S=&eT?bF3AjWDOHfn`?PBeg-uc*rQ`dWM^W8qSU&Xkb z6n6eU)}xQ_Ju-c)W!bVdEAFvt4|3QXE!;1c%i9reiSQJ_YR&gFzFwn+pX8460q{!+ zA*&#wUZP241}HEQCS39tc-T&lIlH=;H(!caIuhWM?7SV@{ zRCwqkpQuT!4{kdbw>_zi*~ZFQmu){u|3V*8-y1>RC2myS1B|AHoMw|Gq2#0iksYS> z1JO^EEh2PI;+_vk9KJoCB5~um&&2P?oFB_P7thfl7hN=Z1vQs zXa>s)_FaBo?634f`vB@b39`b(H5G7Gz)fWBfr$;{hb@Y|Z=?4x`PzFJ5~F#1Y~^n! z7t4G9O4h=d4CbPZXR9N4L~Jgs5@lbmp9`AtxwO-M6vsv;puKA^O$8tFW25487)x`| zexy0f!cA{R7V$aIc!Zj+X~z0^#$!C%_}F-abJ}>ZBzh0cyY4+6?61#w4)fN=$L1|k z#Xz=_TPTb|VhACD2&Xe43$c$;;f52fX?&c)H>PL1N{jq#Jm_jc8w zJ@oe%`r8xh@4w@{Ol~E)9`6mRL3>PiSa?=6F%2?DFyt_xybqJGBuEljNuYY1lRYja zgQ-VoA=tVi(WGsfH+9~#6VIK~=UAr?oL~v5EX_DNM!t{7$s9%ttAjjr>(H)?CiK8= zW_O=EH<7Q?!@{G8+$PB|FXRP*F=8kR@>0TS82WPaVGe9j`-I+Elq=xhL;o3iGT6Pw)ls$SmbsjG=KT5qTnQ zrtZ=(NvxDENupC?X5fFpur|sUs7FbaG_{cPYUm3xa}lt9X%T`68Z6!CzBTFP7m{!F z?U8qFU$tq|nr+y-ElQAe$GMXPf5?Kq6eJg3$Gao=q2(dAc}tZNB#V>?e6SAXoTp=M*5i7kl&r_Gq8- zvfvlxP{bE7$9ug_SLzudOf%4+Pli0GktF;{t48 zfOQJ6vH;5su*3i}2iTth_HBTD7+@O%Y6o!u)_hiGr%4Ru)71SGQg(AZfGB1O#&<{V1TIsh8s#lGw_Tvd`!4!C%uxy<;uq*yGVaM2kw^SL*=F53tk# za|W1^628*kQ2d)(`)^v^Ki-ZSRsYp)OrPtvpN0}@wf|~b3;Bf{l27mx7v;BzBd256 z2p%tBUt6J&SPK5}^Y>#;@2)qbmX??YS_^u#&gb@|& z$h*8OHCa2azXDFbWuwIe$p*Zi6tarQA7*kmZ3?px+ZNlb#lGIS#ro6*(WSqrn5`jRFLtn;lt%@j{jl?#g(kxJ|&P~HW zO+GJ1l<6wXE=7_TqqwdAUw`$#a)rN2%VMAK9rFIKkcqZ(nJ5w3pH#mST2=$XtYM=^ zvWjF8TmY&rK?jgvlaYBB9zZox8g?VhRwJ#sD6ZPPwsK2gqB^4A zp0t*;Zys2*?)JHBJ-7Ha_3BpAru&?JO~gYJZ|yrFu4UQGjE1Sfq>`J;#!c!!(lM^6 zq<*10F0G_D@z^qW{|DrA&=EC8Tpa#zEh-PV4dN2M7F$6arYOWOf$s)b2w>qLrtu`5 z(Vf>rWK{xPNDbo8WUnzaF z60vsw-}@0!V`Iqw5B;#dE2T$k_ruo%?k89el%JyYD5~u`q0dDtVC5@U%9qh>1lF;W zGzIGzA2LI*<@ezTortYM#iZp zvb=n^*hyPGV+NbfZr5f>Q?vzqUj?ndxG^#p72$6egv;Box1x|Nekc4f9k3TVwwV4mqpq`VrGQoL9C}iG#O=9 zNo!J+a6%t7dWQPOkMywfdp|((Y}~JXw|)B#eF|9X)rj9oj=azLG(bd*PxDEYPP4wowa4&J z+*k7K?%ik4>d|xNu;vXLwrBw(;Q1c6Ozz(OmRq_vEG=!=u&j*cx*D~B?P6QNtY@DL zN1?j%o*7xpAiM3*zC5{$Iayq?_7#bu!0q4Sz9alTD?u&lcgV6G65~;IFdTkZm8X5b;n}lJM{ZXO}`*VC~_d!OcGWP#ItU{QZ3pU*mY9-GE6VX zUssoQ{-8}1_eExd>jmv39?uP3aHSWTA{r;~^Zy&#uT8v;_P@yPX18&EoG>D?t3ZQc zG?_{1CL%|3K2_i$-@cmUFksIi^7~$V_0?D9@X3=`H=I0)J-I`kF9y*M;UAcj2p==0 zk|Y+$J~|}^A?$t?2`K$4`*s_sL_Rx}D^Tvo%5 zmFz2I;$Yp8@0KBHeSgUrn}>+fs_=i~;~){VrjN!2e%$X%^RE@hg-sQ2uAn(I0}h=k zYyqBY8gh$9lIbm0F=SWEp`FGizkMr0I@<6xLg)Dwl(wiCFzrTL-A@e~=; z#Hm`}=cj!5;gsju_PxKcL`x23zDNV>;H^vO9X-u%BEl0ax;qiw(A@S^^$~ z8_@RK>)I0B8?_D4M;L4|eFW3?LR-{@>#fwil5DOdNr^)^Vc?jWigmeOO%;j6ca^oe!|5D7mm zKV?W?yqNE;_PM-|Wgs7qU2s%ckI%0hi@a&fg&ijtLgk$)rc$3))r#l7e{3dM^!CUNgGikU1$sc zNxZ;**T9aW3P~m_`B5n(m0z_|a>C!}BqPV(z2ibAVQ zv*EA|XQy}j$hMeZXkswsQUJHSIZ#H}0yVdPV!z+EC$<$X=ZCw2%ifhZC_KqLCdQYMg9b zXoUSHe{jNyA%nq;5;zva8L7DFk;%XRK3N+&ed;uQZbQ(=kysy!D8ezpC>o82hqB2G zp^*U6nm$nIi$b&0c^_S-O`VQ2vqFB2%`vRR9Bq3fUBO8(OL_t$f5si=3iz002Fu^#Mi%PAuBP^{`q$T@lM7H{rgj*nL2v3+WJI zWfP7M=E@R36^#TVQN?wyM|IUJp4SEU?T)+zT!*|8(A!!JIP;Uu3w3%4=ADYs^L^p> z^JnkYb*UJ}+;0D@vPsEmXY?L#_DEaIZ)37hdeb)Sh3lAk8R z6Q#A11n!RJliG~DJ|C+{7{yIASWy9-!^6@0rG+MDs;qT?O^&7AvpW>_Dbl{%to^#) z{g{eG1mr^!iCm3n$HIFex2i^SQTP&p7Vy^(=rb;YI6ADldq0E-;jRi$BRxeRmK^I{ zO^398F*+nhnqn?*MtOhtejn7t)m;Bn%*X$KOe?Nq_E}_;Rwj&yJP4fQ3n49^Tq&5< z=$>%YgU)rkZPAFl`nL}0|LXC6x8{!;u#MKZv({d~L5B3_-e*I#9I;a6{HU&G_QoM^CTecHvcw8okDv@B0bZZjc!$Pi6?K^|e~TR%tKH%jWI!(|@nk(aS%?Qo9aSF)c@t{CH3>FB$P+AT-Y zJq8T{o~f0+>9VwWN~^z6Gb3&ADAmT`B&m^*eS<=c}~`v~_G`WC6Us zl0$Zv#S$+;fN&a_33ns1gz!H``*asTfWZ`)#E71kngGgC`LK%gHkE zD7O`kK7MBGmoOD-{eJeXTIC~Jjy~G(J`z#C+HhKAA|yv>_l;QMA;4+8hZsu;>A4M9 z!Y{|3Ij&8l9)6~E`dpjFG9oj9cYrfM)A2BT|_*1QZ@BXF=w_K^xm1#IF;RN95PcvY9Ac9XV2ZKw5lt#Bk|BwEZ^n1xr=FXwjb5H3WFlY0EYqS#ssIG?XVxRi*`6P;l0!BDjvu1g>@omyInP)|DiXd(ym*^0 zor@Nf{j`vW^TqkEx;mG(7}TP)vulKXK-Vc{!LB7;huXDII}^_6+;zafqWS~6Vx2PO zX~L(5Owho%kR__V2h+?^8x;5$Wi74!r~Q^L>APfUzokq2E|uTtvwT^vWy|_3TiRwf) zL#H<^z?))e+&aT@lOZd|-?+F%z4l%0H<~i?;tPwL(=){IGX|U3$5_DE6dW)!RC1b2 zPoM*oNr&he`6+@fgKf;1gJZ_1(3 zqA?TP@^;qs+P2JxDoq~N_2}%|l~apnfXQL(aF!{b!y)xW1w@4bDaEQYG!JaGA80}(#p9s zp2!wi5=Q?W4_L#ykG_rt6eR6w`6PU%zQLT$I0xSS-0up>;ISzn2Re(Z&nn7Q?0Wgu zCE8OP+5C;lH%{$}jhVC`pTQr-0Pqy<2cK<&%^11Ih}m)CcN-@R9Gv}}6 zzIaDkUe=nWSJV84Dc1Yod_{ckyWo54;WRj^Bx5aU?ppdOzZ<2WqGrdjxD>gK?>8u$ zprp!doy_D)CsOAFae^!z*H5#8t*3vQUHax~vh>Z5UtAkjp)13fwn@H~ zjZrQmLfjuhN;bF6Tp57p6kqW|=nU~J(gJWhAsNDmO@aObS?G+}J?bZq^l(#Qt8Oia zx6BD}V_~1>Jz5S|;yrU=E-aW>GSNGC{p`k*O0mBW$s^blqloiH@vRL)Zl7DRn}p;P zdjj?wm@g$ztvrPK7%UeQiG+A!&=@mT!hH|?Hh_S1B9G_xxUoZOX`y+nwV-F~yq;a! zr<4?$#=Ayq$YEKflm@fGfL07HNy|j;PN+4OKpJ%_y1anhOMvKLfU0k>_zbfJr1I zO43&gnm8lSa(1b&2!*OQ{<(4EpMUPDtlYC>=1h)DAanK;c7fMM5}goo+91ygcnX}V zgcDQwp#oAbg^bgD=B{1ywSE`ZuDwY2#fM?E63^ot>*vuYQ2p+Ph^=ZVbP&1-J%kEj zxG+wbD$EfU3AYP(37dom;1{z^cqTMpYu6{9c+@D}a6>_p_@+&hTCh%P@7~`2!HE+y zrr5UBpD`nEc4+C+_A9NM2d!T}?7mTV-Z^$%_s8Z}HfhnR|CBAWS8l#<-D8!NkFC3J z^UB#Dz=+6NxQ2_)22-}G$|`kU9)j+69OP*_5T-;RbTUt_5 zdUffUN$e(hg|_F+q^sr<{E2ta>&D`yO^d@%6*ny@O{eedExJ*wpnERU?HlQ;wr5RPO^GTB?U7gOl-h*~|_@}*te$L?o+lUVX@F6^n9)9I>*6FV%lRg(e zDk*9fZdF`ddViB9WnwVeIvgo2E&dh_wRe$gEeAiN&#TiXO=6us*I{;syg*oONCV8; z_Q-}xU`}|u0u|_%q}60dQf5ajNrd+vm1hfsxL+&IhM^MK@nA%Gn@U9Td|Y z)OBJd{n-c5pDhO#auFt!t*H78k7ft7p*a7PD;-L-Iy^jywsllrDOHB$F;-sd#+7K` zSG2amr|_Dt;a4P92|7ie%(U1y0(E1 z3&|q2?}2Hv5;ZIF=;%G%D#zC{W|YG{qV_%Pg3t2j5I1Tuo2<4vx8?&S(j#@FUOKlf zGTIUGHQhEC?M-`Jw=~i?9>e|&j_!}aSc!G!!vJ}y!_*FnF}1b5mT|xEhrA7b%cHT5 zrd<-myAyx~?ui9;AiT2vXquhfv@9#TsbbLIo8p;muwDlvuVNFCM+{yV7G%~y$^=x) zF&=`shV~W`BFKzxzy(IYtU%-ZF%~krD7Y-JW^lVMUE2=1GqBv;>#d?LnR6Q~ysg2U z%o~f>Ai_gC1f1L<@-p-VR2dIc#Hm2n>q@pakR(60`9x9v3I5_HDx%O}^|(PAJZjk$ z#_^m}ltu{ZRE58CWk3n*Fs^GwQBg&=vE#Z9C@dV%ZEX0>mU(&2o9E@VWand_{_Hxs zQ>W2gyN)g|AKkU61eaTu6h+@@-J8lcLm%G+JdC}HNe1vGt>6BF7EekqMg30Wn?t9D zA_88NMcTJ}Si&CM(+jbK%Oh`LtsPZXc=F=#!L*OXQN*;MR78<*8KuhdAHt`?Cx2Kr zgmwP$N7iLXiME!@B33+Ye4=r%Is+0Ogn*VS@Q2(=0h=1_}DvtRLo}aY=xNubI89R z{y{k6Ds)8A0%*GXq+TIy&kJq)TIW0Nops;i4{x4!kN38?+eh@3CTbsay3sUg*|DPs zPt2ZZxnm`N{+oDKGl~II#%kQO0DkBrgaM>uZgId;R+H+HLKN^tv0mKm?QH@E>H6R< z)J8Dwt`*cZ@49#{IZnGov0PY-0G{y!UyCW>muM}pS0k~ehj`qTH)M8K2V3#*C$el@ zA6~WTzlT@BF4>Qr7G6}=Vt%tCrSup)dpn+8!Pk7Awx6zH&iil;0ZV{2$Tdv17-i9< zT!7j&pR&|*f`TYZ#=|Tn_QvaPEM>~&6$kd+{ueXj)@Iu4>~=Oy`k}!pjbMcxqf{34i^Uny7#L-_}IM ze2_z)a(=n9gpN?|8pX}*hN}_e=}l%$5_>0(N?e-wSYlP;`-$+HXJR5luDm3{o0;n> z7V_cWSxkEzo4-3t!guOJIQhT&@Cw)TaX|ZvEs~q5@Z%vE5l+aju&>~gppfr2eEdah zP0ZC^-^~*pP9BqANn;tA+8Npi3I0<%#Z`tjQC`0p>NOxB#bUR^feIywjRxa7sQm=X zVFPm-S{p=g5rd%(&OZ_L@{IZYfv}JzR-&X}r1$@;!;5>hk2kWm8|9s8Y)YoKK9vjT zeC{x{C_R-WYL<{ebC=2@6C?A5hPy=XfrurP==CZ}W_~>D9=|hQEQlW#zb^i8{H1tB z^ni#Jv%yp0osC6f!i(%6yG^_wvxtP865PW@g}twwRepy{1UWMJXU#9~sf++xC3R#7 z8R8YM0-vt~k;;(y=E>#lmEMX-E%%HZQ%+a+3>r%Bhxhaw$iIhtJ^GesC{2|1igZ?C zJC(!Asc7G7yN=}r3QYbeJESUN3%V_2Yudd0MYy9V>-2s@xeW`-;YD^zIirY=DOJi* zMWUflJQ!}eq_BsS9k7}x@Rg?y_S;?MOxVnw5qLf+cnl9E$KTv(({jn%Hy0Oj@yEXn z#aEunj254qY}moiKL>1q>lBoWSl?`9p@F@IuW)@@;gE$nB<7TzUZ;2$V<66cQBT>HCCAV`V}txcHUR6@Kdy}O!QR}@^$xAdbWCYdJcOO-mTzN z^=_R`Cw36sPDViNNkOjek`3m4eh&iGhxeH$d=l+55q*Y2UaK|NDmJk$wSpAFpeP%? zMvQb!D9&h%jkKFE6df-VwZD2meN+8j zHJH?~>TGqTD!&o0HdVzZaaA;_@o4a-`my>S)o`y#WmdnsLfxiHH>fwOAE}a2^{e8( zZ7MUWrRu-wd#TzFKh09_Rlie>i~G?pete~ylor|eX$5}zx2n+PY_z@s?Psa`)i==H zcdFIMFZM&5ZP82rRxLJz8mEc|wM3n%Zd9L8zf}!O`l_$vT{5Uy>T+CqT~+8~-)M_{ z>Yu79M7NcwV{mPG^u9k;OP1PE4Xy5r%lmNGht$x37NV-CnQ8}hgZj9-S2awfZ^ddK zb+mdbuDzz7Q~yvcbg2WsxD3r+Q$OIBtqR)gU33{Eg(4_^q4l)7P1YqO|ezl3Oz;LJQra3c*wc^2R#4mTws`;J`3vLu$FW~|Eq=(GyOWn`qKWrFP=Dm)}e zsd(8ToCc6jje!v(I0T0%rWo3wzX^aMgVgLELHJMTpfivf_&E>>$nWO|S_it(CqxQ3 z`RCIC`7vkUWZ*Pf$ddyL10uEF5s)UW&0m+2W zh0{Vn4V(&`3CO{~rGQA^#OFeR?)o(WKZw)yCQ>jkg&MGy>jGN?I|I_rz+tpPNF9Ag z2Syk=EEZQ5-qZt;&#Tlp$xL*pxLDw?tY(R$D_wITw)QwzI?i8&h#cU2e18(TEza{e zUfMr~?@Z2D7K0AKrY^y^&S+3XI15M;xoOA9IY_yWJcQ-^aMMG|;w@Ja;Vy+g5xOZs zNx&EQV;~we%>e6+#R1d2D5*|WWlo$zNf8~g1987@3rGeMFB1(bqKuHJcdHXJF6|)c zurIAN6R8NZLI0T8?h~e6NoTTGdoHC&G^gfj&nkq9+3@DnXQwRJZjf@I)JF2>nbLA3X)r=Q zbx*di0t?GW89xg!`QX8{K}FaY9tg*ke3T{wTLLb(Na=b^o8P;)Husp=`gJCqxqSI5 z7B;%Z29ZCzpn5F&nm6WaJ^*|23HPxxf|o|l4vYdf7?AbRYQgeC_hT>)tuVL+X()0? z)LG^j6hc1ZW-(tH0Hv7zEs)Euyde*|vPe36b-%J`%a#3GwusB_zaNjp`V0q6`bgP` zeJ|we0oW%vfNb76gk-gnY?RF=!OR46rForsE0j=@nX?lPSGj%&Wcck7!0JgRDX9Rh z2qn^`(~#zphldS^E9m#Hg>}wX5r5`H%==+H(?+#lQ76%25=8?;6;-v1!hh^ln&l#1oK1Wt_U~xXJ^OW4RdRS)&rhkPA*tX1{N$9n3gANIdo_zs}*A!SY#(zyFhP%x^9?|?ju zQVgh#NIIK~3omZ%^?a{~FW$zGrlS1$8{nAXKdh}QX!_g>SB)<`*R;vLed3u-KP~>| zn4G5z`;?3sa2^*Q_rUUo~jH>W$jaYr;5SUf>hCqY8|PlBb-W$;xpUI zlSG?1tU~=6MnId-ff!L*jiMstE9F_fqC6%}8~Dg-45$MZpFW_BPZui#bsc@9#c_#O zdOiI6;zMsd)AaeKJ6}DtD7cYMN z>BSeS!h3#S*yGg4Y@o7H+ogSaOnYX!V%TzKC`&%@y{3J?U;Ad*nJoroD(mwW%VOOy z73>+Dd&MS(-H`u)?_36Ry-yJ-6@sBpmX$1+`SjCLY^$5Cce5#OHpv9H^xwUF82IUZH+zwf7I%xo_=EG^%<1ODK@XFx ziFmAujI0$CS@Sb}s%gSM)oKUuA6gxmN$0~|HiUBCy&Vf>)K3N|DoWh)#0oJfS27HJ2wiC35&DJky#393d*Lm;Jwtnz-6=cABG+y|9`OJddHZqE{>!566VN_k zR6?>nzIJ;=PqE#+z0RAKy+iQpV&L)!{i~3bEC>kBzp--qG_%V;iCZwg+Pc-=B3~|}@ zk!1*~*))$$R^xmqs)ZstW|hjhR%fzAexcUDP&MbD=pXiozk)F!%QSMJ>h;zs%Zq4? zbCa)fww|dHS+Ie!r7o>^%sFx7lXq_0(@^vmzEs%Tq%_apAD^y-k4{@U`|*V2`(~}2 z^V7;X%**0g5=&Sw_m*+dc4Q# z`<{Jt;nX>|+_Q*nc>S9p)B0;4DbJwY6XAZz$F8);Z^uikqOKCo)t1}$C)(~^Fni(8(0xk^qqX)n!xXvfXpXumVVcdsyich+mx zQhhD?SqAK93CPZwk>m>6T@uO^Smf;b&idWzix$9snq&u*AltMv6`X!*DzZ2OM*Los zZ3L(oF_Bu)wl#_1=`a9MaU#g6H>QLFJ&$1BY#SYtwx_CQPd+A2oc$6B}+>kNR#aM=pNo`Tnc=bRO7eMRwmV z{f6uF>Z?5@4Z~h!2m?dDqY3gV{;?5Tw3 z6OJWFD--TZ5WNY4WD8;=^XXI*yPvBwX3_93I@(-ui!xovsYlM5 z;xnOiXTjiQC6fm<%gDdYAI#}hIelQa(p0hKLTwug#e50<=)88liqoC-%KEp@o7Amo zYR{yMf{c{Jtl}=C+p^{2a9Lim5I(8t9PT+DyH+5vW$Os%?JyJeaDjUGFNu!di+mn`n+`gieW?{1V(%CZ? z*gHR;)^gS^Rl;8=i`K1KY!K6x1>zUl675d5fXxsuYHP3h8A?-VKWb;?PqaU@a}46E z4l_h|Rs*k?g_lB0z|=wtvC0s5Pg~hKE9+)utr4AVW#fOgGF*Dd%BEP^Fe^jJ!-$pP z;wdYuBpz8u{qeQz=E}SR;q&sHh(a^+mNCn#6 z$cxWDdr#}c-ewmkAu6g__&3q6<%jRZa~%MXi#U&QLiy><)QQ=2AML+LGC`;J~y`{JK3AwuGNH< zUD(qHPd12m=4#)(D82U5sOCwQ#qoE)c4d{=X3CgG6T;A*(t2pK;IPyT^308>uN6RT zdWXZA;7m;sfG3b8hKnk0ufJTfsL8$n5`!C&p_gzTWk5kdI-rvusk&gA2JEfp%#2b- z+K-rw;+KXXR7P&GY;@1rNWQsm-))QMuia~~t;(J`DAX*so3=`MH9Y^}E&Co3$6eiV zD2K+0KWR6{X=h2HkImRa=${m1Npm_jJK0hv8|Gx~oxPo65QPRveSXHtjym6Wid*T| z^-dAL({*Yi-yHiyzfddFXVjYMJq@IuSR%D&`gN-_(b>g0(7DRF*=Z<^efpR4GnibQ z?7uv*&WNrZj@aGF@{u2wwDLx);xtm8YZwcmu+x)~RP($9>D6FQtg;UD(cf!HhFqy+ zg#LvIyhg0F`{bVO+T52}DYG=*)kNz0oYwp^lvxo7Ud((;q}=rVVMy2~ETPi#efTBli5BCR*>Fdh=?S6P7H9ubj|f zKoi@N)R*u1N3s)@2a;hwGzdNU8R-W75Y<6WMb@JFW++<~M?gxBi$nE~V45>6KMiS% z;uVJvm6d!>)Er5wNnTx}!;Ebcrp=Lug4ih+(h(4&>!|9y5K6 zc1x2+FEwe_c56cXgeG$4`J5(wZ}LuEF1}xWPRrfwh`m0PxkWV!0s57wN$?a0&Xw9BPW_Fs6X zC|}e*Y~Q6tYlK)|{Db-K(tdmVou9N>+N&M9zE9_y;BX6Qdp+UikSpl2$nlo=jP#)y zY-a|OGHBp_3>=lj2=biqsqvy9tFq`a$OM)yT=dHEl07Xs89Z6C3O6Rc^nfr<@4>zR zOOYa3C;PzOOlP%H8Lz|3*_SMy3WYpLku*L_?AUYJfJLv~@&SjbQIGC^;-+qkc1<3= zvCWL3(vi7;&RMjib$Z9gqkKz2+q{CQyH-rT{OpjD_OnLC6Rt&L|wJS4q%T5cDVn{Gg~8&$iE01*Vv@D)We?x)5}h3=f69BZ11hh?^)0>PcJ+9nfBrP z-{@s0PySNL8uj62CvlGY;2eD__k{mLJ)v)?kzGZ+y)1i_1n9}kQaW%WOGY|3r0BQ# z{iqM(SA!k{YPm~>II1U0nag>R5PHpzD*vnLx6VFbkiPE`wBN$d8=?7Pf)7kvvaY z%M$OXC$vw*#PAJQKjN^9_!%WnX#qX-uuwAqiN)fx!p8*3OydIufnl(SNyzDSD!_J7 zKCyy82&OaHAt5QR3NF?LzQt^dv((xhI7)CF2TB@4&%n)e{7}r1^N=zwTXkVEZg_^>gC#@cdss6153zgZSaE;?nTk?RSatYx;TX1=?`| z_&7(HixR8mdWL$5$Y4_6;BeGWlydWs*Qp?nUCLv51mRRWZAaeeJW6Z)nw^P z+!);^y&2JMvJtZexR3OBe%$5n=ugn^5bEg6NbjfX%*q6eo7yux1^?m#b=&j3eV1vZZ-7aW&hLnR0l0O9A1Gfq^QjYx7C6~c@aD8vTyz)xoI=^W<3A zce1^Mx^`XEVEWpva?X+sGr9~uef-4N8(9mM!RE2aEobzYwk-VmUH5NZclX9k;u9q; z%geg~Z<;Y@$_be(6bXYvg?TwSLFThI&IlM)V}=#MAx+#snk<+HbxKZNz@6vLQ?j%3 zdsJi#3K$UO+WCUQ5GJJw$8=RO-9x%pG^C@Xa4GP648hb?Lv|Nu;oOopuN&^PCCr(5-)$L-F2AR z8V??v-lFm9l9Bxj2M;c6+vx$;e3vx*nVFlaw2!qj?3i|J-V%TSa4a;s2D7pn8Oa-k z{HSVcm+Z)~9A|Oi&F&Upz>676NMfp<4bCR<8eIEng#H?-fXu-AioM0M=Dib!WM(uD zYSRvezy6Rt!+Mv_R#rDGS{dIkxgh*>wI18i_Xfaz5-f4cl;di)w1my)&{-HEt%-3r zizl}%(p&Q}l3hf?pj}3DJvIj|_=!wPBFhNQ)fSv9r&RkyNxl_XTOqg-&c=#+n5n!G_ z*!d=Goi^30{YMLH_D0$O>5Z3P_g#HE{$Kldyp<~DCX+H)6nX*={{;TRFEoTkEFboI z(U5DaA6KuQ-{Yxokn%y|1r-NUjcb7Vx@O2c<~S+UFQDKGX-x?Tspyxk57ytKyU0mK zY$_gb@HlmKaV&y4P+T0!;Ow-RN>kTR^i(!aEGVmmXiJL5Nq^0l(W>C;j>au!%mBij zDK}d+`1{edG41#-M_hgL@UfYfI(7MF`df#deSi(qwm$GIheN6p{E*xVv+Nqm70nJN z=Xm^CNlE@RsHPg_XG3wB9hZ`hiiL@8LG(DS22>h#iliaq^t`O>+D(e?5sH+#UA4}c zX!J9$UANe{M;JA_kd#@|;;Yg_lC@ za*IowjvQWGPfES|73LLH}W=$95zNg`ou13V;LvT2TVo=owmp^lX8l6{zM1w@O_jTN`WOq`5K#Fx2F1 z;Zp_0#Z*!#<4RGXARar6#bwS(FZY@>Gh(y!WonDwXMnF46LKY>*O`}JfC zs7k8#mbNfO8!b<%TP~HuT3_q|tPY6aIX8{~yo#KR_zi^zL`_K8~M@}t!;K9|aHaxNlBioFeu8fHV2t@k| zaPnikHd=b)%)d^4{^3Wb2scdw&)Oa977$Pq*lr65aNxwTO-%5!p%s3+9~Nx03J(MB z#h`;Wk(#=LLQP=8r|yiRLRm*x1j=$zsY`rvyB1mXILkcv8B6cKv*`3at4~d4)=Mo} zvTeha`yXKTO;7D_ciZigkIX#wk)(Cva5?~j;M-WEJotDdJA`1C&u0pn8a2!oeCgJ7 z6iG`@w4#8f)5>`reTh&4D3$|^%N=mSCo2Ksfa?_K^}>k3^#vrU7IQm{c&UE(o1Z$+ z_or{SeUbdUW9GQKmd59`AGEM+QorK&J|6sN;VmoL&zw1{0lWuND&N1C*_oeL4}AH{ zE)#An%7p2n#mJubKa%W!GB>$1A3ym=-K8vo9Ui2$%uhKMc7Fu;Exz z$wU@?jx}OSN&+ZH83d7Oln4!IA2ptC7GK&MzI;gfK|ZH-(C&ICJOhp@{8=lJ6YK(T zmlgSmlN14-w2x_`caddTfL9$x|MlG581L| zjCBD!|7@Rb4RNv49hI}RxB9W~^*I${uPoRrGq6THDnKI_A}S<-9wX;0s*;*a$mt9O zNZKlb`RWbAG z{CDcPY(^uB)yY;t38o@W%jR^rY%VF$?@vth4y#B+95zxD=*t$Bm;e*8HN!_N88B2f z@Ktd(j9*#wLMtU>(dYoWm3`VAty?1?&yg&=udTQszG-+_TXE`u-|t?fX#++qVmYsV z$;Q5q+~y0lW@GPd#qMEk-7f8$rL5$RTeW{JWd-wYTRwZnz00m@U<;Tq5_OWsDer@S zX&j1oS`-1rYFx4}&Tey>j23u-z~PF}6)`V5-HvYX|HIjP07g}8|KoS=z1w@=B)iFO zHk(R1N$6QXnsjMK5h5z0R1rmyDpf^5ih`n`q9BT(uBe~{8zMHuUcuh`d-@(^FaOV( zySqsM{k`vBB#qtNnKLtIPWhbAA!o@g>9CXqnu2IPeGsE{00>@I_FP>%Wn*tPx3rJ4 zs1KWVMA`BX+sO7^{;`4vyIJ4AzIw;Xy)&@?HsICi7+%) zoEHwTVr1Kl#$sDoZdd9F=9K7hYZNp#cZktR0g83}Me1bT; zrNZD?QGO)A+M#!_(bz6xlgrA3-cYGN&t^kwH(MxFggmr20~<2kSQ>lih5yrR>RBr2 z&L^cB89eMRJvegOl}jGG@U4f@p>6-|x5~2I;jU@dtqe#VHeIo0-J>I?UA+(qi*xS0 zX4hu%%I2yApHI4J)ApMuH;$V)x$)6g#5d`mcHRwGD!7Yro|t3I@qwk2BGk4rG z=DU9T_I$Wv}acE+{*i~fqTXB zow^J`2m}zt4rasNZ(x}=Z;ltP71Ax4g3>0T9L(bBD*LDK4FMF9@}llpgM2-iQgU zpVV|d^qSPM`o7t_wz7=s5uLhTrMAryi_RZ{E?$=@>wMCC8@o+x(%h2S`4r$78btr98GF2R&!&52!|1IUf-nHGbF-9}ib`12Y?m zP0cyg)mDdqsY3I}@D^6~yo|?rg6e5p|;ATD8%#)TezvJk(^PvlKN|o|p5HuRf zDschm5N;*$63;pKMzG{QZ#BF)R9}W_+IM3Zqotf-{W>J;;k6(UU!x}Xz-wZqi0mD`Zj)IL3Sj&|7BNKRk1s- zAOFHv=iS_UN%2`l9jo^ARo7z*d;tUYZKU3!V2rD0}bqM|O@4gl7FK^>+pheLT& zN(N0@`0(JBljAc-Qp$m-fE?cFjXQVTJyr{GhzS77x5~ zc-PWGXI|~l`YBgjGA+F0wgZn1nst8vs5igE$X>JN-*`jAgxXnGz4(I~C&T>YmoUF} zgeA2HJl=c_{OW*QR%*}B5Ir7nDL}~x?b?YVbdE?a_dm=I`dy1bV{Oz4hOq_d77DeU zU!DBiG3A)@mGV-0cFK-T@xasNO#5Hh6~@%;K73~JH%m+EKib!7R!1+J6yARO^SfIn zb?t&+A?;5cbj`b%OD;GLM#d^kQ8W;WAk^3K^P&ubohogCOq&0ezYV43#$Q38$(I}`Y4Chbz^{lNDmFTD>gp2 zclV-7_iWa=`B7Fo+f`N6y;o#>?5+W0XB77A5uKrQWAFZ~x%HAsQ>w@3mnrWia&Ear z{EQWs<&UqPGU<{FuGqhS@`Wc%B5g!)^3}K!B(5rW)Z?pd!{}wDmX1+ z*`5&$BKVG0g;r*Vk!SZvz|QC-c%&S2LJP;;(zX-Bq|jC@H7_0bX~H!w9*1o;ty=M- z$)6k;u&Tp^aE~qnqSror$eMm_uL5`Rt*?;ZC;0tXS20RLfX|hdTRB!G0L0D z67bh*<|+3n zuPA$zCs(j?mdS2XWqzCv zNYz2-rEWKjn2*^FU_U%KAzEAQ(O6a6m)!qIu8q~uB_|gv|N8#t1oE=o@0g=}`0V{> z-?@3?T}!t1?OcB4C7->=WW6 zxKpx#C&<+Z83u#bXmzl#z)c6bS;1A&5O0T+ z1MlRgOAegv2ic+P5}d+^n|odykh-pAA$HfrS;}YFdrUYB>vkVv9(m}(iU5locKHH6 z*&u47`PmtWba?+Z($VsGY_)%iaHwrF|0%GMjCA}{P=iM~lCg|&h!Nn@f=$p)h{eJo zkEK8W-BVd;?aXJFC4Nc#`^#C{#>eA#+|Wa}Nt-u#?Xtbs^~#Z=^>e0e zxnwj=Cem(V8`1)Y@L4vdX}tHeS^umwm0 z)VAt$q~vT2*Aya%sen|fk_L+g%ZOW&blQp-KWj*x<7V>UqrHfsJnXx#egYpH(<;B; zbQ_zg+<-)s1r+i!uA+kF58ts!dGM6+^om7+c(MU@BO`L_*1&eOX)qp*#sw!A0eO!J zqe9H-4C6_Kra^@l4iSYO&N{glP+VdpJOodUU<5O7U5@0e_6sKb__gxgayDYo#vRL* z4-m3RD8E9M+wf?$=4vyS9r)1YAeZHS{`~~#*lT&63d~;Yd%T`%X#mqhTtKhU5M!O$ zj20&rD=JjLrY}(gHGxVj2&Y551-yYoYP2N1umgfYPyk`)YKLZ|(rby*b^8gno5lYv zzn@quPC8MjS&(>89C4hO3t($wOn+eg+(Jz(i=Jt9IPB;&>~*6W%Iff-X`xMp%(2>08aMVfn&DB zP=aoiaE6jPo4ou?V*h~A1LfUT5vtl7=Y}8-K`O&i8Df#4h1NbZ)Pb4}$7jv@JHW%7 zrT5*pRDA9<%mdc@e5~hqT2BUh0N*Fq7qziiUw}&;U=K&FV)}LY^g8-EyPyA@_KTwZ z75T10f#UhuY$W94-_0BjO+Ka9o{d`_%%rBIs3beN#}w_Oc!v5OH3x83tk~{}dBl)s zh-bcMm*<#ABX}x2jh9bkBw)OblkbnRlRs() zV^8KHPteY-&tZW;E^1`-QeK1*Ww6owvVUv?N-=^VDd;ph#x~SjnbinOpqh6Arj43R z`3R|q2(Ab9hCosP>kwr2FjlCJ`(#~tLQ5vEh%l$(;j2yz|NH}G^O)7Wrgmv4A3kKk zs?o|XJ@+XAc?lwGn=^9vKK-t8ZO0zh)f8MZ^`n`ya_tGT{MAMZwQ>2N&Qbd->O!H< zAk!Rn1^$6@f?CK_8`dI9+LGgAsWn4hWMOQu#)5P-I`d?jEpAJ84r+Z&ID%sv#!4%B z$2ELs_n}4l%DXZ#Ew@|7u1H3$qzugRu?vbYu-VzodnZub}ui zfdVD2)3GBuTO5my2oW}|7a?u(wOZ8|Qpi^FzoVS=h$|A)oy_#^nlJinyij=)?GIkQ zmrY_lK4WEl9c|fTY1q6RMENi`1FAVzps6T`# zgJ1S~Ay}_uLg3oulEYLsn$*lGXH>PzS}QvS!whjy$w=MWc*3F^3`XM5tdc23NMF?I zO^WhG;t8d(vU=>Q>pKNp(VCu}I&~^WA;sJunP&f%Ey@Nqb<0L}=ibld&y-clbmg9P z#`U`Hx3MC$GVGCf=*a$S7bxN~J{O!(-Gi8m6SEl-D+YC<-sbn4Q6MVFdMOaJ`A|S= zQ$wjHy@dkCbUsp(p<$qYHhLtSNT~*D3+g9 zH_oa%>h=ld^jTG3ukrKOBPYo9<#4!H^kjoV#J-$q~7K}YB4I^h{_^Y>&i z@G1%3n1z7`10VsN7ZI@ZJ8&l}6EuFv&jkCXP$L5Hg+zde#DLwxLO>V;Ukoe9AnB;a z6KyHSHi%BjS8)#<+$gG$QVcYo z3@Pw-Bx|^Fj6&x?J+C>p zYe^SlJ~qg<7v^KddqFS@1HriqOfo?u<8A#a9iQ9PKH-y+^sBGJ0P9=EOm^UYQ_v zxDb@Z5fq;1$laj>epc?kBzb?}PUdS+h%qysm+ylyM}$?eq1jmupWh!0XeEQ)9&zO6 zM$ju9vW^6#XkK}gi72x)2+@jYEV?-=Qq`troN*z6XhHH&G6jQ(l)cuI`qN;9;7ynQV zH)3>-h;&YLYKAi6Q>%>BFVq#s<)O%Q^GX9xJ^MAz$&6x>+W*B(Vf0UV3yX45X^H57&0232Lz2}h1pL(c#_taVu#^F`2OQfFk}KqC@_bo7j654q zQ@cURf-Ep@OEkgZjX9H-B{nRFAE)fRq}t)O1Q&oNH?N@136&4myoA+IoSc-A8s6gF zx>3l5kexXBRpmb;@0-bV8+PqK)ThtmJ8oGz_o3=Xdo%sPx0_cjTfbfT!F~7Mfy%Ed z<}bc_?ab>YUU${?U3YAI>4ACohkQF1J^u~1f@-gG-C&_Outfxgiot+lR4e#c%55Sn z3@yXi1NSecf-0?QB+^*&j>qEQ4{96m$*qH_9s%x!ZF@*B{`~27Z34^+%@s*d^_{(=(z?`eORxCHR}`=OJwH4DHnGE6~UBIdIhScv`~ zVrVo>1EIiX!!E;qgC49&so$UU0g(>F``h?@_#XFjORYZEjNeau&AKADZ#0`sk`dl3 z_LRp0;I|c?pbRW6w0p{&qR!Fa zC!YV1J}-~=daDok<$ z1e*eIYlss-V*w|?s67u`4F5kk0qD+u5o~CUurIt=jWtf z@7Uev(+~GuzhvFN_w8fNx&FtW6wgUKcip5)@n1L@YAgb@E0Dp6Sfw75l`udAbIds@AlyT zltjn|;`&vYpd;JWCX9+zm~A$!f(cCZx#uo~3Pn`cBFeb9Wdd2dLg43zD$a>ie zWEs{;AiGPzjv;On*#1bJk}z7iFsc$ug-~d-^UM-=WNO0X{Bo~Fe3JEmiBIO%A4-jQ zqj)9!ly_M5mW@*Bk$vz~zS`KFnQ}{TR$&`zCu3Yj=mj5qbq9=P5LMZIrVLrgfKtsM zK(gOt?W?l^TBVaU9*;(HyJZ;;7dHu$3r5NW0C%dgpq@9XIgeUZfMNQ!Pu)`H}yQb1^kGY4qC5x0Arj#Bagvf_R zN~7hFk+vY=wH{gMOoL*v^oU-#cA)~}n)cXbY77o`-L}E0BEg}JOCIg zPA5!kHL-wiIaOj?bCrrMcm|G0HRMQ0;*k)c4)KrtJn`lAAC^Aehn0QCdaz0ND6hPy zjC^uqudi=;hnZZ7>Ea5Ha#B9N^y)46A7wKJE#JUwU*G;!LY4Q)A9ar* z=SJMQ7sfhRP#=k@xCJ9_`*27IV|O?(QwJJu!d9~|yC>HVQQl zwQThBe|&uOkH0=R`a97eJ@yD2&pN9#B8+iB5eeWEf zC-zG`A^kY-W>LQYcRgb3@|C;}?JPd_Yw&%&=2Pg{Txd8~K(K|hTG(X>t>x{?6CG-u zQ~*AmSwhCjY<32OtX)~ZX5n~bIZ-*_RMFM69u!yT*m7)D6-U~ITfl0zKuTs5s6eHg z81=}=zI9{L^^4YRT6fdByO;m*>5=0<|M^|nxI2DebCnDJx>znb#Qr?g^xR9&J-=6c zPdT9+SN>8G*oT9*Gqz{;fSS8&$G!W`oaN9zgxUB8abz1<{=>pGF}F5T%Cy_9*^=Zo zT8+7piU^C43MwZTHBufq8UYYZo9WN=4{ey5IS;2yN}V#B4X9R|&58_y^|W*Wogaal zr6tK*E~%oR5-?rW%tw6{!REsbm+AzRpr9e?4v6g!9-KaT!Ge#TReHvMym8vhmB-$n zKY9GMa;N1BFPQDoIj*0$=04O^_8)PQD8B zVKu7NvaK9qk_*|vWI}QuCG8KRrE|(37G2fIp`_elaad+|m-78Vc46d#_n%(dqi^T3 zcy>km9?w7h{sj?s;X&nlSNF^~`^2nQY~qmMGS=&t%P+`dA1e9w>d~yTvUgc9v5Wnt zHxr=&6Dkq+9D^KSo^WIA9CLOSD#B&SmX&45m2#s<0zeYkAVoJ}bDML_;z_dMz_^7N zeWPcZM|MgckDov}e!HL@($L7gV(Ax|h2R(?%0s#e(^icMDnrAu$BYpwNFRx`09vGw zas=QDhih6qVz|k?PEtK7?9kR1OE=ZL+<1lZ_}%L-x}f0-^F3v(|F+j}x%Ao%@44qJ zXt!m|`3G+pe(u>L&+hGJ;-pWAJTyj zXFniqyObZKOOQQUo3}cr`8IyGpj%1*e6OL{;dg3WPxu~tnc{j@D~fqQo#{0vNqo3( zd?Q|eBi`%+Pe@0rNz{{X>~ed!du)ahF87E=iJW7smRK(i7A5+0Qk39EqoT*`!A+0c zq3YMGA$(jO{6546R5v4^58sR2Uv9hkZal+QlRKSpYeB{)oKeCZy1)@rEx1I7(cyx^ zn1=MuDvvY`IUZ5)gm{jJex|iZ4yY!FO$SAS>VB@EQWrLfTeUzqqa8?Ukx^Fh{uC~A zNE4JBI>*kQJhG}?MoC@Ix=1_mb*PU4${QQcwk$AA!Rz&`*NFp3otVrY_KxLK?hr$z zG@{mIhdV!C4KmjQV=IbQoy}oFSrZ*?igHmjq?JWZB7;T?|6dY{iFVi!rz8~B{rCxF zWkT(7HiNYzVm}(eI2f}Gu{2tw7~-}fmEFk3;HtskcM4sKXx|T$(opC1J*J@wXh9v`3)xx+Qzlq0{4CsEqVh^1-z?V}C(`NlW=yI>&M)5KrqQJ)#+`-oQ{P zYY;I4L>@d~j?@lK7tp978R%klKM|~vUwz?-bg{B6u59DipY#NtkBSSaS?+}AkH)!_ z1k_Y$y8&MvMO-B&+!MRBTIkfSU3Qi+AMjv7>d{?e$;pr-Qlu881NNZctpLT4-2r@? z!wXKQZj}Gs(u1w-!4~ykGkUO#d$8d>SoEujuUqXf8kR%J`>mqbycu|MfS6zU$*Kvv&(6J%r0_p>9b@46} zRu)ui>9yUSihn*>tJ$v6l&zb(=P@>U+PYGWX1k;*TQ&WO-N)aVeD(FSq?t>uog`10 zc+Hs0>%>_LW=;`vQONT3)1!td4Q%V=o)gYvE|wwIo;U6`cCRve|^fMcLFE_o$*VqR^LoY+-=kt4g1?-RU{9K^MK}1SGuVY4?5s+|bNl62A z5~>b7prusx8X3cFy;W{;z|D25$82?+LCX)va?qk;LC zu5E5R9$E`EjGP)1$qVCJ7iVj+HZ3DlSBHEiWM$=tFgA)|To&_~4Hk>e0oT>%rQ8wX z7y*eF=ogUZ;ThVn)7)f!!z_`}GT03ESVW%ST~URK&N8$?M-ro}rIm}zC6~%)(-6s2 zme-EFe?YlZd6G3By8fCOi(e6^t=V+@7U_wFO08lWebJavkSF&q zG*uXLD!x$xlNqo{x6g-#^Jf@c7^bnw_=XYYYL0O*v?csIokwqT>vX2%ASuGZ6R@Yd z;jM;O-7+|ap^N$I0ORWDq%LuFfTZ2k@hKKkwva37^Y@Ztygb&IMrgx0+@5(Jr0L0| z80?wqS?Ss2+39%$KQbV)8;cb5{hPt-5zcB6uj+PRk-UFYLr-1&v|w9}T$qyKHD*G2T&ehfp!Xh zi`I-9ERa;7_SFVn0h^te5zxrcgw1Y1C6-4*4QvV}nhb#?H+{O$fS*KuO^5|lmxb?P zWjotoTetsk^zAPUpT^@y!)(h>?>%$P@)h&uE?zww3<_T=-!t8DG8JGpdu*Up4Otg7Vu)6d#gyeF4ow-n~}SmP-;+YY7*><=_$G}vRt{&rY%?&piZqoLhApXW{P7>Qm zMih5ANOG0Fn6M7gr`X?_!lu}0IPX5c&FPdRhhGY0*#fX6 zZE!MECv>GvW5Y2$YQy0M8@g5thatyUZk%UaX*_Dw80qX9jas|lv-?5-8cg%e_wDx` z_UR4kC1J*i2lhz-ike*1wr9J|(QX|pE(+lf$Wkj@4yXyQn3rYjQKo5QPV82pLl#40BsxwyV-1^>`O4v7zB+oCS6G( zz93L246|X(Zb!spr@hJkhF!ARbL@lBf`yI zJboczYOG5j!{P9HoVrY>6xQd)a);zL=1Q^L`MKiCTqfjJ;73p;0k_9J6kvRhgyz|> zez-|1Vd8P95vt^X4&jyt-4k4oq zG?1o9j1re5RYZ#l!G7m)4X!JGP;~{XPwEOcbcK89sTeAD+5KBRDgh6DfCQjO%`Lpyu9ME4TX7fuySle(3OF_dWOsDbfVb8 znabg$Jwq%duMQ#Kkm8JVabimnxh?K`0ItDLIsDwoeMF7e zboOV9e+7~}@xz{ZGX}naie5Hphtl-EvhCH^mF@2-O%D@}bIltAXUyC4z0$ns_dAz< zKA2Up8 zN>KSSsCDVIIq+>NE{@fp7zFjiqCZrz$MdSmtSUgy%bQS7bQQZTnOeO|nW1qZu{xhE zV=;OAd+)9Jo+np*iTON5$j2$)4-QxwC+eWQ7^lnCGR{m`S$d2bip-7(9F3WZD9Nc+ zH*U%;I(?K7D#i$ST}qc>199Mzhn2=zvGOi+ymsV+7nM%RTkNyHj?Y;9AGs5|MVWe& z^3}FYckYsQHXnX{%y`=4&mg}S!yXRnWsvCgE$`Pd82uhFrG5-+s z_S)<}>_6rQE}2kRUZAi*5s3mWbhvYcBw%sv5sE{*OnTm003p;@d25 z<;IJY58pst+9ub_$Q|FxD%l|A-Y#?JHGh5kH>kD98~8lhs0Ww(rHD>VKmyopJqbb6+t^7s6g+I zgti1f8B2+fE3P9a3i4XR@RKJSaW%|!wjp0E`@ zl?9a@aH!CMjWo?aj0rJBj0TWJjLc_4iIMf4ySwCE*+EN>QZ@cU&c z%j-2|;zBo?d=8Kiq9y{-1GCo*8VA9PrT{+l7x76Z8HsmV@Z=?0%>~`&hM~Pkf1tye z>_DLok-=aKSj8ZkhokzEg;CNcdqj8q`~zF|-S^`8afB?5N*}p7v5@slTq9}96MwxH z-}2OGO=5*OT{HTL`{S>HIMe4BXD8+`pf*K{OKKS8oC)85Y^=h=0O@tP^q2&Y{BCe1 z!}357Kx+sZhBkOzLHx0~-GLzuZkwLj)IEF31$4$q(1r=8r+_+Fngd92S(vErSr|S1 zws&5(@PXMkcH6%D1LctPzH)Hwj)BU0W$?SbSKV@!tPEv4d#zj9ySMT&c+nEatrHLI zlrBS`SdnTpbbn$EvW2iPF;-{N8bdlAWFMAgXF%ikXGq!55gAS+>P1}kpcDkX2D}nM zqd{2(ZTbwSiL^WIoVM$c+W5(ZNUcb6RcLj>n7|npr!MTk+C0-FeoD)s^lG(6Ta}g4 z7Fr%0f<;(}MQ2*1;@OFtMTM3K^IL{J2AV9)E>D;o>!7t*%(>`TioWf#?9S0-W@gbP zCuF(JC?D}SLlU^>^3(}ND7egQS#I!g{~ssVa>Qwg_;RWHuVwLY-ovhCaj{2wz0zyg zp^Nf?bOeis<8wT1B!s^(-fZJQ-)kyh>zt#5d z9y6fPO}qS9e|f~no?U@A>WPeM&z^mYG_nTdC{=?8_XWhfd*8kt#y0fL@pv*LF|R9P zvqd5}a}iib-0}d}xH{ART2(nv)KK|UwLu0e-@6X=4o}%6>DA`OH66cG@zX$&!rnvS zJlGw$r*1<|gQ6bv5e==VCK^~%BH9H9L?qXWTnWgf@V0KUw6itFy6)NAp4@ov`N6)m z!%BNN0#=XZ+&kk0xvn_p%9}RdHo5bbP7^P>pmfaS^=Ai1PMX_u**(KI-o0khsQL(7 z(}KmmVQVHErtE#cXNQ5^vTebGfoGPh(CqWCAKf|6`qa~^tZ z+Th~#okpKCsPz2HA@*LQCY^QNU7H47)Wz*89ndwM_CM5)HMGe5aOTSKNf5x;#X~Y( zIc0e>a`JNKOc)r91aIYX?g?EBQvtVemsmD>Yk|c@&?9Z`1GElBa}1rbES5HHvf5q> zq;E)#IzTQ`fc>X}I0(mse{c)b80+PW)=wW@ePh#&6+ zr0!VvF6|?o)%^6UJHhSP`CrNxXpe>Za`4XSpkMqnE~f)mm)C@<4HH0;nLgP87CJLH z8gPf1+HlLJ^?Y(%Z8&9^a*n6Q$=NtJb{8BtAk?2xfU%hW7v^J#hI|q^9U)!k(YUy zd6Hg_J|}v+1XNfm5dXJRrWq$8hv6zG%Y~kzv@D-;v`TGGqyuq7wkQXW|NPVM#KD)i zrdL9Nj5drKL4`zy- zIX4-y$NTsgDb6J4tM6m4Vh`}hYj!q8UeS(VXgtd=@(RD?`{-OKqx8!mPxN8O9vK`f zc4~9OVY84IjfS!Wt){3r63X-Eg>8NW_x)K090-Ffp|~i`MM9}b{8hnW>8!r0THwfo zs7j@bY>iQ6DjWujLUr`OyU44RWB+YdzGPX7!ZPNbRO%Jk?=KD-bwSSe!HO)6lSD!fW zJ=_+F_{nvdSVJT1{{o7LI5go<4FuXP+{6$n)w+ z5UbFJF+=%7`Q-E0Uii0K+!1?eN^b@TCtX$%L;tM+gFe9VIB>4DWB}~j;yI2;Zr_TwmswYHsO;9QGN8PLJ&l~x$(8b6c?ajF?H9|7w9Cv7WrhItL;t0+%#aP{YEjVO zv3c9&8_=YTss~%mxMjj>kdXp$6l4S`2sgmIKChDkaq7(S#X5U(!ql*d8prSnmmGV; zeNOx8bKLKI@cFCmbE>O{x(|Q$%rDLnwKcR4Jw`*hOD9}U^Mx50niOBh1=x2lo> zy$1_}{gSpz4g#lI>71dxs;asS9z3Lfmmyt-cvCLY=Vxk@TEL1+Y)- z1o}6vE~0LcY*rm5_A<2WsEO2Q`VLxt4ypt$2gv?y<=4A5E5E;^ymB|YoW+JMAK1U= zr8VE9M}zgtd0%z!{?)uMnf2rk?I-kPe~y3ZlIb_}zIv&DLFnj~?hEHl-99>TtH~R^ zeApn(s7L_4njSu;Jicr4gPuG>NOHzGq8KGcD@kXThpPw zZ1S`%j~B-}R!ZZFyR`2#uFuWbR**gNdfJ)VwZKeO2xp;hYwt>-U3o#7R95bj^_?@F zdS~xmm7PKKDpc4j&paHm?c3lF5tm3hceK z?5ioZN5Ti;TRCSZSLLr-6RPyF8%g|tIMslchEpSh>1+1huwm_o(|-A4@504Teeuh*57uqCVb$t$AE+#S@VwQl5{fEC$#Vt~i@8T$xOKsQ zcY{;yvth$1Aqb+lTVFWx-kY6DOS&A9r>A5$zqLepPLSsWwBO+_LRKC5i?209aEBr1 zQD2WT${3N+$7;f!HaQO-KSv#KK5Aw7d&Egt6UupnLdbc9g|OGidypn;FDED?G?Q>| z`GoedER)eEH~`@_N*R6~I$~3m5~E5Ia2waZ#&0Lu@o6Lhq?_h8R=IiA?F??-r^N+>;@N3tJ&Y@J)oKsUgvTcvDwyqVO}M62Lmxudg3r!ngN-6nM%y{jv=! zHmvPh2FjpKFuIyy;%9K$-RFB8|ax@|e`D`4Dlg`WS+7I=$0@P^Lp~l-<}# zjM>cS17!mK3HX6>c&yNV=?|&Vs8;~fMI{aXMqJT44gXs$NzFydra|oDBISl>9C_QwV5$1u;B4P)8!3NjG_Q|^(kV~v`x5H+yG z^^(gi$p!c%Iel^N{JA1d;U4)jCXBzV{kg># z_P*#+`4wE8zP>9%?y@=bdrxkX`w8Qavor|SxF`zqH9~pjs~yX6MR+1^YQou-jcXV* zGm@=&b7R#V_ zT!Q^^t!2IoMoM{gRVEkD84k5rc824s>DKlb0%p`4RklkXX`aA5T!_X|Ple97RR#+a zVl} zE?zX{fn4EELviu{)XW5=%`>hf_VgF@B~}2T3QxpdUFC75ziRo}nZA6d=+frEc+zL|d#OZlhci}|Oc>d$WBpAs4Tlk~#6bug_JE&n9`%s(Cfntzh>TmM`0 zG5+1Z_(xhBee*#vFqO_KNj%RyYfy*`6$;11--yqHP;~ClJLHFwUvwQ=QEu#G>sT|Txfi}P^v0J{%_A#^RrZCwaqwQm-uFJqc} zzWZ0Vp!ik`KzIosG{G%LQ=ICK6SOI z*7omz`k@<_qrRV=t86rFF*h}tw}2T(t@S_AF7Xt*U9I=m9A~kYUQ+h+@vl%`me*^3 zg?|@7uY*z`O|%FA=yh7J$K$jMf>Z0v4*D}a^HDe~A>ZNAn_?y%`T%&)?GX`PftZxx z0d}m%460J;>(tOw(;ywssR)vT=d~Bn^!U{(jEA3l=;mvAB?jBoG6VRDyLujxE@{40 zDP(ox+PW2Xs?4~$)f}MY--_yM*>C@1m^?1j4PoyQ` z8%ZU6&4^2iwzuT-8?7PEzO2VOa8yjPsmC-ISH!Ka5dO_|fkzGaTY z+oiKFoC<;|wVz#B9*CVxeLE_P+i65u%9DaZp3`En3fA&6gIAZC2^T5T?yy<2G-6RZ zxkN63>Ou5X?aj;GL1H9MO(ZF597+0L9l9L@CbKjJ*+(B(QSAe(I>E`t9qMF)W5!)P zX4JLEM{4A__F;xNd&OM~4jy=B*5ZlR)-T&|ZilNc85;o<|A*z5pLa>^)B1bB4zJx> zD_4$pU3>xj`cFQ8?!2ZA?;I+gcm9NN*hf@9&!j!N0OB5zm_q|#sTTDVI`As#umTL6 z0XzhxN}Ns}Vag+*yoe`uG-(#FI}$IS2x#WxSK)UNM#qRfsS}{p@q4vQi-rNzEkIP$ z)8sLwWk4Y%{4z?_^oX+$?o~PfH?TSZI1t$YCvgtT!82+Rz%QXS8ub!!G@5Ky1K153 z8<4^kO-@c@M2Q4=WGM_&YC9v(NQX{Jq8E6*M}$2fElqS_Ta?jF4@mjq=#5HjO=7aR zMjb!S6tA1Kz~;-5bUL$9L!b_;P0M5INV@AS7GN?hHa&%^_~uMbj9Uk_etv$`Fo#5I?~C)A1iwwSh+I$yn!qV zBiVY~l@m==5`WVC^OcAAn%|7^XCOu$LM*u0Dr9*)UX0(1vySlx0W!$~kuau%Xpl`0 z`T>uOU@3K~ORm{JOd!hnx>J06$(9Xt%=!4?(#aV!-n!|L!)1?VUps#Fx)B@4%@`jS zFI0xjxN`Luvqx|G;kxB3EyMdivwPZ>ks;+G^s`ZBM6UW&UF!fi8wNq{1?WE-%gS{^ z#-X4yA0ZeCErtqIex!n+R8*LYvd}1yGGIcYzLhqRb%qUL#0kRA9O6SNAWcPBB(*?W zbx76jQM3@)8MH3+6{##$b zI99I-v%*%1(vu9Dg)wE2)sPa|o0qK%>Lm5gQw-%VLEdrw51( zk0-aR3<*qNjmuow*}1tmnz=2(A#8)BUf`d2Z-_Qqpk?V{ZvmG^X6pZ&3yRa%(=Ea3 z8b&Vgyl{NzKhZ*c@@&rh8CMQFDMaQC>8`o=vA<=A+HurB`<$^;a@8|Y3BQ}_f~$q; zvCgh&ZXg&m8Ot>2Rb?+K(g;SmeNEKmbm36DTsgWxASVa)?>Y8LsKiQLtQ{EpxPuOK zH3>nePB+F-la0{OTqb80hYR9B?XPOK^DqCt44An5NfO*P%xFuJL>+Ff^NWeCLhF&gsh?ONt<0dN0 z`94V1UVHhtJstyEGwLj0{B`Ar7@H@F11A zL6L%C!g0xB8P1GgK<{g8a69z6EFmieRTW(sP7C2wp|UX+>Yk+PX?$w&2xzp)eVgIl5zPg2I_CyX@J*r%&ws;ieVh0CA9z7bIPDUeIdRWko#b`Ie zac20dMRdZdCqj000CacpcXsZ$KC(=voQ)krBqO?@1-_3YYvZ)#P^(ML?#9NUn4 zKL#0)ZxgcU1+GjGbhlY`=wt-WD}w>oiUGJ!LDhgMBe~7|32>5nHf`_J+{JlHtYz*S zPk@~qa))oGF=F2IOwdD{%sNpo16*yjS!@imW=ul!1_^u~yfOq^fp%*08cQ3nm@b}4 zPb1e2*vua68}^y_ zkYduH_yPzRXnKHON$LNSdqZmtb+67|b!9joRa;*N{ff9mc(B_(ScSxh^sXi3WtL_aEntbb)a0sxw!y+ z)hOB$&-M6|79yRgV z`^Vog?6T2adtT5-893^a@k57=9b4F?YgaahHNMaKZ?9XNt33OM@~QHXtC7Wa{mg>P z{@C!Ka&VlhT-yA?z1x)c`<>4`cir_i$sqz9j}v*Nd~i85#@ZJM;fTM&T3VXrHW;!h z!gBj+*&l)JFJ_|wMs~K=YzC={*{;PUr1grR@)uPetE3=!kK4gy+1Y#IdFKqB@bw?>zR<5tFK*oW5rn& z`|8Td+t-$t)kNx|Q2=$p$;3*t=DB}-<<$?RL7!ap=;xn4s{FN9Dqz=Kz3j#rmp$9Q zJu)NUiU*Di{TtxpT^;Kc&CAmptbRam-F|~yR0u94ixiE9!{CDp%kE56mZ5D9sN@B` zT_4jA(XY~P)*sd%Lu)lao6KgE9^sb5mQEE|RF}6L*3{2hHU>YcOigq1;);~*j>tW^ zKOl|+WEzn>l_(zNL(KLKD_pm+zmnL=y%_R$UQ^z^lWL68T=6_Bolst6(Ls0a^>QC( zDR*`rV|_oQ!XlM9o(ZGi(;R|NQz*=jo#U|EgV_;{4~%M>Y`M57$1%^b(s9%whl10B z^Mi+j$AaL@vIiUaxBKw}lwb%#zAhgwPCg%pbfC&-!A;igIqTU=@6Dn>E zCWdDiKratF!-L9i+^L8Gs=G#gQtOAHXdYLJ>SJ7|Y$H;Z8TT#*wZg*YSNWw0x<%MC zV=)h-kQ=iZ8Iwhk?l54eL~bL%=)={Vk~CdS)yxMymR5@vJTkCKv9@i0D=It)0SC{*!R}EmAsCvy)1OVs z1;`{nD0fHYOO4qs)W$N66gvbjuG4`659)f0GH%fdl5lQ9pa)g4w?mZ1feUp8|$sCO3g;O3Ez$1r;HbmiFYUXloDgt(+u(K)V!SZY_k!h zq=Mbou|r3IKrQ;h&mCv)!S^;gZwl9%x6@rA)=qXhD_#R-Y^;>Y5BiTA{!#JI#W zss+ddANK0+SZ_WqB2xILaZ%0$bSP?@>U4bXe;gN6l#iZc3zS=4!WlKctlYwiFTS^o zJ*}K2wiB(&`Ru;LzmyRvo3Ik`$c|iI-C|)7TxwADs1YrK#_rI|7R2LC7ALg{5&#_J z&IIY-dh~6?=k!scTbNTD(P5(F1RJSRzEyF9%XcR@eH9O4w%5jVAOlMy4X{MJ(#gwu`z6M89c$v$8{{nV=(+r%vqrL`bsW;kIJgOw_2*==~ zn1c`95}Yuy9AA8G@2xx72v)gtUZ>8TtEPcw_S{F5XI{&z z7gsBPe*YaaCFd(AU~3^sirw2TTNlg|1fSV$vzdcZepIk{5g^V(5Y1u8%nVcJREtgo zD4#c^2{~G(+IpVJ%?s_8rlRgpk-;9;K+hvDneT&6KV^yyq27td=#+3?ui*KS^ze% zhL-diI;hC0Wp;nFkoDYRZ=(%AT;erQ=}t%ii~|6J;Gm5-Jv)1806*5CM)k6C^pJ9g@F-U#sm)|q1{^YQ?^1Vd^&O(2E3#QRY*pc%$FJ-<<3&!~Wl zFqe6Gz+AR{J#^AGk8QWWwb&4P|2KGY2&bLtk{d6AIMhfI^t7w|}WFrQP- z*9|q5^0QDyo4oc@`bw1@g=(oQdh5JzP5V5g*?Cwz6m{Ci3K*+(yyeGv%C4JQb@EaHR-}i3Lj|Ua__Ta9E}vKQ$S$+X1AMMa9@^lt8LTkL z4EXV|1`&n{*(JP{cdKEM)&$541k5DJR`jWWp*Gw+_jC%??awJMqmJ@4B2_|7@iAfl zaxQZpVyF=TJoLoWZq;hR_3F>y_%w?Rwv*BAh6;336WBI8x8<5u*BSIm9HNlXDwK+l z0K$1}TG`g+Z$F)k=>jMG`9bB!juS=?Vgfv{6{$vICnlj~^+UG<#X%e{qd13)PZ$WE zSBxn&K8G>G5B4Xc8~2Sn%c2L2!(s$l%E%+U$yg`EM%_MJSpsc_h=M+H!w3TdLz8Mp z3s^tbq=|E{NrrrnzWCT{x${S!{pjQD{v<=%p=Qld<*&uJVQd`VhdpMa7_7}If{jR{ zwP+o71FR9Sx)GV8)+vJJf*c{fmT4<9b<#N;l$J9+j%rDscH@SmjGla28P9HS3IEbo z;%mmSdED>m8Os&HqM_9=0GTvaFyWdh{EP6j&Fay^>p-V^bt@qpaE9meJBmv{1>n(0 zC=MLo3(Vb$G@q=Bu}jB-VdrzZ)+@wfVUynDwKK3XdG#>#t!PFDqt=U#3b1O?R0mcW zOfn7!e65YCPm963W}Rm3L&Zs#+|;UB$h!DuHu{uyA-rE1)?pTWK;r!Lkk7%~Mw14n zpx%VOK0XN%KS?rx-QEDHn27vA#l4?l9IbdiX-~A_{+x5~q*K^G(Yq^)otSuw+Vec?ZW_9d&HQy^Q=V19va%@xlCFNAgf}j`k1mLBTG?y&h%oV(6Y0sd1@@Omsj_ z_oc%7i6`(%&DVJSS@e4Gc82N-;t8y*M3q7NCr`c|Z{JDEN?8J}p7Ch<8M%x&KR8dI z->(5rpjYd4t4~pj2h-0Xt~dT1;d0?OyazRhlGvdGaSPTydGc_)w!QkOmRd#VGu<|B zw}6QNzo!T4Zl2@3&B!p}dwfKnR+=yLHlOcCK7QU%mD!#=sl-8~+O|)t%8f{K?wg!5 zqF7u;L~|$xwZTXWGa{Zb2Dyc;np=^(8_J)_-!BicYenRpQva@nJm}vshFtbYYHkGu zBoENf{T`1-g1n5SWp3&?AQSpMsIjchQuGekF|Xhp2d60@n;=O z&JCGi^*!a~^sG`>X$fNMX--JIhjZ@}ub=d7a$bZr;LmB-j-FGjFD?WPfBHQ*X9KYh zmq1r!63#=G!2aO?NvQ!OW|EW@u)5sJReJJ8j$C!61u^-A6Azs&nlZ9I4P(#305vlScfQ?4+5 z{OH2NSJ9fIZe2DJpR+hTwGYw%awF#aGR;}MiQU2a(Vjv}LFxx5*c~n1FY$izqrcY- z!|T6Gz8)tx`FhyXc8lH7!t{pMZ^OCv^L60AAA5}I`uUmz#|D|Yo;-Ox?n*wSWzE&+ z&|2{4V2}OP@;+J%yzeC5r#`IZeZ1EiJr8*#bx$9|`}i5p{r@O?55TCZ?0@{-_ulm0 zGntf`Od5nFKuAJo5JZ$B9YPb3-g}iU(gakHCITV~A|hZw)D^^nE8wbY-Bnjbz!k9J zuDc5J^8cLs-b{vw`}_V0gk+L=_uO+&yQk(5O@|HPEF&M+tG)>^@)~m9&#{Mg?(xKX zsIFKNGYlE&`jq#;$*0Y9L;kc6bloNv4x$50I_m9>LS|MFhACzkqV3L23+z?jPr_>8 zI$OOLXyFz&YmC7?YE3<+_uQ!kJ z^eSMkt(A8PX#}$$_W>WWCIx+Z(C6q5g@7;;0;H&rM<(bE74kQMIi)xrb@7ob=0Q~? zI_%b#!z=#HYYg!thSi(buCa-?S?;~D{ypC0=$pq+Nd2C8?%-ac&E(n9TzD>zC?OPQ zhwN@#?qn%PmlOV@vy-=KXZKs;JycXDOVs-{_3W??pV!1`63^%BJc##jnxu6^mUFD; ze05IPnKI6r;H#uw$6P)r+FF{t6j?M%G#MdUj${k|6cM4mPnXSk>;rlqcx<-DW5fc? z@Ae03=f&dp)!vBUF|{|MxLSY=<2;7mpy-Wg3#QlhMod1RmgHky6FqilTk17xXw0>{f|Dqcj`Bt*BeMEI8_^i7CYXNGiv z8|e?SRM(i}=DLa-23|?Lk9Y*{Q}8}x>ia0qAG(6_{Db80b_RnCeb>RO|2NK`iyQK;94J;8O^T{*}2f!<}D( z%b9P&Q^|UOD&?CNw>Q3Zp16L!S{9%f(dqgV=T7$q>;s0m(wE~qC=8A@1dM?|kh$GK z1jMDFE*MI;AjS!yCW=8hh|a`8pA%VC&X8Jlm}-6)TYgv=T1CT zH#pb6&*u3X0BPICc>|t8DrjnyuF*VB10|sGBH-19u6>2ftjCa3s};?eO-7?Axg?L< zsng>wYh#kxsMkvtOsL=;4f!S7-3%2eDQ-K!oA}Ff|02}c9Hy7r$pfUy4%KoKpT z1d1}||GzLw5oGNk%-Kyji}1@SE!6o2t3cSV7sWc+#fLDQ48cyN^+QTJ!6*v-AbYn0 zk5sbQ1kq^$oxM?L=o`e?X!(+ z`-%9o-znE$VIA2#WxnzX+q?MG1TpWoh5tCK{2(?U{R;a;`hnjGZ{x5GKu=PNyISDi zMmA?w#2O5u#a={kN8E)4xk#7I^<<-Y83d2j=W_%k2V{{$6FzVX@TMPWJY>yjktd(h zCrDZfMYcwfGbZqH-&-!6u^env@~XX|5G%}Qp*>sfdA7H*V)t%I?EKHC&S7$$?h0FX z;@W~KM=l({|0LVAY<2f-6Avv_o*LHo+)Ky*D)zcwsl4&X4s=<)^_8#a;6>V(ejajZ zGZAa*9cz#&qK3%p$<4Caaz&}AzE3ciJ-EFh5digR4yPI67qef7vK-y-bDD?@68;6c znQPlYL=)Lvym3eBL>by;MR}h3pE^(G)RA+qD8Jp+{LgFOKFQY3+uc|ctEVhjF!@@S z*~_=ATD57#9C7ZqCoY~DGmLpz8cm6r!%klNcE^tGJH~$f`UhWp{AU>Q=skzC$w6&K zJ>=on4_d9+Qf@(kWc0}Oi!zJ`qhUx@ULNYh{cgA4kFdC37IKDEp+;Miw7kO^j!3J> zNIDz9c68}Mi0pq&-}aY<(U;ui)$4|z`TY3bem{MC%bt1nFyoP5y>gdgFW;Vq^KVmkd$#nIgGPoher?EP%+<>ySN|qt^?VHHYRT2sk?h zBoy*S^;~QG@4bYj5_D}O*^HPaW^|?uO+!I+Sz{M+x4nA#PGwz*ef>*y2B$qR*JNHY zJbvV|mvtJ|yYB2X$K6BcO=qo6eS7je-^UQd>kT-QDCKlIUFJX_niUC$vve*wH!q!b zg5o^AHxiAa@mJK1{C+R|ncmt08K@cODb<@M)MgOkOgPJM_x>MDM9;)=ke}~QcRX_V z_s6ksaey60#5 z9AKu|11F)2!W?F=UkYVqNoa-{$;rq-a6s1`%}UJ(9Gd+CGAokA{y$;-7cuIY|LhEeIp-)H|%|EV^gqEAG(K1Px6gPbeYW3bXuW0`IX$f>PArZ~E z0g3$VF$YG!76w?4m(4(VpPuod;KcRlY z&xa9^UE%Y|HUecI(hE*LkHpx)uPN+43TjMUhR3a%=n zdFEk+XZ2_*Q+fl0nIYe&WK2&WPwKaA+x5TRKzo$y`!;|jAAgqYLBs!*StLx2B)*7IctR6DI+eyKv%-az&&W`mv4ErM_$59*E*{^porvj`3l~p_ z2$2)d_5chEAuIEdbzQ7aHzIL7n(H$fvrtH0SfEb}rNO*NOA{;>BwQnPT#iNs0j+O2 zPI!JTFQiN68S`YsRC||dM&P}pmKfZ&P_t;E0{BoIpZbN`(_9?zgK}}tBg#LS;r-Vc zyX|n3-Sbv#edfq*x1E0F$W|uJc%lk@5`TDg@za5J|K0cQ?nAAXO`SJx-mDEXmn~e! zoL&C3Q{B_;u%Bi^e&*pGRBF(t>&>VU&58QW=1eIn73AxK=|NZ(XlxFAgBang%RzY| z>f||p|KE0%5K-7yxtcDWXwyy^)QQ$jG<}7Ae&WN8_l#Tf-V>}-$u8^U{)6t``1qbZ zlSaH%zN7e*_vww}*V{zXvbz^QrYPPHmBQ(FtJwr1%Jg*?d{ zQ@KlQ^*b@TWnGI{gIng^Ec*Ma^57=>PUi+4%w&rH(mcfG@c)&^GOZz$Ovxb+`h$lM zo(ef!=>O^R^9p;d-k>gIy2N#tVwZJ=r<|#L(<;`WeP#3C%gx2gC-!a5cHQO1b>$}K z_o2#s-N(9(h^;i?-(yaq^dpZw^y1%}5SxI{8b61?z+4)5J9TIIjKBWG8#V2fg~IWg zGlEx$#5^vY-ERJZA*vf=-PjN`8FLDyX9uB)39@4Om6#hgT5D<;GbkiDJ~F>yS;6pj z9Y%I+rz|Y3C@HBZZPc&7W$<9jfWAbll2D}U$`0uN1lv9Ud!;aje)Fj9me{k>4^jVA{6l*Zg~8a9pzSFq=E04 z?9O!rGpiX5*4eDz3+OF}B*yXaHsQFSax?zug}r;>C zEUd1s7Q~u*9EVrw`GPNILV;Nf&DCAhf=ZTqNbO%I*8NB_F(4rl_fxpVz z%2o7VP&Ht|J%d^l_v_Iwb53PJVe^*xxfq0!iSLVB44l7UK-Gdd&2#fxHZRO?s_#*? zV1ECq1!?_y^($>z*tA(eL9?cXElcrj8oj=ts<3JEe7r_{F6dgYE7~4>F>{cne0aK< zn28_<*f+5D?_h4VpsL@Z1^t^f>eH)VXjaq0dQF=bTi?OVH zo$|t`QRU6C?+`45LI1mMCTlQ@)k*X$lyYKDFv#Jj zA00Wp$GV+k#}1%(NR;`^ZT}sno7?a z*B`t!PSuOU0K+y+e=`Wy>KJ1TJ@d{E0{(U7l?8b|!#3sXHTzihI^}R#H~*Jqor9!X z>HK!<7s76NPuNXh0WPnu1vIeA1E3#Isg2o!-3$!f3N`wX{s&#R3ia#Nr*YG&g^T+2 zT{H_s*}Q4JLJ)AP5+YWjAtKgh1)v|Subnk``tMj9_y#6hboT}O!QBvI2}wCUtg>X-AgB>AD@8+-kWaG16V(_){l8|3Fwy!Nj0 zA-zU2Pd88a82cyPk5PQYJ`V5>2|N<1DH=Y7)jwWPMI18V9_x#y`m=y)oEauyZW(!DMq7|lKiBx5qssOu@t|k4N(7VGTrTDU>R_*Fg+qRGu zYtM(ILbt(!n71qxY9>TjEV45qMv7s{6gX%#?nI^~xsXhi-+yGfK(;OOkv`Tzo*^Q#Zsx|wUQil9W2bhD4CG}WtjQ{zBv8TvqUL4 z8t^4_O-)ePU9%LlV6B!65O5@qz%B@Q>}DO6WAn!n|Kj(63;I2Spg$-IFi|A!fXXMa zj^mWwGscdcLBDrU_oUEG-vKy-t!FyM+Q5{rgX#oax?fbTYe+Ivdmm)hPgqZ?aRWa; z*8IqE9~D9|8*&yxA(h)Swr&isC4ri96oEhZ`uhZHY)>q{Gx5a`jaY8ifa$1WtAzF< zP7%6Ec1|-A39FlIgdQUvSGlqn?|BP&s^s`I9)j7x--8QM*2v%UKY8|8f@hhq70Tv}d6rtYQZo`hn%Wv-*VRPc|h(8`X zUA3?I(!KHT_wjuR<&dQpYPO>QOn(r96aJrQ0Jc*lq!+tT?U~>_%nrW2)9c+y>1#Ou zTVN1h0enGB1$ab)sBL=Bw?U4mge<&m%-fg2w}spN%Uj8@a$@0VSBPrUl@^KxY0 z73IhHr~CGa1;;k?HQRvsi?L=p{Df|!*MKqXdVNOFW;c4h=?438ER#;E3TK&4s3HFf zRV)w>sfSp${1p&BMsx%`QOS-m7SYPqQe}yVZplm@I;MTICI^mA>D8sNKk{-kt)%1l z)s369Znj)LlYQ&`U&Zz8zrL0;=T{%S_r8Ix@;}(nd>_uN9`H~Jm<&RL=h4FmHRwiI zQO%z-AQ3{{z05)3yH6YuCC_qaX;j+&LcG0f--6u|(TDMnXdrzh2X}9(=e}GoHLKt2LpZ=HoXVooH|vinr;o88frA&s_l|D8_Jw?gNURazU^o1~XXG!0bmYO=Tvojm z(;QL58}d5ZsuIDAJ<(ALf{Cvvpv;I^FAgU0qkMe6*ADy%&1bBEBCldvX6&miF$+wg+3yjGso52<4LDY{U-Q+4zql z_N7DZ=rP*s+p1(uIh;5WADB>}ER=QuR~8}qJZ}Av9R~f&Nfa^GipS|ynMN*%S~`(- zy#NHQlzSZAs!|Agyo2vv!!upT05IFo=*K7s9=lBwn|6Q$Zx3HXqC;2Rnwr?8 z(?-<4922-(@eaD6Ntg6m{H$`4wL5Y2`QysVtc}=2>=oa??}HBtK74;)d_V1@L(Tir zr+TUdt&CZnHZz9N$u^r?aJDtEmLRSQbQdu2HO+~m$2gY6q9m*-%@#)Nx66^AHyD32(gDV`-jId~BKfVsA31U)DYJBE7{ zbNWbfYC9IOA}-y0b0SU@)UAq91@P2>&QE}gTun~xk^|-!LEI0~{ zgeO&Ey-z`VX_$V9Xr>?ibsSC~qOn1PET<#5sq?mVU`;2mi|H+VT><2%LoaC}eq9pl z5O`oY>^Ol4uDN*I-NtUz?5J`!M~`{C9-07#QUl3+^^5qEZ0CYa%MaW!|J2IT(%=3z zzw2T73zVClh_Bqoj{f@}|5nb%e-VQ(&18$u%pAjEJ^NpHE(VYjuN4E*_=q%JC-lr zxxW2A(YmOyBO% zo)b^6eNJ3D=W5x$%gPV^lvk7^&@ye<$yv%*<)58D$5_7?)tP4XYQB(f0jxHm41Ik? z5w*^^PC%H7=qhDx#IU&63G4FO|%^BA5{<$aL zP}(VDc9luLT<wGKSnS7GtQw;m9J=E; zONXGEMnrv_uc1wo77_3R<|YlUN)fo5A}+BH2>lq8&E)Z&l<%({lzSZ8tUS3- zJb-oK_LZk#UBA#7a@Jti8I1v?9jEEw!-^fP(&=1iGo=xP39GsUHzx>JlE&4tuT&^V z#&A(j0MP09G>KC#9ewh!a^+{G2YZjfl!DY5#P%LPD(-vvMGg>(DKGYb&5Bw!qmv*gfQS+%l_7z*wb+K0N$3+J z3db6Z-2b=zcbf-}F1L73hCF}%f@E~ngD)|i^2@ndfo{v^|5y3;lfR*$mG{|`Wx(J< zY}v9a=AP>8_|stY-ZL${6FV{6HTRBl~K5Lq0(#8)PVuW?EzvM5J+o~o@xZj z^7U@wnd_G?viV&)SOdqs_R5iiSet!vB>r1u@v<*Iz5l@>c}h31IrwNE=%gdyktom^ zb9yXBCW@F_YV@J2qSXRv+yX4y3YQfRKPT1NAvr`SN+DH34|K6aoysAM%XvGgXoQVC zRwnBI^R9A8IrrXIGuFkgN}1P&$=lh1LuZwXO1;_d4`z41d*4XHO#$?fs@w99!X}1x zW2z0Hiov7}z*i;WjQoLmBkj+X8-twqMYj<9%S2CPpTj9IL(rI(7VsMbM&^{Wvx0_W z?3Sa5k@_7%;5g3T55eCS63U+-NRksNk$W<_RYQ!_7 zc($W#`1Fm9(%!myf0sa;ld_g$8&Uo+zS5pH2k{o66?o}uz#fEzLkrQM0abeGu*X6kFPQ0YmC-|%s14b!Dy@*- z(j*+R8*h{%$VpPoByJF(!J{fP03_&RP?TtlsHSajQstW`iv=LzPOD~LQ<919V7!yEXy>b9 zhBQn`V<(k1BD)UjV=(KipqJfh<>V>_p?vl?phl}_>kJ-k0w3tcSPxXn7rSySEeid9 zp#?5R<@xz7TvDr+xlO6BL_vACszPLRSnLHkh2iW%R!~?_Xb%e+sBO*22#3+C57j6Q zyH(kfeJ3zeisybHf0Jh&Q63MrMEs^=nGmi1NGrG0j<$A}tQ@^4c+OqegRE2@IL0VL z?p3A?1+^!ht6W#v8cpz!pND%suXl#6)Y7C};EgIU^sVj4JOBF7o~OS1`Nv6f7EH1I zDgE;|e!F|-c_k_xd~MN+&cg?d+}bB@M*oV1>-Azg7n_0x7EDlT?w_%L<1^37a*O4Y zx(#$ayl(Ysu}vC6h^51N^c`BYx$o;wKl=2eBzGk`d$}IY-V4c;PkHQ73-XlH&Gu}+ z-^<)~B)s}8Xh49b&PMoXQNk^t%P~?6+&(djH}Tid#4}Q;g#}M&rv5{m>{Yra+9&e{ zCNd$zfIN2U`^QH#fD+U!ML3GoFM>1JvoC(sY0v6N@u|~F;~A*4v(Fhhaf)*FhdCoh z&Jl;PPqsfWyu+%jL1hnZW}herd-SO4$^NDs>fNJ9AIJdI2~vMIqm>BpFn4CL#KK7UnToF-Ja_?uA1VxfvQ7O`-|q z04AS^ltQ!?O8`K%<|&;(2%O{y0T3F$VgRsco(dO5-x^meC!kdDmP^9{+)qCdc$3-b z_|F6)yLkKQBg(7bqcf0sz=yO0_^?oot}M_b1t>XYv+7OYZaoee1_17W1Q)~L-~@Z~ z`rV=bI?YRC=u_k^*%#D904o~ne$y#3Lh&-WOVOXrOHOmiYswv0k z_oKjyTEXOlC$p>;D{zDq`ZS*pGgdGc6O(LkLNXL0T2$AvBo-%aC^TalbJogOFd|PB zLnxk8)E_quXQ%hPQT#`Z!aw%TU;IbC!8h;6S+7(kvR1$&=>T+!Lo`7LuY}3q5YZ4{ zX!FtA+^?aDeH}b<9?Ga|g)rB}{rh#{Yp3vD!b`)NE8^erwG;I~0h7fJ4KAW39O1!1 zn}=FS*}TeFKt1*tmiQzGinVoeD)Be#X;q=Gy>~y$bBfAW)0D~ zXf=|MS@3w``&N))2YK$KA26a{tI$Z^Dg75#fnUgvxxJFzC>hfNdRI@(J+btxg1;TS z!R^DO1Pf#IjSPX&Bv;4N|5WiRPGlL(uY3Ws>Rsv+`Q@2+KKuBiccuR(h6%sC?9uIO zRzJLJHJw#kVVwN9^bhEX`Y~U?DPhEc*C~h6Z9>l~n^&**P^1oE{R?caj5x7J|quDWk@=5o!iF> z!VW8STh|%=zATsBYRZke>{))DQEwK^p`KL+v*788uE7Yl6HcIzQaaNDR*+3z7|D^9 z1?2$@J;CItfUiJbfDW{jBxtE4A$h>r9 zM^e|Q<}gw6gFZ_rBs7RPoehFgaY=f9w<=%2f(BRtQ=lHEqWC>ftscJ(U6X8oWS4N; zgezSt%^-oSvIVq-YHq5RRm}_uR74u16$57J7U)3{hGO8#(Zm$hq-Tr%#+P1@(=`(q#Y1HLtz<*Ar`A^U9geFJ8EU8RG4jb>-3}_piX& zF`o~xb~94}Gy4rU%=&XED8IVnKQ=9&7a$_tr{IG=Z+XM_-J{)ykoL&!_lib7{~7&1xb#edknHuOiBJ-!jT%z@TRvrZ;hF&V4;L#fm>5vrIK%q= z`pG-&VD+UXv&Y>NXQ!14_Hh~0J-qSSDR!FuG;q-1L3QD3AImXW&4Pe(b9B;l86-o% zZ?&U1)@qfR*&$;jb;1%P;v>0Hdv}9eS4j_FL_l;-@`eQxAW5ETrQ187{Hp9d!&+RA z-!pFZl1pTMT|2e$VWul%A1f8}0TF7EasE-@ihAQ3kYAi5>kxv)e62+$*2ct~rZ8Hu znqY$Ie3C&xr6_J?+FhCq9sE)CAB<_TxDa&UiRaZk;r#mT((}+A%fN4_pG2|TEWTcQ zp`ZMabR7IwKjtVip|HZNvkQ&OW%a7O&I#4;wdO z$l){bQu@iWkHy!B_w<=O?Y`$U6gS^Ee%`tasp*a>yB|K>XY$OY02}9T0DYlDE# z1&FWTkY|mb_r%Y)@}Gl6uus)4Zw?*b1M|kbY9kSsECo<)jZPxSD))L_0(BDMx0jok z<6sSvBdJUb3~+uy$xFfxr_#FGP9plG@jYkd)`qMi{*CypvS;rR)$AVLf84N@tlsM7 z%B#}Tn_zAO&bBp|r6TldFA#>u$}yA*v2kj^$O5X+Uo%i+X~XWkpgdl z*M{cYp>90U&mv-h}8P#5bc_;+pM=Z?u~aSL$tO z;;EXR3{~l1YL2qDol<5%tweF(8BO~|`WNTi798~89nD{R;n?fV?|5)f@V1=d{*ivo zruFMPw|Qk{vpL?%Hq#24d^!%L$%O$w&9sl07K@810pb-`Ys ze|SwidZx$hG}b|H(4qeT?%8C-CnPG6%Y_%U8-R;v>Gj(o4sWDgv1{UaT(1~}cCjK6 zSqcmuv7M%ardLeTPSckrafs;w6MZj3YMLbjI^f~{rFNAn;u3R8(lp1aBrx$$db3(7 z#lqK4QS0Hv+M4ck>vc%C#a~7`kBL~HW@x@Q?WKEPu=NuoaoCZjM3H5W_zu*C{R*K+C4_c#neUp8Qd=L1f$-aX= z_z=UshQ1;EHN8!80sStA0XH}UOaQc}=LbRb!|_AtQ7uP;GE%o3#3haXeI3Z}`v6`$ zg>$EmNX(~pz;)J+XME8gkVe5Cp_F@e|t(8blt=KCIE?@O)XarSp5U(Aa4?q8br zFsn#FKV3OJRXH$SnlY+Uc>(u?a^Ma#uVEAVc#NQ)Cp}%p^doe#6tc1{GzAradjK`& zr0R5T4srU0D#9f{OEp{t69ItoB3kVlK+poQ+}9Qf(@iXkC45?*CaP;o%6jQ3WgZ*A z`tuIWNt%l#RUe7UeI>#r?{zexOCE<}<)N z6fmpzvVO4!W`izFeM*@#q|$7x6~ zb7jvp2#xCm;N&#qr4?XcT>@~;IhQ+_fba09Rh?-ggT-Ut0Qj*Ld?Ru(+wt?*TV}VP zaPLNTW)DFx`e-NEgD5n{9wuTBFpJ1H=+I#dgy8Y&?H8)_7j+Jxxfa3P7*w$d&IPkH zmXY|cp>X2#PgW^gS(5H*=+P@k>Wf#Krf)yN$3`0oJnM7L=>Q zC_#gS3#-*e^ykpwXvy8qV_6M!&X6xjb|UHF<{%sji$4cTYX>$*=t&!|zg2?%g{(DPN0vautN{#g8 zRc4wtuU@Y^&%X4+CzWN-&QkVt9f(vnlBXrWOqSA2Q~EXqUA#SM zLW;ATx?aJ`UmC8m^=RF$>+`JrX7(8~rS0T<^mxYy_Nr$^v^jv|p*4Eln++zy z1M&j|i)^vlM1$GDXe>gl@aC)C7{DJ@sgEFQL z^KeObNphrS__ZvTwD5>fW6CgPT{{R^H_grUjf&qU?(WildwdMIyGPpC$WZY>g60$S zWQ8x+hr}*fz~CLY)(NRsOUygGBI_1KCnQ1M6u}>?qvVfFUI~VcE!hI$8Yo+ zD6X!%j!kF~^CDLY71Sp5OR+n2Rz16DA~mLFY~HvEA(0PChikHul`KN8;bV(*ik(mz zpJCr6wYZ)oZs_0f`f13c$3z{GmP2WghVf4sLuxyHKBqs$Q z?Ifg@MM>Y*eDy_*a_$SE=7HxP*>U*rqmMl+efd2+7w)S+Fst(O_bofGpXPqSrpFFtcZ`T-m zU*55157Il%J+|gg%3|P))|`qp$$sb_tH-RE*hSQb>>iVB@&~LKO^U_c&@ygBt933i zAT&!%gMr%)saH5c92q*?E+fmC2QncDDBBH}R`I?6C||B&Z$5c&-gxFX`XL5bl-)HM zY`#?)DV^JV--Jy*rMLLnGs;$~!DB*wz}NvW8id@Kn~8c%PsimMiBSZLsA418jFMOr zOH$uS<$mRul%eZ){UK@MbYgovs{ph~&vFTvD~_2M7)9dt*ko04YGeuEYsnHW8=`$4 z5uz{j?9kB<)-1kf(TX*nu+QFlR-7GQzijn`E5zaPhpXS?YcO_6{x8<>!cXV*x-BNz z=eJ+9xB<4ubdecJs383$Ayfb{i4i>#pD`wfMgpP37VbTD!Dt1{Y~3**UsieVddAiC zti@;KtBC64paXbTCWfZC5Y^G=M(vkTCnOmm+Kg8uNIEY`+p?ss-z+&ynAntdLUs`R zk~p+L876%gT;4!5zg(S;!LtnXVrg_zultuUd#}k+W|$$%GxW;J?t?}T9z1GbPjSQ@ zZC*Xa*0#O1tiuYW{)ndayEEyrl^Y-3bpQRta}s3CXYv-DPa)5}*c2-%Hx}BWEt;gI zu@=SI9#4xxsb#Ce^7B=Ng?W~G0mx)$G@>1tGm=F|m4lfB#Hl+N&zyPdovZ%1@>-8YeWb@cE zLXenJx^TWKon%OQx*5YoiSo?S`6{!fG7{=o`#-5bxVV4~%JoOBKQLv$%%8>#C&J0z zHf8s|eLGqE1t5*8Q97y1y=y@k1FHV~=a)|_<6J{I1*kYBINh*t+QuRtv=9mgt!`_2 z$n7~_<<`6P4wJ)wzRK~f3E-KM0u6Wj#N8-y5h32EBqEa7DzOuFYra0QqY78-3W25k zDFN0J4pucm1qxN6BE8xabU74o#iAARXo-#$~W}Oy4 zG_rTcA!{aooi)+5xJ|nczU&fPnL9pf8T)MR*iNHNqG3pf{*#W{N3?7*cgUJpyXn~j z;G{rq1@=>g{X}U$mdtc7hK)$}^mJ21isa^HS}{pQutIa9wj@iU`7|B9Rt7wvtP~&z-L0IaRzDwvw9zagr zPn}+$s%pv8eE4WMss&s@XHYT~m1Pc0JkWbM`KkGBZ2E##o_Xo3j)}KDdH>v1tGZ4L ziw1EjlQw0I>o>Shk0Dj#kB{!{p7@V>8(uqc_QV~x<_?s6E8{Ob_L%t8{X6d8vE#l+ zN#+a|-a(G#1!x@uxA1swjmc=ZEaGk`lIhH6nh-T5cvDNT`FPHpIjrNHIntwXop_CC zU@&a0mmt3^B)xFA<=0w^jgMHaz1v>aUDE7fx+g*~@kf)Cp+09|O^L9ic-L3?=u{Q8(CVagLSGcp7djKs9a`Kq)l zKAO1*rRTn4xh!Ow$et|It^`o;3H7Fe04C>ViJt123d#aLRm=yyc_l z)dAQi?mtnleyF(HEq(irnnz$Zn9y_HM7Hid^K{E-v3z{rB!J!8E?k??KW71x33nUZ zL!I15hf%43$Z$o3xFij!8Ai1RKj}qo4b{`A3`0NTYzv*3zA6ocpTsA;J!=9Wz||yGhh-j zSrf^%Q6e&*d9Ici={szNe}Qk~q%FH1-#TN9eV(P?EiXvp9$m58JaOjP6DQ9sooMWQ zJHgOG8ZFYkf1iJl)Chl=f8h6tooR=QXs^;}+8S+Da#$0<;i6jH0cVfUIrRtaoP{k*HVvlPWV=D~L7WOw7i3+^$}luhqxL#SdG)E5vNTs>GvOkWa>n;r zxolCd{^JIaHA_a#z53gfr=``)ePS@TzWeqzC(i7;Z_^{2)~o|=!|V%ouWlXgcIZWC zL9bao?#ePU(Cm5xgucZTgQ$T{OTd}_7oOJ&FuBDEnE)su3@P)jtg_5U5oa#zak6PZikjY?`MC&CtRrVp&ST4V3eUz<^#VtX)ALP zNE=RxWFyZnkq=PB-lfHq{$l&bmvm_diIOHhcq2gAHr5}+K8HNKh&? z{Nf@_EF!tTXxJSj7sI3bP8rSxqkK`xAi>D-H6+@zb%##vx)Rdpsi#YN$bk+t)r_JEm^!MugS1jPLu3bffhwQ z$_iREX@5It`15Gv9&PLOT{Lm{s9Cc{77x93Uf+`3%pQ%Sz6OJa6nVhwL%{2opi{lL z4fczbWVzAY9PFOsbcTSRdiArj$clAj!{3yhjT*B&n1`24-$}3?6JA4tU(0r;F4$1F zsw(IDPF1&*-&H`x?34q_4}Ht4U*+~k8E9W|E3Ys;y>k2 zX?geXnUnf<>CtIef62IZ`z>q#DhqqJE}n6yjc@JsQ2xY0BPR{_4C~Xr+ql`FO%eNf z1ZQg$>c>2Y1Bj6MW{4FNYH7sF0+b!Ef3J;J63X*%;vu8~xtLw~*OJp`mMGsHXT{=g z@m1o?xK8;1Pvx=}crwFU>wvtdkWI>PXXw*lj%BAw;fNFp`7$#snJ_^!FZm#UeMwzL zt}xh}WQcHk1YsW>HP{2fz7jS;fZ#+vXnH z_{g){c6`FFN)=sq^{l^l%ItfkG5QO`$IiZW*GtD9Uw*3D54|W4L_}A<<5Cy-7wEh& zIJ6*5u$kO$3`Li94iiRGOF0oA?(M!SHtUrv+)!|m`fqg{Ga7&1UJ8QBLTPB1XZB2gY~QBk_WdJ9elYpo#Vsm^O&lT3KKY(W!R%=L zTgP{Pr$k8F0mg#XK*6a3q17;CTdOdZI7RSO+ysb%O7&qjT{h&7mAT9V;*1#(K8 zb!;}Idf{)(yBM=gi^UFChgTQ|+HaZjF1ttZ#!n%qW<UFfhQCDeR1{ibjW0!-lN?13VgYi>({$#nESNWWxn43BVRWOw zn_u$K)IoHKAZKpxq>)Iah@+YbL@>|?{-VB{JZe7fWuc@Re;`W!r4pvzxz$TgOR}<* z>TaX++{>TXFw8hfSGjQO_FF_3nP7K~n0VJOZ1t%(lxgLS?j78;x6d)+dzMWm*(O+< z;5%ig)J0qbPe2Z;RrAw@uq=CQIwxnk+$cH`1Om8NLwSScked#R7uE`a%e6F5`-ZSc z5fmKyx}3*lMnILM+>lE0-p;2t-)HaHZ}`YD3^x5uD&i|z(75pQ;pU@Vyo*8b4X$O^% zG^(;g^%W!}w~Sx_@Kh)ozD>E_iFv@%cgcCcKQ1UcvF@`oM~X)bd0^U+d#Zmw5_n6u z+n+lvz8CK?VS1DKo5cft#-BKbqmsaEI{`EYKB+MUA>u6*+@~*(X*aC0EOOPTIrV&BM7NfAT$+5#+sy|-n=Y79Eqgod%VSH1fMT8 zZ&v7Y268h4ncfIK(s4D17qNm*fNEH00u*k-#G4u_QscLr%E6_iiRrkL69~%x02SnG zgc?e9iA&{tr`kkM1iM#d zIH=4-z1F;^`GK>5}pgzhUmB{1Nl7tui9$HpWNzB-A4 z&6}6p*QrTy^w!qzBi(C)df3V^_NBUOcIVQ$8y|a+;79qv9_IB@Pk>RK^I<-qBe~-ED#ZT$*MlV>o#~Q&B>KL6zeXS8f3? z0T!V=&L#zA=+2Yyp%8!gGg0;XNRWuT%ffvOUTa0QL=J7guXgiHxP zP(;d5FEwksWZ}H#)eVZ5%pIDV|J`-Tq@nR%ckO$&4bKf{UCf0g#dcKrUbVEyAGF;0 zx7q~q|K!e}^=O)MNSy3#)yDQIQp}&j{x|Yo*Ik4XRQnE!H7pe>at$aR$-oRz0ad<* zas#nJ<8D>hxFapw?8Io*kMdd`w9`X?J2xF#BL`YBfz+leL%=J5R9T5*clZDziq}S5$LLQ{08?C@i6wX-*E~9UFv=*v4 z;S!|m|58Sbgb-e(c6q1jIj*j=#Owq~8vN4HZ{9uqX=F#>fki79_8PUQ_kBg3mdN*I zzkm9<_cL}mmQI{Iy{OChww+d$h64H}LnEwc%ahC2WcTgft5@sJnR)4jV-D9VzhlEA z3s!qOcDbWdy~@IDx34g(Y)^xhDxDp!`BChoYXIFnB-SwOZxHkq6u2E`xq)0#thYro zY#HWke4w#u8nW%t%EFELjc$inzn0s`VoQJfW|}HRRgumH2FhIp38N*&k(Py{MQlMu zks1RM>sJl>Bwr=Kdh_ospZ(`Q>&irx^mNU|vNpNxjcNa8ejSmZ#LAamy}DFcA#w&Y zh|hFwW9R!58X=H<6gk}yVL+^*(Sy-!*><~R$o8hmX+DFT6HW8^QI>6X2_aVq*%jG# zo!?;q2cuRTf>gcerpgm~CBajqmeebHP672eC}&CO(Kw)1J|s9wJXf|``B8l7`aD+F zG@~GEi8!fu`2r@#*Go;6kM5|?cgCJAP>!r#$!6JTS7Fv_^-ASoPeaHIY(^B8LpFv% zpIJDqPOInUOzJ&)>gy6G~S~~!~qN2#!Arj3JT_Z^J(_ctJ zTylu|U@cR0f!7U!R-;~SW;#8ZMNQ@;#=-VD^w+E-?s>zNpQSNZ-+bwA}(-rbwe{gizHe8iQ(v%!E2@X3$a$p6SNZ- zH!jbSm5GEtd`50&BW3TbS*+h|_9<(pyez)0e8LLj)cc|fa99d@2m^3}z=maNLs?jyA&m?|2{M#tTeq!f32wBg6E zLs&Bl<*;_JHg1p!YaIOFWx+p|hU*nII~0Ff%2WPb+-lIcL9G^jvv%Fr^VoM=AAe%= zBm16ey5jG1!Y%XbwQR6*_43biGczjM^`5?U#M)11&-#>IIP}Dx>Z4EW<_(aMYgO~6 z?nT{YSPl2ZI%b5tK9AFxmzR(JOAYdo6JMs6MKRwKlFOUe1XJVEQt)r7KM(!#+)^OJ z7r>?iPPkKO;<+(~>5-Tni6RFslfl~{pi`=RRtIZYbnYI5-5_!9VXpUSr{ zOAEDE*E*=R)8FK^L+aRO-0KusAC-BP{bWF}!xv@a+hzCm~; z@sU2Edg|2b2}rwsPbpodWU%x6UaUPIwIm3QAc~m^27D$E7;m=Wv-vRe%jdVDM%d=mC6u~Gbv(9CO)g8*oo_d8 z%oB%@TDkn|GQ|jUMyZn4C~KZMb@H&({lL@D?;;tMg}dM_z%%t1 z=^ZQ5q0ZN6l|v!)7O}dsv(nYxB1T!;u0wP{`a1l2LVmLAB(_^NcIHhs1rq+UjfZuJYU*F>}%5o^GW06p#q_|!%DnP9IEgINa`mEe;Rat%y zpW5amPElhh39rRXb^t{Im>c7ddPuEVC1pFJl^13ilOq?CjRV+Iz~>c^;GViE^C16# zG0K10sjjm^Vwcb?jN?%LGisp!uFg!kdj2cr$2-Ip>-NUi?-9pzUpI2~>ij3?&e@Z{ zdi98PJ(juM?_rpr@IkJ!>G%$-b;ohy2T{0}>}=fGbA^VO;?^K9!(CJqG*aF`PzW|| zB)YSt97j$e3>`T513pIyostd~k1l1%OyZ8F-YNLii_WDCttyz)kessGiUMb(PgK7J zTr@ZJPz+Ybug-7NG2yAG{=Hv9`F@?Es54n-c{LY>{GC!&*DFDL#5@}L7VME^ecxEA zT_`AUIx{ld5xqX(hN#a(ePgE2i=IH<5bwAw86g%ilp1GCb{&a5&8$&PYkZ@*=4qSQ zXkAAHf0<&L<&bJP!T;%esMhAq&hPU_9tU>vw|5c~XlMP-*C1qK9&Jnb8Y1XC(;$p= zBFV|@MmzGoaj3my)NKu0O%VuFqv>-fHdbVHru3pK%YfYi`=1r?dDowdP#9AB}J%$-s2+c5DV}@pJ zFjVY|zkuPXpBRxgmNbgMpG;T<+Qqaa__o2FsNR-r7K_1PwAyTv)96eKdf-g;P@;kz zuUj2xTI!R49Gc@wJ6GFv!jDP>d}E+Z?b=D=g*+c>;YQ-H#lO!lP#(Isx|)6ZzH&S3 zKZUcT^6w3^*!L5Zg}SrXhl)0(RB5bHCCL&dIF!EZVZiD@XS6gZR*OaNken_X&de4< z^LBI@q5@40S1k)+;*829Nv3fWl`kA#-B{eCm%Y_}B8&bU?t~8&?2SeWHL|+{fAV^KNA)>#L z@HreRm7p6ax8#AKn>dbqb(ll@T@1eegIeujnq(VCo}xc2`aYdx^+ffzx^MJJq?w^z zWA$=nIV(#H`)xLtJ!~&5$N>@Lg#79Dbf7rhiS%%vj7}nnrUshpgA1`*8sL(PjDC;+ zwUwkxXPtf4ZcxgZKl1lg{{53=ar1in#7+`=26j-M&=@kfB)>Qt4WI&&&0rJKk#*Ok zOmt^UQD+p#7ImgO0wE{-!Os7OrlI(Nc3PUQA%5mU7iIvIjb`lp4+qXY%4M4M-6~d5 z$qk;Q8FZuDMtN=Zr(D~#>pt1WO&*;-VFuN4+AuyJoP0Lfh(_h(W+n$k(V3~kz^X_% zgrf?D%y!w1RyB5~!664y4vK4;lyf3gN*^%h)MKjSsi{4nN%IHa;GR+Ef)mF|tyA1G z%BUpAGNBpfv>k;UZbE*Jyxd$r+9go zS2_pSqg-on#gUvfrzw4&1Eo1UP$g7B0kx#30|n}mPdch`$M{`IPe;FDiyv8fM~ai9 z;owc{7TjLASk?R+%QPQHtXq0cVV8;Bk`9iNmi6;;Tir3VHDpH39_Yv4AeZvMJ4Kn^ zGRQ5q9!ViV+ zEXYNGhR{4x33Z{Web}KygH`2`iWcdtW#0ykZt2pA9Vg8|`s1q&Tbq>C2==1x6XbPl znEUb2m_^r5b2acNw8fd~XCd)O<{6Ye)Yk!d*tf)@7E$!u1T^T=`{gtgy!q@78C)+L z;5y__qO``?BoLCJd z_lXhE%trgFSm7HpW{R(>Fa-e9Z-6NP4@7+=nCs*K`o0)2f>|%iX~6)x%>;aQ(2YH0 zz*uo;1ExbOM7mjNS4Xaqv{+sOp((@UQ^AZfrS6FKyeS&E&1tf@*Z@6gFjzePFJ({? zWRAN=fRp?_9;&_7iynp&C|vkFsGaBIW<7%516#x6)1h|>^d>BL)z`y61Bt2F1Ntge zcaCZmXvf@0e3;xf@v-7d8c^}{l~VrdmDm5C0*v_ovGykLQC8Rg@N=JM_9ZiunMo$= zWHOUIBq1xvl0jrs13}p$giVx1k-*fNt%$7j=_VbU1z~tO>&OP^>bI(2Z-gBj?>J-!!7>~Llt%!R@l$-17 z{C+%+oS)@#H7ohbpteZv#IA@xREXLY1*V3HUFCGyr#*|gvX*3Fzn@=rK`joqg!mNd zpy?+?K3HfhqjU(5ck~I3tP-Nt{=g4jBm&l#yz9fChxYhr?alw-HzW|FCvW`Vm`M)X zzVqJqzxjIU%2gNGpRQw99(=7kKXh^BfVwL6A9q_#qxPM3_8ncnU3bOkS-yvE*%dPx zhFrP)jI%r}^6N*{x*g^(ZnbT*u*y~TS1+Cav$M8AQ{{$v6Q|AQvH29kcaTXbqQP0w z#ym$j915XCvs8uRlMVGn?mTCXCkKn*96afSJgbZ{Umi~_J#}eEvcjX>y~@Z5!_ zUXtMg5PeVfFkF#{_Qbszq#ZH+HKpy94$yiyQ8u)0U-#O6Y!=?#itz4X^C{n5ck8t) zS6zMkbt^^JF27{RdDB@A%aL2Bo>P3Yamdl$MeMFW96b2J$1na_UxU13{4?+88PS@m ztkTjvUr|wB*qnz=fD#1gB?bHiPn^!g#XWS;>NcR;hQuiI4M5!^X~$i~>_Aqn$F96L z6?FcU=g4u5OQcmwJZZb1gT+R#w*AQpMvOjb$g0)R>h@vkq5gM0?`M^+UH&aU{B4Wt z4i~GG@6xwBNw23GKEN0jVHG(&T4(Zj9C+BaP>Muy3v3aX&xqjYz1a$}!KP7xj;$3wp*IPnA%)rXX?$b>h=O3b5O&rS2E+Qo@nT}ktpR=Sg(x#Y!>L>)VR zSx-NBT3Yd`)-P1>JZH)M_oqCde(zhG7OlDU?fqAuy+-tkomYGBIg{s{n|wjhF-0 zBbIT~CSbbuaAve%0l_feC=#3V;;6mYtH7C;ggC!;ZJ2%C&o*pYxIF90qId8A@XKQ# zZ#-9%BO8DIf;Zp!=r@bb%DVL-+Lb`9+`o-^MAMiTC}zA7avKA=g&?((LP_#l@GKK1 z;8G7b^?35^d01fONp{2zc3o(6MuRMz9~RXt8m<}XF5*|*r5#3ldK5EK2rRZS{cPx^ z>*V?&YjuVY(!;u;g|USr78El=DceL=siW47?%}cKvC7phGF(iO2d}tj)BH^v*0agi-=fBf)J^jrJLmky zai{Ugk&VFkU|z(Y)+;#Y?S~fOu3wI+(4Uo+jS8|Z5}icXH7ON zqD#K&(&(tkA-Vg?{DG&cKW*D|>z%i5LbX@&`%&CYxE<$db0yRw#+?_F!-QKee%yL- z9mSm&Y{FUfQ~8wQ7E}Lg@C~NA`He7Qip(!J?XowI`xq|%(o4SjvXj`u3(hb= z+L5E1?Rdwh)SSaEJ-;KIH|f-oJMJKcy&LCD&E4>`C)!s{AJkDbY3bSZ?EOJ)%_A;4 zcjs?jBj&HzF>BPudxp&%JhQl==(H=Es4tDEUva>c14mjRofWOO6c*;Fk7tH#fJxal_Iz4^(z9zF^kexl87; z_xC>ZtG(5?b#`~2+%hE4&^=+qLKJ#3?0c5YxoYi(4Og#S3mI2Hk0;`OPnonbdWt(- z;17iyQhq^!*(8}N%EKNM2*lNuD6V*W$XGodrP!6va+G}fC_lfr7>EChk19Ci9>t@x zW`3U?CBV1`;#pf5t=P)tW0=xk@=0){6#603ODiZSDt2Jl^n!eFvhu_mFTeB#`zpsx zr*(9UA2F=GW8|od{HvYUEm?WZEoV%VWAiS$WVLPksP=PzV-QmCkKXm6_Gk^ZH8H0;j>#H{#uI-&8-!)_Mf*F=Mrhy|)nZfy}mEL6A za3{fs`-7!khve}X6|=+aM+zTGBH<`7od?E~F~kQhDYWZHzSG&z9lDpMY`Ufd5u)-j z?U03Aex1UF97kQA{2A(38XLXD2F|f-_f4^vxtQ}c8#h@V!kva3X>c@XHRZUxUR(#r z4LBekX@X;xV~GPBM`rIKxA`z7S)s?>iBkqVkbq1m$DUr21KGzmxut5!q)V2s88dO( zxog&#lxsRpS$hNB!?|$6+6CqOl7Ff=dz-FfP^)MU$Gfhd7;=F~;H@~V`SPB~@u&}S>dtktq$k|F1(sV}`@2Jh3L4JdLWYFrr@ zLrqLv8pAy|aveHa&NiMy!Ctm(RL@+AW6GpM7oh!g=@~;c;)emq&(`L+-PxWZZ2b&~ zL@F&FLtTA&&8J=E|0sd+l^o4?puEA+q8yL+h{Z$qi7`^x^C1_@-E8W9CGUh5H&DW# zPlxzR-^;~`3%$ytbc=2k|HH7tL^q7joa0q+!+74a)s1Df6O~ub=U0i@$LCL5vpoHx zaqk0fI#xIrLiAdn${~}m)Df+Kn%lBjX(4X?*{~kS_By;dVN3oI7i8pe<>X*?%=z3- zH;pvUhQ5f)fMm&uM6B?&JUM_-M^CtqNd&71VliSkC@-%a+<{)KEI;Qoa4(lEoVs-0 zD*A}-O)j27_W*xilObTT1IY4JU8*%?{I@Xx!NTcVz5l?Ooq za9)}1k8GGHN4aph`upq>;|jwkYEePmH1;-LOFdc;U7^;>KR^zObQ0p%1BfB5@NPUO z?aRtC`jHSfN`X8dCQ2U#BR+~xP>dX}J@Px`w&X=$eJ1aFo($=7aiKi@$c%h|u0!(+ zK#!k0)2yyA&zg7kY%{ywJY89+u6W@UT>ViUYc{N1C(l^3b_12<2A^&4!7p*&lOq)( z+O5eo_^d8h9%4Y8xiBb2#pc6ST=c=gBom7Nn2!4L7#0Uv>~nI#$xtdUVU!(@z_xF1dc(DbKQy`?-B= zUWB>TFSB0Qr`}Smet@bBH#mDAaoup8`cp(6@1yH!(6@SNzq|>t;6U(R?KRfs;VMI@ zq9N4K)L2*bX;)punCC8!;D~Yrekt;~8&iqT=f=_o88GgNh;hmW4Hmg|v2jF0--y#9 zd|^*#%PZ;dttNnz`RK?@d7J3DJ(Q)^{oc21~u((v;ys$P1IWx3@I zMh8V%d^hHX8!@7xY~O2zSY*p zz6M9v$faZ06C9Hc@W6~>Dg9hm;;uWztT>~fAMg4To4R`3IPNYln7C%i*s;tzZNB=z z$?)V)vN7tDGatX_@`>%2ZJIihjp;pW-3r(63np{@9fUso1$`I@8AmK050XwwbtqI= zSYEHxH#Sw^lqZ&C)l$THL;~OXl8hV`F|2b-%<@6!a}NbQ{6#PRa43q^q9~J?bDFsx z^EbWe2q<0IW!K&IRHQmmR5WPP(9?=aiXuz~(jx{JN17QOHS`&cczYZLTwDsb-vZl6>Y z^;%7ytSnSB^yhfYM-&K2zs{}8gaktRhE+x)zDIC@`!-D8x#Q-WuUd2Z)IC*Y>q9ll z+TXu>^F8bjYSsJ&Y)JR>r;mM!+g|!Tdn79DR+~{lc@&ybn-UjAK?Dy_E4b;0E4-!~ znO(bQZ}_y=$o`3Y_Prkf9rz!LIcNv?x1vU9zFCp1G8U{hyf=%|+#*!hw;UsyBt3+XpvT zmRaANPE5_|B{|Ac8Cxt<4V@ew<=5n7<25o|NvZS15eCylH*V~#wCgt=iuNqJ!q zV^)Va>sj!gNAY2fk{6KWY*QYdD039#E8tR5tdbRSbvjW`1Vf`s2@TdFsw&VOZgE^NUOey*tyHj2s9qQck9jfv0?Y#qVAv5Wl*nIrp=u*&A?U| zCM*BO2J;Whz+AFh{l0g^bFV!2igMmlFTea0_*C~8o@bq=Duh5*)CVny+U-`{zE-65 z2#fGW5c(MBR`tizv--z$px1O2Ry0droeQ1Ajnfg>{cTW%AXQ#faSK_m^ z37k|04F6e__0aqJruCzFdakqGz{7RXI@8`3gd5ab7>@@>nh>b2&pV1Mr_*Lq7 z>D>5(QmvG=*AuK#0^sZ*rD?sT!l#l0CErS!o>rc*gA*l3%6)9^S&xURLiit!b6`!q zb?DHAC1oW=@UD@fibi#O)a}c%;pG6EJKN>Yc3VBZhS2apk=JQ4Ssd5^@j9&748c+x z{z84g@G9i%m67T7N#3zD3));CBaPXO^H7h_8+7Q3M&h!v1stnk&p; zxsB}@_Te|`+ry3?F^*Wgdi9pY=yGa%=qdJFnPEMuaKM??(>ZA<79Rv}1sKR}%f0x> z(P6Cin-7N(?~AvrUX275i!V{9O6SL4L2X#~UL&*|t^#kA{Iv z^$$L%{@oN>K701^2k>s2B&&ziDeU3+4}f=)1nhJk(IBsf0-;?KUWh^CTv!+09>d(# zl-t(C)W1LYpj>#y1B7L__<{FM^B#PVa&HFg?VMrUkX?@59m!*s|1LiSIR^u!OuN=i{U0oWz_CSI--_uSEDl-A}Z;J3x(iV9CTK@nc{Wy4|4 zFUM6>{#mR%ftK(tP{$irnZ9&aSc5V)o809%RS6kOtNGi&S^FfT z(O6#6%(}I{O;zVAuNXgZR{$bg@&SG{hrgK32aT+KGL*ACn1qLnjpuL~S7`D&$Ym_& z<{Haoe^31Bm_OH)E@RxCSB`O+jM3V_T1m!X+-pqA*mw??F~a(PTq zUrxmb8RI4?#zT(u#6OKi%1zmPh&e3eoQfB+!)uWQo6{4&B9^1Gs^NV)6)#|`083+M zDoY^cV>dVr=OLNiq6f}EPyG5=ph)Y!hI4Ql&O>sMfUW3>e;2E$P3LuL8s1~_(*m}j zC;n)xz@LtHY8u{Sa<+i2?uj3XRY%hB!Y)*E4Ro@SW8tolH`4o>(4~>mi+E`u6wOtF#jK(D?&3YgZxnx6{3ULT z$octVNb9?owQxPppnZtb&fh2Fbzo4&=pK&weF@4fWN#UCrHk1G%nLuk=@VT!7-S8> zyMudzZv;OKei<|cm6DPSy6_t+*C*a*`I!7Nmk;yy#Qz!dIt4|1MDzd~;64WZ1f65@ zIQ#H?LKUKg>pK1IzGP{M2atf35N3asj{7=yVymDr>$4=^f<@4u6^kf0+hfSP1^}Tk{LOe9lSnMI~H! zIDYt|^Z3{?XHWdgSOPiMolWpXUvS^Xal;pBdY0c4|5q%5n8OObA45L{-coiw@TSK^x4%D|2pQ%)dZ^bd3h2Chs9B3*b6I^3h}A| zA1Tm^@5MQj%efqDx!k{E1p+7J7>_ygm?A(4_ENx(<8nM*z)Bx+oHAfX93YVF{ z-z49gfcEj>Nry1T!5O|^EUvr;?e&MmU1hdeqU6?yk`j5F|p1_Im`d7?p)J@KDnc4L|hPIevMoe75Eji;JPlg@!_$b4Y;~_(>idKf!DG2Mi$m zO3i?D{OZdlnd^n%A5TUAo_vHcff+Kvou`*gVLtbAycp{*1Y8YEU?e#3tNDCXJHU8s zW&Z%b+?K#DoK!r9XSJO=$6SxWv*xEJYHcQIJQo*(=R#Luu`{)g&}GdBavI=e8Q`J! zK>tk=y}(U=pYzRe!|%T)U<+w16NojNemWH|^p1QR@Va~AYRs*(t>Jw-6)#{<60k+o z#UhY_VewT|w!#gz%?=c;3X;1uzSZSdq2o5_3^Y=U4$2M}^ z`&G(RP!s&ZK`>DFOY$sN0~(HO6xK(@@iBc*Jd{dt@X#F{XHbI8_08?gvziU!FB9tN=cPiCQ^SdJR6{w8Z@55L)l8_y(auc$nI9teWqmUiq z?7fJy>!Kz7#MwfAs3@haVE9Z&6IDNa9FkS=k-txoBQ$(iC4C%=gcDrw`B98tUmh-I zv;HR@V6Rw<(_D$8V_Xj&0uO;F#5xf{cs%CKK^b&Nki0Y3A#q=ueAt=P25~=9ALR4o z>T1M+<%7zr9Eld_r=USTjMIQm_&xUtvY*x#pz*I5avF&iDKyBBaT;i|LZeYxNi+iG zm6tThL%MYRsSI$K^o1=hP4Q zodw)iavJbE=K@}=Cx4$r&-sVHq?$?KpW}w#5$j1>gQ!?1Ylt~4$IItY;B{mD#;#%G zFct#8FY1s3)CcI0Q49A02YxODjoW!V4jMG45)F?@-iT0yMk5eFFOZ)ONDjRh8oj4e z`yqd=m_$GFh_^glOdNKS`gCePO@ z&ld?=N7;`$ExzDljY{L7Oj;x(eGH&WTUm^iaa|gjkZr$XK{C_Fg12KdCYtOhp6P!~ z5d9iroE~yre9YHq@rm6II@l<&YpViy56B{j8c)@xchRS*0vPKpl#&~Z|#Z(}(D zvriv&u7h>xQ;gdkr-5;|i1AFpPVx%E9`Gi(jvF!NzXV*)paf2Wt3%wJf)8`|Qouo{ zZRGB3I>VfH5c)qmi4*#d7@l-4w<=fXQ{bGMhVz)5j7ffsxvcuqh+r*rUF~vdng+|`5y~Bv zu#)0@?#JLZ`osH`{IQ^q3`CW{$>B0_ekD^*7k!VgNQ1!GFTP{47B3?o){A_Yj*@6+ z;yk9WSrFfRj`&7L%lSlnQ^n&O-hb=ooKM6zGNMzM4VJN$x(M|6hTfk)!s8pr0eZY$ z`U}Ye@y*#HzLD3p+|_cdMd@tW-y*lLf&wzyh=dN=gfJmdExy^FLH{4}-_ZY1)Q-h# zQ4wFmqK9gthZqSO4ly2_kBh{3fRBw5bP;pqBIziybEBe-iX4p8N7^G3B1;Jdhxc><*?w}d^L|dpr_!wSRMgb_yL-) z8N}KXD=4<+rTPK!mONiOoyBE2l8qH@{C+SG-*HbYC(n}ZJ2L46Z+SPl6{J{%(IjCm`f0@Kdfx^rKY18u)q6hesYbF;rYANa%BjMq>@yh<+?$ zJM=cx6PUM@mn+EVZLoVv@}d}u)4{s#EzQr8CL&=|kmv~cS&at8Pn-tkfiFOV{5X#a za-^I@N4Q_cIGeBL<1G5oWT1BwVe2WFVUzJ*=)*xi#vCtv@udPT1tZxm<^^L7=XVI# zv#sn|jbD-bvp8K!YVI!=dU=@7KhrgM^qBO)<$~NrA}XZ?*C9cJWY1|>OW6{%;j$O` zay%2JwMocP&>`7#IJck?fJ%>qTx)FejQ(e+HmGRlGk#HU4_E{_vcyO|dx) znxS!d8sU$B;{BPS!RLVv$)7UsERYq*w~CB4%o+Hh{f3<;6?_dNcxws2zeDhDH^$eK z@3G6Nxn9f|${of(0KS{!zec)G$jj{pdUWE4m62A$Nim9#>BUbNY6~6oX9HKo(O;7Sf{lUlCLAF)id11#-N- zv3za1PK>Fr5z`8^6E<=udm1!&J2DcPXwm$YYKo)_d~A6;F}8O~e@8pb=BZtgy(m|V zZbI*9uVjVsYtTZ>ia2RKj$)JFm4~CjaLNM=A0oC$h2KR^QudI(kseT7C3QmIcpe2g zNhCVHN1{V_?X)hFn6wM?)kxzl*j1(1=iFhXw4k6cyR_7SQ=T|~84kNDEj>jtKsTA^|+H~@jO&Tc7G;>9gywdoG=WJHn zh`O2EZ~5tWN+lb|%5aioTh`koV*~t5%HA{lOf&k3wN`dd{Hs{DPi#KpBfw6DUZ?Cm zLswrCu#G+OgR#bDO|Bf)06RW|#}pAR*8|GifQ6mOl&_QV!d!udTG*u+w{H^Tc0g%X z4`3YRXcPm7z3pdXZHa-?*2ft%Y(?_JpaK0*WDzzLpIhwJch6}`r#_H=n96lJwlaB? z-e%Y^O2G_2Wq6amhpx5Bq*Ki4K!0^QSZm%9^9NE%k-P?G&?@J&csslc4q8}`tctaq zluC||NxJN8!DPQGD=8eR8djB`N-WVYii1uedD|*v!=pDmfkACmcpan~^>7S7IYIT}b!NuH zN#;`t$^&H^O}aOwev!^`|BHShzPm>Ht7ymfV@RJ`TS-Brwnilpk@VBIkWBFx?;GvR zD{{V)U)pcqC`Th?Se5MCz=71aX43QKG%@x!UZwv7t?K`gRyavZZ1?ySUtxd!;V7%& z6mt;GiwKdv!d_DP-uBC>`{nS9e+De}%M0uUMf%1zbaEGBkCbx<@&V;Nz+tW_C_v}| zhx=VDJ4ZuIyl|fj9}+NP7(OGOJ53j8D~-CuqClGufb>Z0Y3MSfah~EWfl-qpPQE@`j9p! zPvJh7(@6EX<>h2o70W6LMK2Oo0B-&dI#9nX)}Fq_kcqugb#0Fvkdcc3KD5QqkhgRz+s=nTvbEDhWp*c13NV4+{q zNA%DA^g&4i;s*TFj6H^#-E^*&D-7gnLtyr6ligiz6?yCf>}&Ny+^KB^T?FbjiPH@R z?h1SmhzAT4@Iwv;m;{pRIgJkkrW+FEjLVjTLaGd&#=h#MJf;`>>`jbf4fq4>*Zz>P zUrT$t!}yxybRj3cw+nxekw=>YpZ5;Wvx66${xzWg2SXGsIqp9eT`h!^ez) z>=1jAb;{Ui>x~tKk{ROuXrNL~A!zXVwW5ON*UEX7MTspH5qlAxQ@L$&I`9L?b%-`} z@(nvNXGd^3ie~?WAIRv7t(eo{@rIxi?1|qN3znvmB6*d8R#6JAa!!l4D=Q;fSaaMS zD=0}NN9{7{1(W@1Y@~iQ4QWc-u%fZyw8#&Lex<}-wArl44I>-U2=zm!BuQt;5Tb*m zRB}@>DZe7_0|cFv*ee($I#oUKf5)mcT@WJVeIh+GE8wqD^8+tQMjm^SzR{e>WAaj| zv=CxcaEkJKoT3yGO|=28gWL~*KKBD3rsyK!DJ&#~k%|HktAb49H?h*NkZ>v<$k_BS z<)qkL-ag332KrWm-Aow=0bh$%R9g}!sD%Dw&81LI3i(CoKlZ}T0q?qwg47>lrDdkX zsV-ed)94g4>aR|xvL}9atg_}m&>1N`kU|F=mP2CU^8V;hd?os$*&FDzw4gt5%{yYX z4gJwE!RADNk~YV893X@1Vih(0(!t!W$t}zdfH$md>p`ZzC%z?CKSU=(u>kZEGQ-@i z(b!J8QG%!4L!67eHCElKlOTBbBh$m=k2L%Ve*|_m#VV>a++sYSQwsSbfgAqlW#ESI z!cbftD~)J)IqY`8rsUTVOI^TWX^-w-*rQ9&Tfdx|UkBb@0v0~;UHHTk1ORlR~GmNBssm-X9&Vue?8y+fSlRT^;&U))ViJl~wgehwCfvPeNZ=3mzq7 z(D8Gj4csj8I{ayn7)=3aN z^vblJ=41_jld=f-F(+e4PN8$?Vo1bzKnDvc&n9p~ueA9aLophNXcA_qz9z@dD+ zi}{&4j5q3{A(ZHe?~S3#OCk%&+lV|Q?bRc;@|dKN3;D?L4Q@=)_s0SSX?e&237Zf! znv|2lJ92r5RP+od=_|*}^MkLC;4jP;0Doe?!;xLAqZf81`cLZ!>c1KLK`(0X2o70^ zx1?4_FW_m;Jjc|KU9NC{ZoUcoNAt)x)xtMHo!*JnR+{*>o5<5;!XK1g13Y{z9k-GP z(xJV9wR!{1)-&7e;4~;^=FvH_RtI8@8WlZe&TR9D^b+XsxB(YdF~=emwJ+9|Y(=pO z~Ttb3^Uv8=52TkrO~ptB^xDUfOE{)d)4H&Q+f+G6gYpQ zM)6pK@-Gd;60}nJGnXdX?LoVtqTT4xCuoP&GxCG9zkzkR$7H? zKX`Nc@2hdH?lR*+cZC#TwH(6^=!Nxh1&<*D1;^1BhH^Fk3FaORhO=@UJiGhAv=cw3 z&r`U+Jv(f z)T%AniZp}kVR}2W`5x)#LU(m?#>T1>(=wZP^ENyWpiwinB^wFdbwQVY$916+R>h?j zF9Zwb$FPH;>64`o%)ve|0W_H+@W?caLpop37-CZ?F@}9%9{Dd|?$Ka4J6xyw;@RB? zW|2A#=jq9p!Rd2d_(;DTLI}+3za9zlR-@$zn zX#-X=_@gI&C|0J?(EV6un}i>exx<$k;=e<&>SQa;Ph_^chqdauAc##bD-_Hn^)9o` zZcJj(R;&kfTJ_b*MnV^O8}5&U%y!~@`%uwl&>-qn*dA_35bD3p9>`5=L%Cm6{XkTslnuI=52bIj+vZkD1e99^w8cQ!mqD`q#O1m_9m(+lQdV{aB60&rZ9r zfqLBr`hnS#2_wOsv+KF;p2scerfoC}GujNs=GT#!%Wg0X&K5m+X`Rk2tcZ2o12t`X&}C)ElJ4k*RHT z|EuL*vEqF%R;p3ab&F!V)HVtKi_8wsbz$A{UaTV7iZn%wBU9U@#F0GFTUuY5Op%e| z_tPdNj?`$?S0o#O5Zbt~)*J$TKu&00>P##baINBaEg3MnR>@^am_Yof`2g3eMmlAX zn2T{HT^~oOFO1NuOqlr^o*OtGp;sq{5qgyhL+g8$`v9KnhCaN*^?~918J#ouB36*A zAA#1^oN4d}5f^ihMyV`XNPZM6!Fx8kN8@tIq@nABjM8p!G*F^H#6lVsT^};rB=kXM zcq9(3kahb*tUTFD(}&D<_aKVayqX|3Boq+rCHyDPaiq#%H*do|n?`F$d9snv2Tq62 z$3kv9AveB0keN7di#+R*SgLc8PTbPTL|gATzZ4teE0Kc%D5k^aeLBSp(f+X{as8O8SvZ_=D^bp*OU>NC%Do7HetH8)$wcv&{}pgW@>a z!e*^>I;B;kqU%j&n@4yXKI62tQL7=;YDlt`HZGa%?tyh`dLxLHw+;~OCG{q=&2HX? z>y1XMd`Pm9&>P-{&xb;8JCpp;xdUhqWs{pnBxt4bm(p$zmmlxfsi$h~KJBL+=0lSi z<6W*yPxD)>j%$tZTNyA3%}RkG-MW?ZMwy=Gx4>{hC;GzZ`j-yVq~S^RTbX#$VRRi$ z;E8_@ews)}!H>YR6H_FuX~yXM^aHae6GqEJm=o*#{aQXyuBF5ZHVD?os`}59B-$v0 znGyDZIgz(PqArdcVn%L7tQn~dt$$7C{R4Tvi&`*Hc?4AYSZiR5HlQbdUu;1CzAn*587#xC^YP}5 zYEP@JPkKj+VQ_CWkq>lNVBSXF&0|)~GGz!(kQdBocM#`HlkNUPelVjgpT`q4|3jWI zqb;A$6K&zguv2)#d|^iWdob;#@O|REVMg2CX>H-J6SVuuA5vRBU!P5Tc;=mu^U!p@ zPn<{0Xuqd#`xECAd952QpXe?(-?R;uhf$_%d|F;H1Mc8);HvRPU`l=w7?EEjjLN&f zn2|&51B>I;iJ4XrloKVc`H7LV{AnLp9{uzS8&IdF=BvndzTjAmL%Hpw<*oX_A{HX7{}n9O za%r$B`PDvn5gYXn8-Jf*x$W?r7uik*EMJ(?I=Ua&J;#H^dHJcy*q@|JF)Wu@3FLrm z*zwi?8SVHQ;D2bx*8z!k|6x6lL6ffs5^evBbwNgZzAi|#=eBeF^+85kzCK8_{SWH| zYRl&x#1LdVkTIWk6p}IL`^4*ojP|r%$Y_7!bp!lvV%?BtL9;<07L>9!z`emV*rWyZ zg(VwGSw|#o$nYibQv9Cu=Y3%{8#)eb%9;UqH5)P@1KAJWbXd)X`ohLP2fr!n46>mg zxVv2W z*+_Q?Y2CqdsaQq47^}klinQE|h!ZKUr1+)Wy#F%J5swn}6-%F0jvimqc@;rRiz{(n z1$$|q#~KG{WJJu)<13v;1>$e4I;r1jRQg{Ri9TT;Tu=TU^_ORlriVXzjuE(L;cz-CkKmOy@pi|AZ* zVO?Ra#vPVZ($|Ko*fV&4%O;gXowNaHwL0v0;(0yljH03=e;yFEam*=%-tcKUzdDgL?PQTYkuZ%}6(XXJc- zf1U+}#*NuN=X!LuzL8f_6%UDtvdX1(C_YNvl$#oh@!i&hV(v$)CygHyY-_bnw>L~| zubni0RPj8|49`5nqbNf!dVUX`O8JTR9Jq3lH zLOlQBEby&&TP#@Rqts;+%57@ZT+_?{qC`7A`bI@!y$Oy`3s~bwX3y^I{p`MP%TFIY zqOhsKa;CF;eC6q*qXpAlXF96K8eZ(_Vff!xT3I@%dBbx2IDghm+{=evh**{EOT5EgW{f~n)B@3YOgZ*1>gTTqpu6zPLrI;C+ zdPdb<^t6G#e+wRTc$|aWE&)D4>-g=EZ8TMz%s6{c$!e(utZFJK`D1V}beC{>%!8+h zxWB{B4(ILU%r?s4_(k{?W-K@*vHwGOPv>}R((tCj5S}!ce(<1nlU6?mxqI$|NDerY zlv|BkIeI}XS*kW%C0_u0HRAn+#%NBK$z*pa*s1blyIeMd!EIYF%Qk2hiVh!aMA6~o z`<|K&hf#u<9!%COS-xQTZ$4As5aw)n2rqPIB~4lq^spbQ(=*7NqU_4{xLnALnC$Bf z275pBK#f=PMS0?Ii;|?rZu;yu%O~mHBTN0r!Ag<@<@hnf8u@9{XV?!5;}z+O5T3OP zha+Wp#Jmia8wnMmo{E!d-aDPyD6)(rXJHmDqPn@TqZy>@cvnBvw(%MAC~TQr8hK)K`Q!E??Y94rgA47%=u+9d0U zdvC#BU<03rDW_$U?|72uf^0TuGOn)tU(D%EW~c!FI-KUe_#7#4lf-vtDD+Q;AN@b3gNzyW zPHFxX{olTcNBneW0DHNAhO1?jtN^v zwPH!0XYRzD%B^3%BmNK6Erh=_7;vu8+!OyXX4We8CGImh&}Ja7=O)^0jK7P@7_=^@ z!nF)F%k=7NV%{zR{4VYbY7@R-vhWvX-Cw}xq`~MjH;UwwN5NORjW|) z1@ii9!)*&~FdHpG4v$+g&TeW}9D)^x>O&JkOF{-$s6Dhh^lZow4RwZQhn9xchm0Ya zGX*Qom-6#WE}FB_tyu3*UpknZ4gXK+P@SwHeD7`r@5fER&(8ov5ObrtS8ps*q8G8E z*ftaXAS#8>p07+dLuilWXsjxtH%K`*u5v#i>0FtYaDjLnHlB0Iw>@nSgL=J|>^*9SnI>65!!z2F`bNKY5w;FGv4aN4n z4eDB*uho~O!jQ~v!mD|S)5@T68jR{m?jwe3drkY-^N0e@MvpsI1LlYZ^E^0#k3fnx zk!j+(hndg#7~gws(lkilX`_V_VN&9U4R6TL89zoWO79X>N3)F#^Q8^%*JjBMhjqQ( zM1|v4)^?02ofIkls`Y_6+(zee89F!PYEqHq@N z$Nm^=jwV{@baZ%%X9+$*=lJbr;OvJ29lEG=58nj zPR8TBNjeO?j%0d0i3dBNeeg*2fH7-$=5l?+{%-~z)U7t|r28w%biqHb^9Q-s*VI=R zh+;bkE3n@D73Tcs`IusjWJ(`XFTH1B{`&IQ0p?>$5w=*-<|x*v`}i8Q6m^lTMh1hl zJ7ihnW&ab4RCUhNx_h$&eSG=)j8usPRxe$!Pxm zkSPF|62N?DyjseVN~4}E*>1ELtxl`iiN_Yq3e+0!{*kfJOR;Dh_TYJz3Ou@yYtA)S zR8Lyh_P_($|LfFa#;)~kyY3(K;Ddwi-_^DrT>}1h)MM;%;I|>3up$UCn@u*9s3W}e zmIL}h559n>P(>Si{DZbDzdM7SdS#p8f%n?h{;=5i(b_>Ak9s=ztmJr#qFF9S(1Eok zYSJc{BxJ0cvls?zU@%m9#&=h?eE^i~RPc85+P3%JAGDTqcRG7Ro**wpC1&i-;@=W` z!LVFDJ=TnH5;n~jrf4D3_&_6D$tp6)q*5a}+CEwO6n1tp*nbB7N4A?WH!EQ`un*an=rRi$S-Ww9ah7q3 zah=g98E-Jk$f2MjCt~Yma1Kmbr}VI~#trbsVfbRJ6(OJ9CYkS?Zj>IFZdB;GBJyqA z4`ame@Nip`$=~E(S5~uXR+rLq)0~?}Uc2@MJ{D=XOt{>RXUT0A;Ib$W;5!2sUW+n^ zTambFVq?oI*UX&ug5k@Bo1@oU`diKyp0*h)y#T(T>j_-IW47Em-Hc&OWs9C!Pe;|X zXw!&mE+e+k$IH;iF?t^jOtvuaWi>o7-74SNWliwKjYk-(h*H^zX*1VUmK!(x_R?#j zH_k_7FH5H>J@PD$%PXB4EypvsHX{km>akgEexKQ6^V|tyZ!7gKkM$s z<4XLUBD~g{cxZ~-D%mSpRzk3$#NXJhk?Pemr}ciNi+a{CMxsx;#QX5#w%M{xRtVji zm2J%?Ub4Z9qBygi5P$Z6&r3p@#>C&`S)~J3&7AhUx>p~n?wiJenHy=ugq*>T`hz@+ zj~U6PjPt{_AS)Z}=!{shJ=qvSPeL^HDWVDJ$vti2C)p%NHik}g~qznJP>Np`6%8njyxvY1VHp3>>C*`Mn&+n(<- z8t~Kjd>1|9S`Y0{C>li~JP=XBim}D0G%00%`P!TnIqKbPcHujPY>sm1$4!Ri1@{)Q zmFhJ`_i}pI#8)VvgPvcS6CLWXSd1n+iism}=tXYQqZr)q^|?9O-siep+0RdRI`Gr^ z{B)Pu@?4j}{QPv8;ivrkbOUBk;zbHT^EH-o!46j+5B|oLNN1}|@s|0^ykRds@UnlZ zx3LB8-EOu}y){3dEl_X6Kh#^cOoSQMrC=H6NxU8r!S>OF?H{R~XYg^D?@$?%#OP(WNy7S^?JMLL};eGOh%Xa;8 z#frOjkR2)vH+)QorT5@p&A?d~&9Pg|2K3)xQ5;Sqvs$cM(NPPe;gm*Gv!;Woq@c(kYu zpQ9%NZ9)|D7g6}) z#Yr!+zbJ41SZw%+J;rGy+SzF?wNXo}Xo`RU{Yy6Xhjaw$MQo#vWP*y&E|osV1F_+l zJD#wkCp9%Z3fgnlF)@F^HUi0Cz>Xov{1epi#1YWYyeYoiCi9>0H?!0h|F`)itrhtt`0W3 zgH?5~)aJkLU=MdNwuN&#uY;Y|!P+^5tAl;Qnb_08zqparjhrxP^tcJ9 zjgyP}e8hVDd}Q3<*!NiMsiz;?8&hw+YUL$YU%Beiclvz74)^&8Hal5a&AKoTE3~fz zWyRENge}7|#9B;Ds|Bn{nH+s_yRup~_kK@yfOhh3J}-PrYXbAPYzpkb8e@jp2G)%Q zG~%m!0rLmIRG7bw!)0I&`F0-7A>Z=49eCSYx?Y}$lNP9m5X~_$W-%%@gb~N4yYNce z5@j73Xgz=9kK7j~Fud#C|vmWs-@n-;PfQc-O_>{Q-Y7h_ZvTukC-Mwo}Rsukr`w_yvnr*t`QDK zKJwEal^Wv*7r}qdMj$ko%igU4Z}z4spPpYF$9sFUxMEOy1^GB`n=`%(}0rnwj@1&>c-Re%TH z@l77}MR@iXU(`J++_LUdkvG(FDwxgSeIOfy`2u#i(iO7*1Qu@8TTwNW{{K)s3Ns<*jLGGT54 zZP`fk9$&19sV`i>(Co`EwMpekbquwW;~vmDhV_{VE4|XF+a%kJX1sN5wkgbF;0wtn zEFsB)c;*ER&>F3+!kk+@>9@y@{dUr2mt6)kQw z*po9=Qnsm*4I~7s-kFrEbgArAY$yU`!)tyL*3K<>2NkUNky}t)EF@olzB--VzH;s7 z*3wVHHa1lLZBNfcIo$i#Ggv$@Kz*&o&gCPWFUzpwK@#E)sl3!2#yg-PrL{2!u}X=k zs3>%!I zJ@=lO%I@>7|8~nk_KS~x={Z{sO`AP=*UEEX`=`iG`6loQw()q1$scacZX=Vs>Docd zF1hhRwL~6o_~e=M-g##$oA%)}>YE~64PHJ2FMg>AbtfgnUnS(t+gj>abE^TInf=A6 z##Yg6z-z>2U!Wi z)-0Q_*de>;-*X{b`pX4Q*}3Sh&DXJOM~^$Si|xFNok4Qg2-+Wkwhy!=R)Uv=F_l1| zxsBc|)TS&4140ySt*C-tcmJIW*A^}=J^yV}-lgMK-nfkoKl{E}v#zL~ZXBgv@aBh| zUG8%ji#E64^5abhSDw3PzONQCIY&0jR~WXTe{-X`PONDpztx|U>oj-_cp!bF^dtD_ ziSLhwt%FWV<)dSTw;>LXP1rycT*d<>J9>b4wd({rHbx{=>pQ@17g(I<%oE1x{!S-4-pwkOu1 zSg2{=<+7(a8!P2p`HcaOZ@c#Ax68BC$?Qt?Qubo+HD4W5Kfm*hh~c5Fk3Rik4`X{b zs(-vreR3mv9Wfa*e$R|IUV$!Q4p+iToz5CUn@T5-t==^FwlRBy?dk{2d()j$)!)A1$lp|a z=IH7zYnkgeugm*JO;O*9PG0usIc(5x*~x5;`@)MYIa^-odT8kQVLvfD%OC&5Gu?}v z#lKJneSe4YOp*E*7AZ3Q_)b+eANK{=`&fnA%(c=b(NWb^RgSzoyfEx7RKkVfx&dy; z!fJK9gXOMj=2WVy%gd!%UF8N-UNCPKM%-N>Hxxt*mKLlp*kABQfvKP%h_cO1NS@Mo zi)Z5l^`ro_8b@-EvLQ(ciN~W$C66yVmmVR`@i)=y>H0IoTtwm02uUy0Xvj6!F(q(V z{pqP)rUxyCnx*GP$|6ITUd*yKbHV&keR(H#@=pF6s~fX-;Jbz&)IX>{{6T%Uf$dp- zT~FD@b8Yw8o{K%1#d08$N7=Z)v5D-mJr56Eb2;=M;kKa;0l7z78jT{?V$E`x6`LDx zV|y(Qr(u z%nBuGq<5PUQ!~g!6gTv-#l%58Sr#IEpeJCp2g zdx>4KM}sa`&{Yp+DbV-%3RZNX#-!aC@R^O(hUyEu1{eYuNHK~{;zf1|YYhAidym~B zM$qB}3WX^d#UoZFtFX1~iYZH1T>ZG|L9EJU<)mBAUvopz2iwBX>AE>%f4MNccp&8@1eDDnC7SXq%V5($PBzuT6Tg{7t2@2;*X z^SV$}9QD=<9f5Grj^`9+byZ;1S?&*1RR*L=JT$4u9)Gq8tJn6Xc3Sb&qdFtbg!190 zvFBsWb%chWTeir!^3_pOlTdK{q(#0LLi2`5aQ-m%N1*Ds{mm7=B}^~98$%U173O74 z6?vQg`Xl@Lp-uSuOnp~ve{l2W&B_cZt}aoRu-P}hu9R+j;HNj1T%vr%Hml3%d+Xn~ zsx|D7^u6`(^rc>={!6KP23Cz)be8#$4Xu?br15Mc_9So~XXMEzS4#Gh?4iY_r3HDp z^>uY)hYroPE2ie=%1V=|bHZ3u7mmK;%^BNl!5?FrkwR+h?5eNH@s)LU`Ep$F0PUkY zy9SSBifd$WWc^6RX&Twyo*&A~&+F_e&Mz#?Z+9`gN$wfz$`9ttZpD@F$~T$KS+lyZ z37j46WDT9Hq?5Tj8A|BC(aH9AvOS&b?oM_?CtKgimUgn)ovgEyec8!AOaW*}X*DGE z7kBq*8SP}7JK4HUwj`aHSG7O@bbN^<{!-Pgxa%k8H>tmPM^52d%=>H z3$|^mYAYNjAKLfej@^ZKdCs2J6B)C9@q$6Sc3gj7&JNp@(OaipzWn^5%DXQe?S0R< z;jagor}ypN(*tW(v)^9v)mK-jbH|U%n!9qt1uOH1owP7m9V{srGIjXUOBP<}X=t8Y z-d2=9v>T>~7z}S6Sj{}IguId4EKM;^#4pT}7k{80F1OGMyx&_V4V0Rt_0bV^jRTdO zVmwG~%FW5~xlQiYmYRyN;`MoFbu||Sn6IJMW_Gz-o*}C|SlmDWA_p?vdtJiJ#N@o; zR;RvYaK1q6#~AiSat#NNi4Wox#$2!4XmXnhoLN~Vfr5gN$K%5BH&@;W7aJ^Pv6BGi zU{NFt>ECYbBhla8_*^8*pr-8+0bpx zZ_gfE;${}N#ho3>SE5$eSf#pi02_d)t-Zdx>0k|l2O9G38VaaHKZFbB|KcBrDVUbp zH0?H3@HtqstK4vTOBqjUi`h%TY%Jd!K7We((Jy{JWnq}Hl`AisF~hLZtgh>vyQF)@ zlDQKvmtX5`l-tI&vo+5>-#)7K!5Q=C&*<%(Hz;q;x3^z)!>u>|?AqS-WCxgo-Kgg> z1@TFZ^k8&hNkxUBDj11mx$XZSZEpe?RgwIUzwY@DPGMUMJCy)RMnH+#XhCAHh zX1Ed{a;n_7K)4h{MNS12Q52EG146(H6kT0b5nNXhFFbeEb(P(9-8IR){65uhCI=q- z{r-Rd-y}2h<{jPD)zwwi)m7EjC`&}F*DEU#O1t)H&PeC5KwHEeKt~}*h}E_&wg+vh z&7YlDCfS^}YMX4fL05@Pj;x53>1#{mlaY!8U)(B3MoMmX!h%4o+h~*)1Y~0@sVq%= zNY}pR`{LdJ-_mujkjB!4vJm2E_y|LEL2-!(-JHhd^b#L@IZAv{EmRg`sxj5zj`rXy ziSZLvOCz31`rwHx!K5Q?mdf(}HfvJ;^TCrJBt8?dY}VcPy4jgF+EiI#ue8x_ig|cd z)%b|Lhp_vvvS!HW)r{3Yz9(XQ)f3OJ_I7WRwdUoMgtKJGaqO{-`Q&ha+X&cTVbC+$ z%{H0CMiXU=ig7!vNa$Y?Fey&Z0k=mrOXtp*lQb_2iUtZApcaaePVyISB|cKS7$mKc zgm3buv$J@&PlKoUJ;C4c-hT_ek-p>XCb>Ny89Ykw{lNwNu6e;PXu~W9o`pU20G~%1 z>~AB{%6>$ICDLxU$VQ_XtNWH18GAaq+~8a`SWOkPTi1nAe+cnmt0{pJ{sWapX1cKf zyjulGYEfnp-n3@t=9gGM?HPKX(Z6t*pxmZ>sXcJ?!NZT@txaV9$M+mLh&Oxf{`;W| zwbf=CzcVa=ZM+NeJdTnoq@Df|;}df{qX(yV^r25jKxgVP!knExI?+@)IlG^)-{gR2 z@aS{}IXA~n4yaDq*}XE5;!JVI<@@~YeK|QkC2rE>fH^M8HMBC|ijsR*2IR2m9mMvk zpg0J{IOheyg@q8Lk6&pvT8=i8Ov^?GBRHqTMYEn5ILxwaqCPTI(uggIV-?=|p^id0xieRcjB` z7xtcdOZTib1IA9Q8kRLWG3AEa#;^Le=d-LJYv}6yK|}U@x$FS6rtPE322B{-um8mQ z#&%`h3i}pp+`Qzq#pi<4{rz*hb#6bG72kbyu%=yRKLSsQjj|seOU~K%L1gmhcrdL_gw}Zi+<22UZi@Eyd!!ODhN$SqGcBhe}wiTLRe8@wv`*xSv(7A z6GmN)hc*Edq3Np=o;`P#m5=Dt-5<65)5ljIVp#`{J@P>Hs$B=W`||xoZA%6zwa;r4 zg1d(JS8a0bP3v0X>&ae%<=s`sleBkPZ}#my)%We1U(~l(ry<&^HD959iqO@2U<}1c zDX>fDNXPxt;u!KsNU>Pb$WBjBNKUp$PMgh=m6>EVM@1mLZM)n|Q)M7S`QFBC_9SLc zjQbv*<&pj@Ph@_kKeIej&bRyR<#ss_Gt@|DWJ)Avb(yf>*fV8|%av0RaLJbNoD|a; zsPRzS@PecO%}x~4Q1MN(oG|MM%akVR5*n~TBou~p&}ozUj+tF52^gpn+2Ah4te_;- z9hsVyo~nE!k6Y>c^FeLOSt#*a_^_w0bzBlSBM1HbtfIa6+>qcxzIpf=ZG(KJ(}qQB zgCEEV8>VddY1N7i?b=O=(}Go1?9x<07tr5=O)lqDHgq;D^OPoT`>IZg(jqTIE@$TU@=(0o+5?* z=T4!{WQGwunWQ_hh7u1?O~aB<3EJEp<{bzl zunS_YWYVWOeY8FX;9;Ap1R8204&1ikVb77?THxbG$xyl|d)FghKkpb=@=v{HaMw1;W>~ElSrjt2;Q~OAJ zf%Ro6x3ayrpUge8o*D1Ff4KJ9*;};n-an_1j`t z#E>J;ThGaR|Ej$iuT1x7{mfm@zokUapQ80#5N!NOQ`XYlP1`HK#g-WMW7cNZ zmNT#w@asN}KNv2lcVbO~{D2B!MJ|KOt{n!0RD@_AAD_@B#vmDvk9T6Z-kxS?V<;|? zuw{^%nwgnY8_3N(JwCUf7Ip*pI@+SlaQTtVr^m~10g|b2#LT%AN~>T&;L0J&Z1ztP zG}@#vgfs)_Z$4L8T7h1fQxI)S-9b0987@7%@BRmOAAID#`Wwgdx$2sRjwc@4S9|2q$M~z- zvEgIP%kTT@{mWn9wb)uV>Ji|EORmOm`6_iM?cyg5^k-$n#W`%|#F!Wk`IFsdXd7mU z&2Zo^IfL^Io?%FF8L_6GccE0*lOTU!Oovm6n0|?d>fW1ZBEZ*r87^pso z`-=&+JF+;6Jv}|OPxR88=dAV(8(Oq(#f`T-bcg?*POvvs%p3B~o(CS>`(%7_|KtQd zaNF+t?>VEL8~Vev#Y*(7l{1%Us-MC^(ZA1QLmsmG+Mv~@!$LIL-yzFu&rEXJB(s6R z|3NXk>`L1Iu-?0A={5{jda`9779(wxt5jOt8kvC2oJEmg# zIL*mdCYK53M{9YS=leh$?kx?MtaLQ!E#o8M!%MVsY(0Q}VE<=*_?2fQwBJs;rC>pe@r zIl{_8XtFGpHW2U&$q%I}m&JlF3MR>*q6(vZVN`~%K#4LI@{IH}g}bAp6kkC`6rXcD z8@K-Phd00dhw`ywhVeUJ(w=y}3j-Pv+r5=7<`|vTm4;^9|pv~v`a$_v| z)rCo7)=s z9eg>T9g`A#ojveobY$>Z*%-W#^CEko;U|7ya0b6`&K#13H-QhHMxUi)G-W2m8mzHa zHzI^(cU-ERnwgR0wV4d@aj?@`7EGKdSOWZIMFU7LCCLnllm_Su#S}aW z%Ohm}6BBe-WCu)>jFEiyzH1xq{9*gOKit;vyMhCw)}I_+R{6m;-+|F57Nu=v#s`kT zq4_RNJE+}X+;_&>oZYwbJD9tu$0F?;8vCZk*NjutuOW@BSpO`NR{DFjiAQ91jN9EI zO@Y^((PT0pIK!GIIm{)+9pY)zi`qcjOuL~rU^Bym2j1kF_>++o!dn}Fo3d<%edbkR zg>b$i3^b%v2_+MKq^l(%9#2gSwN$VOGfOu!ITbS!AHID$lW5x5w1v%6@4qAlqo-j( z{fWDk!3~cEw6hcMVtwymJ+x&j{-chlJ-G9D;;J@hZm#M(_`(wZV-IaP7*id4eDZ{h zUTap(^Q&L~^synWVG*RBd`rVN{%37tFmc)XTb9M%0No5{hYs(gl7T#r}AsjHn(JOBfb-DVGOKh+mME@__7ypln+H3iAws1dt-E zX;7c>=0|FRj1LTc$TO5K^{;MbrFVdQU_UK~yj>yerz@bsWE(a9lkm@5B>xn=fcPaI z1tSl+d4)htS*x^{$~4?)y?-DGS-*AKV2{sKWBe%@rBMq~R1Cn z4jKbiTd}g(0?}k#5Mx}|Rih%E$c_Q`BQbjTKi1qmbAH4#iB}%`>sV8H z$L&1rvlnKKi@5iw_Nn&0_BZX*XYaS&G~+Gc86h(dOI3zRyf@+zk&i>Ns%Fkr)fVC4 zb`!-JUkI;#3qvYuM-X#XHsrPs+OeCj>D;|+^_DHG%er@#KVofrt=-u_qyNsey~wYW zF=yk;{F<^_FB?&g=t1aLR>{n(MK+THOk9X7h>?P_g+kzq!4fex4rcE(mnQFTuBRdW z=Wtt*CIxWY)d+{lCfk1r?kuc8d)yQ%1NSm@b<6VD>gJlRgB$T}aON}FfDj3LVe9d2 z0J{a$nZ?MRVkD%%(ZR=ao6CB*xf1!KmbmsvUH$Qr)xgbWgU1%>j)=gf;ojH?MA5lH z=1AWFvw^~UYS|lHYp%RSzcc~Ys+}H;d9MQjOIC^Mm%OnyPlP+F zbw|C?)a(#wM2jtZtOXug^j%YpE&DLYo1v4iePv^rpcD8rOWpkmE}IQIuq&}J{3=Rf zf-^oUHU^9vKC1=S1Y$?M64s8CO z7dI#FY8;jw&&F}H9H(NnTaC$U#}I1;rGZR9^T;V9Z2ZvG1)xX-WJX)`r8r&qdVD21 zM=2@x6-3D-d=1d&XsR3~KU=o_-SrP0UH#^&ksDGJW$l`x6&*6>&3*ae+mSc-8Ft&4 zal?n)ULhaer`@i7x&8H;gRJ`_Q%5d(<*hd9+q-LD1?$HySg+NuUvS&K$lA&wl_lj< z=xr(_@@fCAOtM&b1fues4ui@g6s(>qQI-@e$s%YZ$|A|Doh;AE98P%qva3#Z*2xga zbB~kFbuz4OV|#gmc~QEB=sv=TaBgcyh= zQ*j203)U3`BW}{X#M*dA$T{XtEy;w-S2ipJj9acStv;P8(b^M9K5kCV)s8`fx7~G@ zL!LC?AzR9vSq+asM*LM9q8>%tjpTI+pig;~O!Nq(qlI(A(k;)GZ zwP%%=v>}GAKg}`v{&NB3h_d#etbCMZmHPNI;Kyy2Ox$7ser#U>QV z6g$X5x0-lMv4BhFlO{Dq3>p`?(_`S6v3{_JuL$l`dNv&AWx+%8!`hIW5%kn<)_p;x z;ivmh_l>+#DdYcuUKQiF=rO0q0_+3{EiyO3tcH?qlt1T{n5tmjT?xX5zdT}ZGy(f_ z>YK%1U z4Z})IQ7XJ1_${-vFKb!EKiCs%B4qEP;1Kn8vICBhURNi|m*L+?7V+W!j>)l3H4XM~ zt7OgV5c3JPl#WRFK9}XHv1v|4E=BC7KgwMunaWMv-&87o=Eg}=9E}<}Bx}b{jt`B#0;_(fyukU?i?B*w)-gv&c$Fa$GKCYeU zyQGtE$>0$;->mt3ONNY`Ib%%UrF_W2k67G>EpI<{^`jRRw}PyynC_p62ia?Mvm^zLvV$TD;iDYAjR&Rq|B=Cq`X#t?<71I z)2Jzj;p60zlKplU#pGI|-44}CK@kN_4-0l5Sx7cJv9KHVV`ec6-&(uw?IqJ++5hm1 z2uk1e{f71b*r7?BpZVgmb3}!Jt#O|=gzr#4mtxrc{>BJcl#NEO$LcH(NLIfUKKM{6 zaJWiei6@UmTA*5AuIds$mAi}f_snoO58A4oX_(z><$bUvL@e>kRyp6$Q5Rx6-@zMt>U$s z$4-d7>f;7Chul)ar^On%>)3Nd z;0bMrYrFSr<@uwvQ9I(7%&+fx^c2ZcR<5*@f57-^C%x*oCz*1hOi}GS#AF`}VCU-> zYtp-xCB@+2geRd)d^@AXw?7T}tYZ9a{cXFJwM|WpKNgsi%A{0hD%Pb_t$xXQEIz7A9BpMl}Y|6dOiwi_m06BanZ`He%uG@a+P6p3!a=i5z^ZEN^aao zFcQ83gCSf{$(iuF(@mTO;|1EjuvHfkK4=t|xUq?J%d>Ep37>A$XKVirBTMJ^o4<4V z`db#XUpv3cqEX^puZ2D4+*i1IZSqo_>^YDBm>btV(|6Cvyf!J5cIISh<6w|)n|Ig9 zxDK||3ET68&MozjN|X#0ZWmak>0z_lElwjsV`au|SjU^nex8QOiLZW2>YelzvJ% zjwtSSjK9!ogm46L*lh%;*{m=H9rTpK97?G&M_H{XHZ1H*#{%Vy>3*OT6(qVuEUgLo zNh=OI3nRr-7kVT@>0Kp%-jJxj<>z#Oua!1rJ`xUAV*d2s+C8V7o)=$(w4)qK4e!H8EWn=efi!KB= zh3kI+^_M7Xpl6z;cK&E?!jdv9L5gfebQgn?1dCwag*+jk(S2dVU&nl3q#1q+srY<= zU0@d)YI&WuPFu%Ua6_;zxM4Uqu)~^|(}JFAlzz~ER4LwXLwOuJAn2T-Od> zescSvRiy)cnMrnMR;n|@YzXc#j(+9G()>1s7E?}kcKS@}1IE{GUTF|EpVGc9BzQo9SCk zm(^(@cS`-QRhg4_8_V;P?DoZyIy zJIxF=<7Ew*rpTDYsL@y&V()F;YApM4plAobE4W?Mqe@vPKqwNPsU6RaswUxaR47q} zuw3KuP!SkJk?H&{xlM59R$jZc9!rn}Z*aSu1bC?&t{PNP4#+~|I7B)8`f>n0bZ?8? zW-A&{_mQ$K1P`?vZBV6*a5**O73ec4rG`ogH>RmUfQR3;mEYU&p0TX{maRJO z)(MpxcnB7`?zdp^MOyg11dFn*4ps!BBY_)~W0cyTM4}Vu!=fa`42xQ8c$yI;NJ}VN zgd~6kED5+;;z^X*uEvP0a|E2mO1j+>7MgJjP z$w}Fa$qtGL7f5*rM1eZhP%uHL8^h!yxj)3O zSth^)SrRJm709G63B!ppt{rUY(QNn1Lg+B zqyhKf7I5a!TM!y&RJ{08Pil!MT8w-`yGl9>*XuxARP=D~6J~=AB?31x76nlqSc=I6 zvl41K6)*&?&Fm%AD5{dUS_{017LlhN;ZFXr;Uv(<7ou`Bt?pT64z(`CcLWZ*)FAtX zg_s^th`GMSq#1~Z)dURnoN5LW3Wj>TB?|S?h4|kPQ3Yp$C$V{GV_jSM;}Gr2e*|wc zOI?13Kfh(XWC7L;KObwYF{tx9!l6a}kPxVIJ)LMlvqW1>__La9CecfhY(pE+OA7K+ zQ_lf^mLNB3f|!p7)L+LRYC=pieWc2N+}d!GFWjmn@`duOy0&Ux(Khf(eiw}u(6m|V zd=hjgr~?v!(khAW0``JoW>>E>Um&l!vzqxGkzk19>CCtx4b2qv7vkw=spte3HGncq zg8o2=iq%BSv=+vc2FZZog9YF)8PK1>g6Z!T2q_&ddZq28w@1rsz$f#UjQ4g9D4`M6G<*)~%NXmlWh0 z+-^Pzf^A4MpCpnE^Gm=344fpIwOMo-45Eb;Zb`N@U|O?G62^M_X)XqaCYWYOEfJ&} zrrjS066eI&)cINn=iNd$M~WY)Wh5aKA^oie&Ga+nry1pf8;JVQM#K}~!vY!>(p0Dz zqzb9&q|wSv^-=CmhcvVnt9@p~>j`GcWv9+ejdr`!VMF^)1sp-ys)fa0y`%{KL&(0K zs&#aeAQxnfgcU^GJ%oNhl#K&$S5mCaCqdvOJGScW7rF?|ihG`dJGxyF#0DOVykM9c z0}6Dux!wTXA*`6tz@oKDU2P9b`j7%7I-3;<-5=UdaHe_|qY8a2e@yyCSl5EQvP%Aw zGB_U%D`K=wR4&;J77erWaD5@RaUI`iS)Y(w>RDa%hg8Loz9n!j+DQDvA&r3acarpn z7>+>h6_q=|7(wO8un_5pKznNgj(W;4B@PWr&>A9gz0J+cm>5=wAq#9He>*7y?$jcX zIbmC;biS~z2c+l6gah@m;4L8?FhqAfPa$KvP31OyCSbH$?M-+Owp3|JnEoo>#Q+m{ z7m8;{J(NGjz!EBAv))X2M-^cmNUvBYb)#HIc|2RI(7%j#YQ3nFi2X8#b4Z;Qx-P~R z@ZLi1Ii%sI%r=|T)I{v6Q`helY!hfFVy_L@gQ^g}By279&BB&Qtr_k5QL|vz37%Rm z7+vsAG?yq|OH|Jq%g}sf4hS9$zKQmLcb7EIZ?`%eZaYGs4cO_!MSR9iidl3N_>2u^ zE5LLrU{>p$Iw772*cm7vT`nY8s3%(R9APL4QDunrfFg9BPctLIg7hy8jF1=&v45Q( z34KxMJ1%&snH4v}S{=4%U`IIxeL;1ZqA?&z5ZI$7C%VcWVHZ$F@rVYJ3?fd9AZyWQ zAu1K!)hrmmsRYqdCx!*YWl$}3W=Kfr0vHocF*j6*cZKG@RzzBQq?>EG!=c)|0MF>R zjdx=Qk7%paWAc)2NJ?%*M6BwLaRWlXHNa!)rKmbbxPxt$U?kRpbZtr?RL5*HK6L`4 z-Uf(az~z=BO&8##a|Q1t*ux_(L;#}m>z>s|p5S`K^I@XlH>*cL14!8fg8*_j)xol- z3lCHS-xc*p-6-`4zghV}zaQFUzG1U6)GPO{Ja8^egYoY zMc)0z`^7pf0OIckbMV&h=kE|aq5GL4senIJe)xX%Nb~(U()8fJ)FXxiVqQob(1pI! zRhll%mgYzcq$Sb{X|=RYx?S1~gWY!N zUTH7(>^wmGk0LQ}I&wyfx7y;EoTvZWdGb6D-}Aq{pQ0MW&t88l@+;wb%kSwkX0UyD z@1H(BxE0?I>hFuwr)!lnW@uAp%uww3o~*wwHs6iIUpV_KWzch>k{ix7AJ_l0xjcfR z`K6{!q1WWf=GWBb!^+U{WO_}RM6W60Sii;iH}Nn_7eBSDQ?#p<+DG`GtbIJ$_+6z0 zd*lDZzCR*8iM+h0klXAmGM>IBy)Ipp-o_r;A0XfU=h9!Kze?Xr|CIiXJVlKR>6wwJ z!ogh3gJ_&&csFOWTsS29SP8oU4&}XAKQ@pY42hmfe&J2D_5b=j(NoJh=r`4y{R?lR z*OvE&k3GL|FFpTj_Y>`gpKba5Yv&0+R2J3qTi-0D@( ziP6$(thE?w;jz~8lsIlV9xKJ@(?9g_Ch zKXE&5lE8^2@c4HO2c-VeU}+d)90SrsX|i;qG*h}+nkOximPsq6HPQxYlXRD~RoW@- zmL8BEk{*>FmyStKODB>0@CE55>37n3>4Nl@^p12z`bhdz`cnEDdu;w)`j_;R6l7Q> zU}k1#kyurUWk}Y+GFe;Jo^`}JOJ}6?=)wB1{%kNC1|fy{auN9H&)%UZj&L+bi*ICr zi2Q{&)5AWRZuH=u>=tk1b=Or~D`{~~#c?X$Mg)^&;@hPMl~7=bcwhhgf4$zKZi0hd z|2tH77|w9{#t>Wtj|X+rn_viE57*oDJ5;VK^sT;AmpiNZHG3BR=oS7JKk!kvv-#A! zvuD3MYt}pX51k8sGb?m|!{OPiboM*5*%AFgak@oWt$#Na#A8t8Y&=K*{I67OQw2Dj zJxd#nEAND#Zu%x5P!S#qZf9we=|8=arA`t5I)R5iUuqIckV}&woeZMhvWXOjb9qZDMxWXm5uD9*ay9-Y!t9nVC86VUvMw= z1>1-F2PxZ|%U7^u9eRYyH*>?F;NjpQWqbW_fFYh~SqI$HWGVcB@SKfghH*gr(fZE( zM!KJ)u@d&L+mH1Dv((Y=LI24}KWi|kM!3LZEy8NRZy=J)J}n%)JeZnbZjuVOexF(4 z>BF@Wc78Z}U5h|cj47;=U*vBEJMyC7>%nupJL-8%dRXb8w1pO)Otj*Zym0GGNsYE4 zt~2k#hhdK>n((;7K24OB0&(Ld(d5v9L=}{|A}xYFw)CxK%m27+*;^}?Us^hP%41Wf zJYF^B$dt*CPvKQ`{%u@Zy7bcWDUVOCJUj(graVqu#`>zeKjK2g}IxLDlGeoxo53G|5~ zib{6Sk-Q5Z?eI%`yQHM(bNpc2aWIYwTY?YvOtG(Xus4o&7Z!Hn389-b83$j`WAAh- z?39B8!Y7lCOPiE*xeosA7`KYW5n;sEg;;72#kyd$k!M=8l%6IPp-=g&TdvI4v{SX3 zHs`WjxA6U0r!Ag*i9hu^qN!yk zIn`4v&(H?8tfCh?q1V7pw=SH)?{bqo6m)4sE5I*+6rVhF-sL%1N|o!DymM2n7DRg) z+l~8%;65|5j}}u7Rd}0Zv)wh-Yv$iH`=(8-dd;a>?|11;h7)AAk(8s#I#8*L_?Hc- zDmY3;8XMtL(~^I|I)gCC);z1NvaUf{{Q8o`7Zxvm1D}hOb@eNGkHv2+#xYJVp$yqA z>qv)Gi?Ssoa3rupO_>>{6!YFl@@<$&FdP3zlt+LQr*(v`NUKPjlO~%w`OOh!X`RwKA?hCcS0J7OLT92LtD8G%qVs}}{4@!h2-^)| z7r`dNvPr(oh~_hhX5IPJ~%-oJM@D|qh;%MXt4kdo5AeM(9P_IA6Jl)Su@ly<>y25sLusQZ*> zo|(eSH0UU7TCk4I(surIg%z+pyRW>by@{A{Haew4_+jy_o!zu4{n=-MGljhcob47k zquAf}QkB1RhApa1k~hi=bV(iZQr(VtN89!@+Ot4=Hn2VGh6GHxS(G`k!ZF98n1*Im zWN}B9BMaH>^WLBo3lo8-H=FSkP7B(Erq)};TBzy z6j9KWQGqfAQk#i7oEJbjP;_wsq9KPmid3b4aRiL`+z4hw26d4_#6Y`oA*&50Oh}BH zjqIpM2vAHLbs_XKl-)+~A$e2Jo>RL%+y3-u!{y7715bCX%^1+VsOPe(=W}a^I4@te zmDjX+;g)&Hn_qf))8@*luP8TMF=V%|ZGU>+!tN_8aZ_B5({$xZWdEA>r>jHc_$o*QczPf1k6G$WM5FWxb4sFGFhVF@;QExkGGxV1gmi7@eTBFRJJ~dRg~wBfC@-w$D|DuDbbQ`@z201UVL0(UVayo$!J^{NNmv-Weqq)Hrf(OqIZ20TtXT&wUK`a zpq`k&ctlG$)0j{p~C#P zy0^5hn;EpazQH*^t9zJKr#y*vx05=d-Fn{?`>0C3L{nC9gT$dA@KnEA!mqmWn9C*SpM)LkWgpz#W2x9qgle}G39GV%-K;wIfNytG&ySU!v?k32kF+dOLY@Wu1Tjvj_ohKUn8 z6?e?*)U|k+c2S--{9!Sc+DUQBpOjajb0F7K25kj^Ea}K;4#|v>6m39ee2f$P3Z((T z5lL+rDkJ&1wEWc6eD>Wv+GiPYIU|>j%t^}9zTHz^zK2B*U`gA`o&%Tbkg}{V_7GWM zj%%~9ynJDs1QWYc8{1#bFV(uRSLxlg0b4Z^9_C6*m3x$GtZhil=W%BX6o`=w#zP~S z^c)(*lw}^*K?iVx6*=ee3raUQK(?_+QObIkva->4H;>^Nes3Bp?cKL;DeKm6Y+3KJ z-u}^L$ReY}ckfqhu`Iu79meg)ANM_IoX|O^+X9=dq_@9&zfM-`w2_rowom`1O*U7J zywPGQ=?xzAjWk$^QPx8wMFZMGkp@M^a%(#;O!bk2*FO|h|& z|T`>X?=L!!u@?l zI%BeD_U?cG!cL1Tx~Uy+UOhiOzIf@*L3v9T4_Y;N!9M2e(Izu%*{=PwM^9-Zn}!Tp zxbV@A!)LDAy|ldl=z* znt*OrE7W@hO7h>+w(kGpLJ!tpMlkz&x_iqnZyr$}7J1iPdMrZy2b^ z?c4KbS??k9Mi%D{n-j&K+8MlU(cYr=gSvKh@oUFX2gN9d>q!6awJ_v4{PmiG_wcOX z`_1*S7k6rlThymb-?`{MnsXI2c9P#jzC2p9o#8L>CPx_z*f%IHhVuj$cD|LcXOKBH z#p8@gjI!C|8N}jp~aS%)V{p^uQYvz*RD+tFQWHXUH$o*zS zzON`4G%JyE-H(7^2_8~h_FEmsWMiIjsIkKMy%GEDAVM74Ida00)sdW|Kw}7>qAcdQ zBBqEuBAOIav_fwxO&BQ?6^SAUp^&Ras{0V-u{$gFBEWtG{P*mO4LdflMvdW1JAJ8Cf z0XF(k^qhs6&bCbN(3H5l`7k7mM(Sv!D7;&7YNxdgAje18I_6g#Z2rf8pp4M+9fMiq zl?`tK#zWXMc?MwgNGVdezpdo)cw?9=F;SKyBFrv1Ej5NGB4Rr+QF1ttjm;rj4anC+ zSbT+`Y>EtPqB-CULyU4TQkJOHNQ$aQwg_TSI?xCnqAXD)?u(T>wQXAeyuz*peY>yO z%_+_BLR@y?kC-c1NTB!9bNP4GW=A>yfA z8>=h;UABdNpctCKc7J(EdWQ~PcT}N|VZ-j|=tM8FD$CFfJIY?UTUTFBc20SqFuzOw zuz)c=yGs{mTR+Q=Nr@j8NQvo;-4V(IZkMyd&hqSAuoaTsiNqwd`2(iFx(5_B86yot z4%un-t`Q9nSq*e51~G&8&@oUDj}%$hrpf2Z64~~mQVWnrUq%xBX1rvlN5DNmc6I07 zm{E=PU{fUqU96=1%&CulG*#yhUUrN1`3`J-)v(&T-pghfn+&^q__)jgvo<893V+8Fe0YNAvD2_lo3yY zCxFj|3I3m;tXQW9+u3-~{DxpR>aK0kI zbavo;6-n#RtKkSo9YXq~^$ybDaZK7f%~k4l2MuS>^18E|pGH{H0(A}L7OqbY{+{Z; zTbrRw1^yDS>s>}dtlJ%9Ga4=C7;|Eh2Z@-Zgap}Qvl)j4Y;HLc>4n0X$q*C!Dh1I~ zGVkUTmV$(kfRI!usvCb|K}2j_MFDL_)~(Bdl}|mk=)mncm7`YguBpN3S~F|vlWzu( z2ft+Z@h9fbDTHiatG=PF>b4fDS_nry1dfLL+ek)--EKBpA{-9cpePJs8$gs~H`)ym zR%Cos32` zJq~G-z`tRCEe}{onzpVk#D7GEzHn{mfR218ym31PXYuC_Y?8A!Z)$iy_z{}NrODVQ zrwF;(NH%rxBk&OlYlI;pE*@D+kfa<*xy_1hP1h{lOAO#_;6+B}vqIEt`S7^T?+ zU&HQY$aEM9Mn@-<#kGgEXS8i>J{$1z{$0tzPrq5b;Lm^8{>`lY#by#ZlOX0Ew`RDMhyN>3vh?_3U5JHCn6r z;svT2K6`5CKK1M5)kVK6nDauz8sxHE zbLjXjOp&KIY`?rS{fAzB_nPGddm~mk41Wgh(lF;t#<-Ew<1?^JRJu2LL?FTIjI_zJ zF~Vtb7~MiA(*cJl!>Gj{z$b=BsQ^O|BNw2{L*R1opfEm_U2h#LH0B+@q`h_glsPT^ z(n~LNh>gC@(`&-W0n3YKC6(s3D~{yP{x$uL-~iw_JN58Aa&$2Hl{l8k1=*&@@gs>VoiW`-P<5Cw@HgQbyJhbY(MG9$RKAn3GPV)n3x19>&-F-bP|#*%4A)ngyo4| z-rc}WTkr!>9=4RmFfe1M*kJ*NBvcNAJ_K5qN18pxJ?qK-qQ%xIU9{qLnhQnX#idcm zP5eIA!rfTw>+Da8krLo=twLnWDakJE@`7BvCTD_!JIdj}3^@)~rpI1{F)M}`bEaa% zW6MhHEC88}jjk}!L{?0oG8Uprc|Uks`%wGK)!(^qt=_VC&%VdbzA&czo`-h9G2j$S zV6lH;IVl_C-#`848)n|)r9WvO{_CZ;wSNYmH@-(O_P}nFv*g>cGG>&Joedt#UH=h-}%-hnNe|hcUYr!Az0exnGTr1T;u26aKfc67*)$l%u zyU-bL3S_}_9F$+XXVeO1!S?Mh@R;B?qz9dl|IKa>Jr{k-Xk-S1Tn#%-+=T+@e%L?U z45v;NPqW*#?PJ)@+PXbzm9~8}yLruacyDOmDHSX#^t|(=(P*d!AIrnu4fHID1IokA zq9|dY_Rt=sV%cOohP`&fb_gj@|54SGgA0R=?+jy<+A_@VON588I1Ww&CKXBZbl zL?rp`L~YoY*h*WEBY8a97oG)%$P7}*$FQRM*>>$SHa?O~HCTc{E_>K0?XXU7t;$0p z0aZqM(4io9T9=1W#fpS66XiV+sXfY)wzDL4qIQ^#@^CpwOReb3r{sg|PU8iEdt{YE zhD9Vo00lD{K1S%u3$$lRSegLJblFG!!A8vN^!|sJl|SY2aM2eX={>?=-XUvV4%&vvyP(%3_bQJ41bw;|vCaNpNVXx$AoAABKo=12rL0&W z6L%Fq9W3Lg`MW!IXh}#4f*10F@Bm8$?vNt#6byU7Jo;02j1pQC^3h_q>%^jAzGsxb z25;fpJ`Fu5m$!ZnnLK{=IqgQaS3DT^TVBqH9VodFqqU3SKJOk7-SB2P?8 zPH=c*MoiKSFn|btT-6te) zh)xOSB_WtGn{c}rfEFOf^4PfepFtY)Gf108mjDuQ{wj<=gYWjAL)Xw>eWR`r{eb+S zPas9^H0AZEEEU?4)2RHwEZRxXcCC7l4KRETp8;2`fn0+q3cM5uM4A;;PKK1MA|Fll zii_BQnq9kU9v(Mj@OZ=LTaP|@_uz?@14P+fEUe_&XH z>&PgL-*Br_&Wr1I@f+e@epvkWir?LE{d4gf79KQl9Op{5G6NE(9k_`MA4G{k@MGcs zH0yypAqcL5%h@X=PxTJbcjqA~0JNtn*$qB+`*RH2EI-SRQr;B6;A{L*U2W(K{K9e= zX;l8MLQ|4#r&y9BDZ}B+%*EFAKf4b&`a88Y^!)zt^UiEMk5;3cALKKQPs7h7o>%0s z9KJV)tja!VwMSZ=zs5u7fn(}ZaX-P$I|olm5vN#XI?GRI=>Sc@ z24lRkM%)Li(|zo1RA8yWj!n!Nf*(18ydLa3r=VAuh! zh!(I?BieXA1Z#62`+%#9;vP(7=^jpVYD*2LGDEO#&{ivB#XZ=)j(8R?lAcBRTL9BM zaXvb9zP(g~^CScmaUMHOi?)?%e-g0f0Rb!{iaGI3#M9iq6{w?3JmU$$F%tQcB2MxB zi7Y>nCE`v&w_%==rGlr3vcvdq*(UG+QI^gFevnUN?EcTjTbM^>so+rpA0!Bg3eh7W ze1OlfX@W<9PKkFgv>5FcW6F>Vd}B=E{J%-Y;=G^-aUS$Q=NZ=N!uS*Cb^QICgCm14 z;(p~@1MNXO!0#dRi3Led6&C4qx)jEuuu`i;_As+9AOJJ#j0Y5aa#oCalCLxmb0rc= zAqg9r(?P2svyzV$yRh>x+-g|*cl(|l&0gM>;5p;5be%e#&HP5mc4e)WUd zM|Vxr(g~i1ua&csVkm&#WjSuH7U`I&yfg(Fk~30$it<C85h54%4gj1wOd%2J}M)=pDEXYW#m|=NlC~~ zcI9G{3~lOF+N(gQRn3cd)a^pgq67U;d_m{LAFdFo&<)`~aS%4ei+jRX^zWt{NnOXn z7&e70!d7x1F8ao10#HDq#zTc~abAVUcKb5Z!U@R5=AJObVoy)({pm{~3qukuaL}&; zPv{$S=FkFdE00K%RCSg03ciG0XnOI|s;c_6bfm1(UbtHIQ?IL4`Z4QOcc7|DPNHjc zZ`D;*npokUZOh3oXgBE(Roa%lTb{a`B@G@jb=m`g(J3iU=G}N>VYinwzjkHkwr#t1 zY?~LDP&sDYgoy!V)zzx1s)khvLHwYquC(f^5(fZqz&>ET8dhO@s$N$=uUHZ_uy?Nt z_Sv}LYin=)`%TN_+*f)}i?q&W={-AZ+82EWXz#OL+PDi>-Z_8%og=&V9Dd@#dx=(8 zYeQIB<38}s4*pmqvc^iS%}R;%kz@n853!z^BFmT~O@yCAtC=61C}||n;v_3;*Je@s zj$>bbY0T`r{Lvlm$N2kFd5XWRUIX=J`jKoA8wqlYkxQ5z-;I@g?2|?*Bn#00VmS}J znIijW)yc=%NbP>>Bet_=ZI4)$r*R&UDI56o}C4u#Ut%kDZ#Tq1JUNu=QCgZk1 zxm+pp{qj>-dj;gBZ?w!sFBDiX3jt^!06J;x%c^J0qP(hckD<)9%5!X>Aq8cY_|pv7 zOTlciSj~oQfeLw!%%7CcqRetLbC^rbtIfz@jFqJet&4ZjQjx)pRnN7d(lbM)6U?*J zIv!=XhH;$b_b|m^e2_oM&tl2AIvFFm6wwUe(|MQ((zZJy_5(%+kDKk+{&4Y%T6ZP! zGw^4WQ>NCjYyX>aT<{rkt7TU%YJX7cqCZQ#qT^5kJi81U+FR&PG@GFKQsP64{GjPc zB$a$|@N24=nuxtdQ+kBnt*E5p0@nOh?&#*US(2S2MnO!w?a@d2{qNz}Q@N zzCT`qQ3J&Tn-9uQ0*TeFN)ERd1^_Ss8qC>fHZ3c$+Bd$L@fH7Irds#Cd-G;D=2Yzg z^EktCF@MbfHDMV+kzrp_6?H3ZK!OC~O=>zA1WLm!7`(vEP8|ro{is@Z8ZXe~*C?mf zXu!HDvQ9?3-LY^(@J8G1>XraDR)5O=qWzLxcG#2cd3Nj`WS9I>)D~>&hpg9vTrW_@ zQ?aRhlY~Q6X&u1MX(+(hnuK?W4@-gH=8PQDFS(@HJl>i=thC+8A$^jnA}2&vsdbs{ zGrM=5HnSVPm+V+deZr*_wN7>z{tgXow?+SNuh@Pa6_c`$-i z`jYSyluZkp8%{~`QQAFxv57z-}rlDE*3OkTZ0W;_&CD`NTy2|iQzg4KIBd>>dvnEa?1K9 zkDK+kVdLla$v3P?PV-Jp+Ihn$Yr#?(aS{>>Myltvb3ZRj>{QXZEG_=G3uD;+=)7kB zAC(oH@|$I0KJYbgSgXE(G#~p2CqMiB5w0&$|BUN<1kNP<-le{c-y5l2jSlU6Xy=a}{ckw_?|m5V18nq)84z-%hx#P^`)Ba}xAy!muQxPm=YuC2 z7dee)U+@}{bYo+~W}GVxox4>(SN~V-e8YqKxrPrz_g%z&>q7Uv7`pEu?i&}n4?^&l za2~u_JFlhc_h^&!d$jeyb0_^Auq~d`{(y6HK9U4fM;E~VUFg1C{oMauAAom*l2}(I z3p7Nrols#_?ttM5@J;g4uPvGv4oYvUHI<*Xb0iWOke~__!=4aTro>e1u91@He z_PmOD^Yi9o%Xlm~!sZM(AQ;mT6s<$k{WGn8?e_$ZG1qp)@~#jT0+x$_#fLVU;LYfi z#`}{^ve99c@#mb48Mo26USg1Y{3+>@^a(K01dhZ$k+6{Qpv3YaM9QVPr1y}R4}(*X z2Y?6~AeC~W03!IdKmvwQbl=`fFTb_--rIGsEXR)6kTOGu?0OuH@W)#rx{*cLG8=yP z>s9bqz@%PMKT&112_}_&qFz(^gDL{X{A#&6SFJ{->lQEpB7q=b21f1@Y{4GF(2TWO z{bzqWz#In*t1L%s+DZWk)=SjqnJYvC+5T9?0e?6Cq_GjZ0TUTu0j8YLfQHX2AQG4d z+*6s9QF_dQ*F@hxfBN*b)mVEur@qWeMOpFC#4b+>a0Je3xC{~mjOse4PHu3@djY+}1e16OF%v2uH zk>2MVgw(R}t;$0(@e=0J;Y9OCuoE9{{FXn;Hq6#$XnFEXDz|SFPN5a+eFS_eMq{{- zh~>!|T9%6v?lJB~f^=|=e>wtSj4D|kQRD??@SxvIV%F4`{E!~Z93=W{n|lg6Qw zKByvkEslxe0&_-1?}wSdv6wxTFu zj`CeMLIqvvm=zUFW*vYA?P>nSz~Bj_7fB(whc?kxmOnCF2T_)(#n2Be4Tw1hxtukV z)*)@_LRQ608~I+ouPpd5$9^HgZ$jrMEy^S+_>D5D%dpr@^g_0R6;A^UupfFHw0J|2KVqTI+VJ-uJN$A1Q%?hL7ZQ zl$qJoFZuqMFi&pk`Id;(`@YqnLgIX|6bLq8^PE<=ijAeg|6p)~(x#~4D?aVGj)$Ms zLF4s*SBKtXC$wHG*aDUSyl77=Wd#i@Srnfqa199);$1Ch;eSqFJGH6H*g9t0$cD*U zLBm-25O9;%gm1)pV8aW^Yym4UP}j9)tu2V^v{ts79sAQp)=!a38lvA-&NbnnH7yVi z{B2q&YNCa1|A)HofRC!^;=MC->n5Z?0wF+Hid0G2C4^8UAq9}$Yk&{}38axi6$mz> zh}gSg?}`N*q9~w(qI5-(B1J$^KoDDU^Zqk;HrW9Bso(d0zxU9boxOYSX>(@Ixl_(8 zfGwO=CpeWR%Ue1kOe^I_!|tz-J3saD!2xI(TJb6h4Aji`%h+H|^L(WGqRpU}crdw81fzjaXhe zx7{xP4Q^kzJ$Nx0L@uwOE2?H`!#n+PnC6q0FNK@i*WcrtV2^|25eF|KrNpQpUF}UA z_k!QqO`Pvo$v4mR5~N{d-s!)Q@dRrAR3r!CE3Wu1B~%wQ!rrhu+Jlym+rO{ShDZNl zsJ{u_VqQ(u|KIOZcqfp&gj~VV)Yg%&9~I|`p+~{|F5i+!aJolLmx4Cx@Fe=swxk7@ zaEtv~CL}zk{${_KNea1a18V9d*6H^#=Z7;hD1SphCt8Ej7spDxHMQQ>a$L0ihW-9{ zaz8hz+eo~(0C5p6LSF}aJke^fUu&NnF0h|qFL2zIJ6R{-#(S!4W9_h(O;IV2 z+W0vt1m{0UbxYmUYTOU)a{KI1pzTtj-f#>$sqD!%_;gZh3%u+OBHWFF9fzlUr1tLXz&co)QhHKyS6E<&FMAH(P7UTqhV5u|&Cy78L| zf_XhZTo&KU<@3+51kJ#I(4@d9-)HzsmR!p&@cPB7_aiLl{-5TO`CyrjsEgaLU%52q zKg0Wwy%Sr~Ln=r-eX*kICk9vOFF*Qxr+;Ruj{mSut`5HrR?aJDRYrj?*c-`1XDZm? zimG0`2NZL4we`*0mddc)S;y%i{yOFFywZ}SmHNW!x9ptBSObt*q0X(^O&*zI@1>(S z-r6?MB`H$$t6R>_~^TmKG!ykSkvAiXN+RPsQ!{XXURYW+UiJ=!N~%Vqr|JLppeo<~%u%_@(w z%=D^z8J1rVC9$Tzm3F`_%oX57LjS}Le7<(rY^$YuvNS!yfa)Yx4Npu@Alg(-(Qw?H~POuhsU1Ipn?r=pYDE*te0EI=sCnwK{~PCJ*%E&d~~ojmN7B@U*FeH=m_SpH&cdR z;8E-nsPV%hQuc)!3rS~R{AI;f&ho`yzIQv`2a8zTsW*!4;Cw-AUEIDtsKy)DMB;DS zgBFq=z8K7ks~mq(3?}h&%jW+6@8b9MKmKu8K&Q%%7sX*0;CI-^UXY^$yaw;exG@Y|j29fBshrQY!4zN%zP{`q$7A2v&lQq6JRYms(4Wrt$7Lxp z9&=VR|MPfkhrOoI7mr2Rfk1zWc&GEMhv}@JGh}wUQ%T5)N@(Qe1=rNGjqQCqi*8f}a9b&SonS)_~o> za&nAN_^AnZ4!0!EdlU91ZLj4%YTOqGhuhA##sS$Jjzq1zzBeOd#tkA6HvDc%|<^>?+1^ zuzDX|JzwHl6Z~K|#6>Z2Ta1hFF5~?Sgzapz{eCui;JjlW5C6lso%!PSW7yd`iX>L3 zIdsmHcF9k!hrT1-sX-Al(R} z>p#W)&)*3i#PD(!UfW4)GN;1+fZgklTee|7w(@skcNqddZEq$w5FK%9g{`XNpa%rn z|1NgdoFVb|?CFz`9C=oS+PW%~ESqSvOrZnJCCXUpe-yJ7O~kUvoC;Q6^$xjaqWvqK ze}?O0?1gk3-~7*GL!XZ>9*-cSPX*0G*MIdidpGe=uBTdg{^dKuJ@zeAkh#4hA~upY z!F~@B8|UAre>uKEop)NK#N{tyKF{pyjm5wg@AKMDqVp^0dlRc3Qa^BPZbD70_OD|3 z(hL$)0l}AL+dJ4Y@YS`(F8pFOL{oT4u8BwZgjY@6fH9NWSnXrGS0VPLXH>8al|AW~ z{q4qTs>5eOO?~;6@1NHUw^r+*oS2|Y53>xA@RP&DI6}>M8iv#KzZy^Ti`<^Kk2gdkx;a$|{+87A6?pGN{HMA2$g2Nvn%_GCCb1AY@He^x_S18a z<8lB7=RT>L)RVqN@N&WB_2ILy*F=n`Ynbo;v*^vwFAIAUE%$`|@@#A#?}6a0s-!2~ z!-F^fafJUt?6$*xaW;98+r(IMQ`M|KtdBi{_7nUk`q_4b^M22FSN?f~=>JCNZxb)s z+Z-CN(I2Z;gG7~^iA6?4=~kn2^!FHJMI1merhQ@5RzJ~~o%Die{%Lzp8KQX%g4|xU zF`f0Tyc36CJ36Wxx8Xo&KHu`UFzQ3E~9zhdx_uJ zyHOL(KeT_M&^aCO+cYw_YNxu;!CgC6DC7CRZx_xGfQaYm9U`8m&UnznHFkk@1L!yK zKC0r#YQOgmgaE$So7-<_O+>%f9)KJWJuw$og*P-FN}&#Y`2I~yFEER~L(t zW@M&_*?`~v9jmpRnC-uop$?5+q#q(?yd{9fUJRYyCiH~JL-ch9AA{8pcsXm^FBQ*= zkv1{fV8_|hCt^@5pn}ZKwVx4T8*~C(wf>oJv=;MSfAwy_{WCCi7kCc(#&K)sVlf}j z{WEw%f1o-O-u}MX7qNYB=Px0z@QCdXBU&2{Ub;xRYWqhy=V|ccA8L^CSAq-1J1(mo znflGXX%TsuJDr<;|6@TXZMeOOAUNf1pccRM`HIFzSTBv;&*%GvIbYk)E*9A7mE?y% zJ_h=#-RNlu*4g`F&3}a3-xF`JA6taQo_#C6Vy6GAscjS_u5PtQW5pK^d{ zV`6!4d;#hSNnldQbEQb0;|gIOZSTPZng76|Fw}^=C*um9uSe&seNP{@*6LyTlWrH+M?>fAbO&*PA8e?Fx>vFFnks)WN53Y}{1-Q-4qshks)a8%J&-V=M65`+z-T8krA? zIj(^h+5oRBr_cUV8xUB<2+r5`z~SURZtuBBot85b2Sz+`vJT$bd2?s%81{}Wfbd!# zxxu&2s4Q$%*g)07N%T+!Ev~wMBHco-1fN(=D{a*`c30DHscH?rzp$z;_FGfPERu&v z%U+#HO61s`PyRaZ<<4{_Rul`wg3}t0cv0`aeuB^d!iZg&3Yz<70W!ARIs&`sTIDs#D~vP+B@&(eBGE+?{zTxf>t?fF0eX-cN}d9pId2D z`BjD5Ud{>D;_WEc()eOPc$kYY`wa{F1=xq|6-X3SQbALz)>hE7RQHp=hF`vZ{%`W@ zbSpe;_hRup>0p0eK_)<}s3$u?eV1&;TEYOaSB1ZaTII+Tiv*S)55gd9%EM-gR`QQ* z9@j2rcZ~QOr!Co#jemSu6VLag?GgJI_+FE?NfA^tkC^AMk-{j_$PjFgIIS@xvl4qG zu+0D)H2F>qu`_}TUe0$#Am0x*7C74?Uf}x}{Bd5E)QIUv^mBMmwu%&NhA78&2qw>7 z$`B}>y&u?(Vju}v8@?0ec6wRWUa=FUt^dA=Mu>P)k~%eL5)q=}nCb|31l<;KFyeFs z+ZsV8M-)ZS(-CB4#Ht9I6fq>CJc31FSRuwEww%nGRm`XN@lFT+TV85^1mpWp;urtP z{)l@H9lKiF(5B7?36c|djSKsAYWHmz$o@S0wZI6oNs}!d+rci*tEV3QuuPHEK| zdk?DQ*Spkj*jHmAhTBLoh8UBLazo!{95hZFY^y;g8$|}?*^}vMgRC@G8I<3IEKNvf z#dpu(U$OVojut63ay{vJAd`2m4vD|Cz;O{|<34T^ftswy*0~-(Y_cMw&OX zzk@xzrhP^Fi+wHil;*E1mKH#hOsL^CE3_}R+akHZzK)Tf@l|;c1Gta{N^XQrUAnZb z+zP8ddV{^n*$s;><$}Fe-lSI4Woa7n?{Re&8ICq}uDBwoj;mJZ>YL@oYdwFnM+1Sf zT5yH&nYxaT*=HW3J-Hzp9r@NCd+B1{&Uh|l@&a;C1t{>}0mQNp4VZ!Ti~uD7nJv+R ztZ<6lu{=Ru%?@1b=EZa!H#9zdhfUEuy;5ZU>f++n`B-GP-Hy4=Zc2{ueQe4o)ofu3D)0 z5_M(>orW4z;Ko?Io3faMUI~K})+MOr38xcig4(sKz}a2tp7Qr_3Vr^MaSC1jcQ~uD zG@k0hUT4By+t6i?3Tv&t2iDp(sV$?;qhg{4N3nHLBqXXl>fxyOqE1I?QN-m6ZG)7k zfl6RKRr8svP^z%k>fONG`XaoKRzLq=;(Z5r$whcytiJuf#LM~hUW9j5_1pg?UasGO z8oX_dN2(tr6M3B}-&TLgZHv&D$X=0yBiBW$<&mc&X(VgXB(x1-G)jqTbAf6ddlhvS z?uYZy{}E1qK41JF;q>Qo)qjsuxvu&ovxV$)WdFKD_OF|2YN|s_hru1#x(+0yLwSdX zJG|H7bO)^iY1mL$Y=jcgSg&`X#fpC50^H~az7}=0OaTqv8VGIPCZ^5cHf&uR64Iu; z&BJZpYje7d)`qo1=Z)6TUTNRqZ{mJY0l%<={~NehDX0Ih;eJHf;n**2T4TR$dO;`a z+NkAF25rN5e+&B+{cUg^Ma%y7&-A?Me~9x%<=y`|pR15TM6}(1&u8^k#2=Lga#}Ib z-zqa6eLDnELiNMSlc9#IQ}q~F+AB$Ss2wA^N6d^^5TOP{G>PC}#z!oRxIe<^j%)Wv zuw4=4Tm*?=D{(+$^RSq(!m!0*E5p`>Z4EmWW(MOzudqpBEAX{C%-}`-`--qtxZ?os zs19?5QR159LR!QSm)jNVN^)tukmMTTn(QiaZF3!TopxF1r~kQa0Tc{rUfUk_9WlfxbTWPb5R`3f_miE~+#1;C^-XNYXWGVavrW zSF~K$QtgG0l`UCt%U&%9!Gr;!4VH4l#oFAQHCzA}7W_}1`K;byP!75Kt} zalMzgu7yM8lQ8)m6cXePiVecpKQAN&4GEeYR1~x==wQ(4AS;O14U%U!1OIiZh=-2; zl~h|rh;{x_7N?H>Kad3Ru+xX|cI5K|pD0g;v{hy(pH{yes=GR2%S7pmmeh{LA>(%Ua*x+USmJ_qS%dT9b1~Vu-g)w4;cah{A})5i29sMQn{Y6=4SB zLa&HP5i9VuI>O*Z{`(5Nk*7)`B66KDHjY^8rcp)ijNYv!0qNr_A z2N5k-i7e}v%UKg$Q!pF*J798CxdN8i7#5H|eG&vnjqFM76F)a#PEN-!~#kv+-TbycP z_G+;LUsy1%_Y&8&7L-hi#7kdDq&qS;GAUByg`~(Kk&`2fBDX~zM8IW5I<*t2L^i%u z?f9C{#dQPzKdk1x_a+)3_>We;`G1;!zP37q_hWp;e5h_UcXvl_^TxE^%6hBnQ3bi8 zPS<1gv^LZlYXxcscil=yoJ<>!_hx=RPlk4<(voLvx7-o+>xVUHfIMogTQ#jWX>3!q zaf5KC;4BKI0R{oZkvupRfv1?~Lu=x-ndg;4wmOkSWCY@4@EebZ3;B1y+tqV2LrUwLIo!RNoxk}uBWypmmcotjE| z-j})LW_zoBxT@5C-`)%LOJ3a=OKZ%}uV@q=t^|dK2G&>VBU5#g$nc>05e<;T3&$ql zFeZmpK2+hb%GbC^ZpDeZZE+Uk-=xQCX}EjV+Q4r?u2^L$oF&Q(8D z93K!wnh}3(hK7eI^taXa?>D&CxiTKPao1=58a1DVM(t0+<{f@y@q{0UFI~(3#xBlJt*DWhw za^tm2k^5wakhBKxO_k)N%M2WcR}Tk=xgbC&1PCQ)^(bGA)lobbg&#g3 z((J^=UWSZZ;uudDuWqz;j>AE!F67T_5pM5mJ%8$iwDg|chMU(1JTUK?TSp9?KQf%$ zy)Y&Hs`RAcmv`yfCBE61nMG66W_KBy*1AC0s=W=mjRk+Llu1e5T7(3L1Roh2!cPhd z32DlLBe+ju!7L)8Y14*av?-Ww%B-Vf`KTC1o$DwcCq()5FPH}RJXc0Xv_b{?acV?1 zz|I{>0#AM2nP(i6f*9cqN$h>MUGrGb?e%Z&J8Vs_An)jbH(eKa6QsHJ_7^`Q{in~* zpBPz`)P2Uxn7HgI!Q%^xCN6D3k;E3cZlLF6)R_y_lZ^Ab)L`}KSW^qogSjxl!8Ar+ zsnd1Hcf|K^(-2ApK*AfR4mqP3#uH<1&fq6MCjf(OEEwtVz1vFdX=M45Stnb+S}&rK zG+nuJ1UYMWm_=5_A3y^q_EzXIj*Z8N^3)_(M3V*$nlynXoAAL|4;aAsEemUObS#gB zSB0@)gu^^kR_n1ap-z1=2DJ4*Q77`_L4y%oliV1%v2kU(+a`#7a?a@;Mym!G?ly!j zN!%05?CQAN!vURU2m<}bW`b42&};eAquwTmKmm%ZGy z)l#m*FO*!=?+moEmP()3!Xl514Hcu#Eg?V{__TtKj13HwP0xKqG`+zL=`8CsK7JI9 z!){{TcG$?^b{j4@^gj=sw1425yB>V%j*SURn@$`wzR~2qvv0jMeP~MGe(7n0Na36n zYhV57lY1KvSg^2VT=|0jDOaYZ4H|?tgSK@Q&PteoI*&-T)aTS6Zhmb+EOL z=h{?5Fni4b#+NZW;G`Oa`gDrJckA<{Nu<@vg?E%cVW-h_TKSy4=Y_jSt2YbLI2iQd zw2j8lM<*pMsa->>MVl6Fj*M-=ZK_2JWKwE+WGoV1w}%coI{LV%E2^nF=%l6<@T1V; zqn$I6#bGQsDVw1!h_ki)E|EShufc z>x-%tohxz$B9*<*6vYXg2~JvjA%?x@fotwqdCz0n6-}2059;Wv-U0oc>ZMcXtay9% zrjLgYY}~%>TwcKohb0Y2PrLF;@cf4MIt|4*zlPJWnj{65qnk`(%nB+uo%FvRes)O) zYE#U#G?HmFafDjeFAq&FMJmnxIS033+UF(Ls<3CC3B!9Ntvac_PE!1EVgeOANrCmk~sBwu9pJsl@h7YFL)*EfHwpq{Fs@ffqYgY>2weY3JPmn-k_n# zR}6+TBDY|c13ZhK58jFE53>dyBwQ3(K*9cq?%sA`-BRvJ>C&@*sE_2vRv}4YlP~Yu zXwWWyy6Zug=lJ617RDqsZJ-7RHV>qMykY}GFkKa~qUp+}tD3T=?#5sNSuQcj!zX+A z;T)Hqw+coQV16`mt3R8%dgt!@w{O^Xzn)un_Npi)aQO-(&ZhQ9;3-k)xPg+I)NZA+ z3LdkbK7=8oEV>KdYkoOi^L6J%W20rLyihm() zE6EySO}5IdZI-^xI%u7?SQ4_mNj@y_@#zJ82%~6FhmQ$Y?>pqK!-syi>$X69Sq&q? zK72Z<2M;kOsZB`J@JOXe6D2$;D57t;;c~gvBz1^7SuIz$srokcpn4i-#DJ4mo4Nan zPiTgJuC5o_7hy=u*Bv`zAjDHYaZ5*>9#bC^kuWM&hcn)x_)>6oB=H_nhs!K!WLNfU z)n9wv$1DjFW)|By0s8aVRsi~p{z=W7g#`xUm=-L9A}LhZkrq|%pC%sBV;Bz{RM*OTD(T;hCv3twGoc%oQZYlbU;r36w;X1sLgL)YSlcNX%BkCNi28aJ{0&4d<^X6V>OxvhY{W@SM!*%g-*m_uiYIBK>^1nWqL;oBUe;{QJ59^#zrKv8j8Js9B}&l*O8|IXE~wm zi=&u?x8oD=;pC3ZThsI|+b-MH0(D;Z83ijC>pmm$FdLtwZQZPQX|d}vzODx2hco3F zHR7C&kT&X>w-qF>K1ZHakjeF;-*x`G82hPn&Ig`2O_nfz#?FP;kCS^?M_eC*^DA&> zL;W@u@Z$ax^d5od!s|cM>*RHu-{N=w4-|_jfyZ|)i0@3D-dO(nAJ219bi0kcnB-Rf zhnvPMrBcq2mDWa3NZDL}tCKg^xTD0hOMccMl1P5Qi*cE86$a~8F58(&1@j(wb zg~Zr6%`Zqx@GBdB<;(oa;eKTUuljbIXm`u#=$7v2qrO6MCwHq(om#m&QIGFZwA&pU z)1u|&n+hhoqiZkX!Byhn*w|AJ68=3FH%CX4Li<)lwd+I9NXW@}jEB`OEs}!jH82`C zZXRVAQH)|1QkHTq=hN7>H_5 z03TDqhk?pBdyPKL%rBpv+AL_Q~S>{Nh*sj7KWgU^LU?!6D{5NatwUHi(6X z@iCYys6`q!YS<_WClT_aFrsOE=PPIeiO0BBqj0=Vu*q?010wrfKdncreuV>v7Ns?} zi%HDLFmKy-aY+kSmlUsF*t_52XC~uFyvE0-XAB+n2>D{HbT8zn_AEzN8}y3M&d@>;AO$3KjZgkzmDyD3mnEDMLX zE3K&~yRQ~~=4I7q-z;@8mvW&nmMVbwW^Js5t)8Kk0|C}6kh)wt3_ zl*i)E*2*{mCva#S#r5_|aCI}lPysm)F~GALkmHF_IGz}#n}8foj52}$if30SUfj7C zFxdVTFjTVKGKS5ATP(pwqLC{T5nt^(P5+xwVkf3BH&Fsgb1Am^=z;H?L+@q4(`J-{0;4;R0V5aluAsj-0llzyPn z&p3+F%aMZHk%G^WxH14c=7aGR2Gan?iC@QwUnhv`6Zm!3o%OHY4LD4|QR2#I0moE- zfa@8eoL#*a(f5wL&ta+C*&@-R^zBjEVzhj4wexSm