Skip to main content

max / audiofiles

deps: eframe/egui 0.31→0.34, cpal 0.15→0.17, midir 0.10→0.11, tray-icon 0.21→0.22 Foundation bump + downstream sweep. windows-core duplicates eliminated (4→0 on the windows target). objc2-foundation still 2× because winit 0.30 hasn't bumped objc2 yet; that floor is upstream. egui 0.34 deprecation sweep (~100 sites): - App::update → App::ui (new trait shape; takes &mut Ui, not &Context) - SidePanel/TopBottomPanel type aliases → Panel::left/right/top/bottom - Panel::show(ctx, ...) → show_inside(ui, ...) across 26 sites - Memory::toggle_popup/close_popup → Popup::toggle_id/close_id - popup_below_widget → Popup::from_response builder - show_tooltip_at_pointer → Tooltip::always_open - Context::style/set_style → global_style/set_global_style - Context::screen_rect → content_rect - Ui::close_menu → Ui::close Required signature changes on every leaf draw_* function in the browser crate (configure/progress/tagging/export/activation/ vault_setup/db_error) from `ctx: &egui::Context` to `ui: &mut egui::Ui`. draw_browser, draw_normal_browser, and update_browser propagated. cpal 0.17: DeviceTrait::name() → description().name(); SampleRate return is now a primitive u32 (drop the .0). Other small changes that landed in files touched by the egui sweep: - core/Cargo.toml symphonia features +aac +alac +isomp4 +caf (pairs with util.rs AUDIO_EXTENSIONS for m4a/alac/caf/bwf support) - app/main.rs synckit.toml parser swapped from hand-rolled line walk to `toml::from_str::<HashMap<String, String>>()`; added toml dep - ui/export_screens.rs: yellow heads-up under the Format radio when format != Original, naming BWF/iXML/loop/cue/ID3 as the metadata the re-encode strips - app/activation.rs: "Activating..." → "Activating…" (U+2026) - ui/toolbar.rs: bold the Import label when contents+search+filters are all empty, so first-launch users see the one action that does anything Tests: 799 passing. 0 deprecation warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-03 02:12 UTC
Commit: 2b7364128f3e711537bd7fa824ff33ccd78f6b84
Parent: f605a45
22 files changed, +350 insertions, -344 deletions
M Cargo.lock +100 -137
@@ -3,22 +3,15 @@
3 3 version = 4
4 4
5 5 [[package]]
6 - name = "ab_glyph"
7 - version = "0.2.32"
6 + name = "accesskit"
7 + version = "0.24.0"
8 8 source = "registry+https://github.com/rust-lang/crates.io-index"
9 - checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
9 + checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a"
10 10 dependencies = [
11 - "ab_glyph_rasterizer",
12 - "owned_ttf_parser",
11 + "uuid",
13 12 ]
14 13
15 14 [[package]]
16 - name = "ab_glyph_rasterizer"
17 - version = "0.1.10"
18 - source = "registry+https://github.com/rust-lang/crates.io-index"
19 - checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
20 -
21 - [[package]]
22 15 name = "adler2"
23 16 version = "2.0.1"
24 17 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -59,21 +52,21 @@ dependencies = [
59 52
60 53 [[package]]
61 54 name = "alsa"
62 - version = "0.9.1"
55 + version = "0.11.0"
63 56 source = "registry+https://github.com/rust-lang/crates.io-index"
64 - checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
57 + checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
65 58 dependencies = [
66 59 "alsa-sys",
67 - "bitflags 2.11.0",
60 + "bitflags 2.12.1",
68 61 "cfg-if",
69 62 "libc",
70 63 ]
71 64
72 65 [[package]]
73 66 name = "alsa-sys"
74 - version = "0.3.1"
67 + version = "0.4.0"
75 68 source = "registry+https://github.com/rust-lang/crates.io-index"
76 - checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
69 + checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
77 70 dependencies = [
78 71 "libc",
79 72 "pkg-config",
@@ -81,23 +74,30 @@ dependencies = [
81 74
82 75 [[package]]
83 76 name = "android-activity"
84 - version = "0.6.0"
77 + version = "0.6.1"
85 78 source = "registry+https://github.com/rust-lang/crates.io-index"
86 - checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
79 + checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
87 80 dependencies = [
88 81 "android-properties",
89 - "bitflags 2.11.0",
82 + "bitflags 2.12.1",
90 83 "cc",
91 - "cesu8",
92 - "jni",
93 - "jni-sys",
84 + "jni 0.22.4",
94 85 "libc",
95 86 "log",
96 - "ndk 0.9.0",
87 + "ndk",
97 88 "ndk-context",
98 - "ndk-sys 0.6.0+11769913",
89 + "ndk-sys",
99 90 "num_enum",
100 - "thiserror 1.0.69",
91 + "thiserror 2.0.18",
92 + ]
93 +
94 + [[package]]
95 + name = "android-build"
96 + version = "0.1.4"
97 + source = "registry+https://github.com/rust-lang/crates.io-index"
98 + checksum = "f9fc9904ad2ad097c3c1cfe2eacaaf0fc24710936fa9ed941cb310b7c6ed2ab7"
99 + dependencies = [
100 + "windows-sys 0.52.0",
101 101 ]
102 102
103 103 [[package]]
@@ -117,9 +117,9 @@ dependencies = [
117 117
118 118 [[package]]
119 119 name = "anyhow"
120 - version = "1.0.101"
120 + version = "1.0.102"
121 121 source = "registry+https://github.com/rust-lang/crates.io-index"
122 - checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
122 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
123 123
124 124 [[package]]
125 125 name = "arboard"
@@ -130,7 +130,7 @@ dependencies = [
130 130 "clipboard-win",
131 131 "image",
132 132 "log",
133 - "objc2 0.6.3",
133 + "objc2 0.6.4",
134 134 "objc2-app-kit 0.3.2",
135 135 "objc2-core-foundation",
136 136 "objc2-core-graphics",
@@ -166,15 +166,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
166 166 checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
167 167
168 168 [[package]]
169 - name = "ash"
170 - version = "0.38.0+1.3.281"
171 - source = "registry+https://github.com/rust-lang/crates.io-index"
172 - checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
173 - dependencies = [
174 - "libloading 0.8.9",
175 - ]
176 -
177 - [[package]]
178 169 name = "ashpd"
179 170 version = "0.11.1"
180 171 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -258,7 +249,7 @@ dependencies = [
258 249 "futures-lite",
259 250 "parking",
260 251 "polling",
261 - "rustix 1.1.3",
252 + "rustix 1.1.4",
262 253 "slab",
263 254 "windows-sys 0.61.2",
264 255 ]
@@ -300,7 +291,7 @@ dependencies = [
300 291 "cfg-if",
301 292 "event-listener",
302 293 "futures-lite",
303 - "rustix 1.1.3",
294 + "rustix 1.1.4",
304 295 ]
305 296
306 297 [[package]]
@@ -311,14 +302,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
311 302 dependencies = [
312 303 "proc-macro2",
313 304 "quote",
314 - "syn 2.0.116",
305 + "syn 2.0.117",
315 306 ]
316 307
317 308 [[package]]
318 309 name = "async-signal"
319 - version = "0.2.13"
310 + version = "0.2.14"
320 311 source = "registry+https://github.com/rust-lang/crates.io-index"
321 - checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
312 + checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
322 313 dependencies = [
323 314 "async-io",
324 315 "async-lock",
@@ -326,7 +317,7 @@ dependencies = [
326 317 "cfg-if",
327 318 "futures-core",
328 319 "futures-io",
329 - "rustix 1.1.3",
320 + "rustix 1.1.4",
330 321 "signal-hook-registry",
331 322 "slab",
332 323 "windows-sys 0.61.2",
@@ -346,7 +337,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
346 337 dependencies = [
347 338 "proc-macro2",
348 339 "quote",
349 - "syn 2.0.116",
340 + "syn 2.0.117",
350 341 ]
351 342
352 343 [[package]]
@@ -401,6 +392,7 @@ dependencies = [
401 392 "tempfile",
402 393 "thiserror 2.0.18",
403 394 "tokio",
395 + "toml",
404 396 "tracing",
405 397 "tracing-subscriber",
406 398 "tray-icon",
@@ -430,7 +422,7 @@ dependencies = [
430 422 "egui_extras",
431 423 "hound",
432 424 "libc",
433 - "objc2 0.6.3",
425 + "objc2 0.6.4",
434 426 "objc2-app-kit 0.3.2",
435 427 "objc2-foundation 0.3.2",
436 428 "parking_lot",
@@ -444,8 +436,8 @@ dependencies = [
444 436 "thiserror 2.0.18",
445 437 "toml",
446 438 "tracing",
447 - "windows 0.62.2",
448 - "windows-core 0.62.2",
439 + "windows",
440 + "windows-core",
449 441 ]
450 442
451 443 [[package]]
@@ -519,9 +511,9 @@ dependencies = [
519 511
520 512 [[package]]
521 513 name = "autocfg"
522 - version = "1.5.0"
514 + version = "1.5.1"
523 515 source = "registry+https://github.com/rust-lang/crates.io-index"
524 - checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
516 + checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
525 517
526 518 [[package]]
527 519 name = "base64"
@@ -536,37 +528,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
536 528 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
537 529
538 530 [[package]]
539 - name = "bindgen"
540 - version = "0.72.1"
541 - source = "registry+https://github.com/rust-lang/crates.io-index"
542 - checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
543 - dependencies = [
544 - "bitflags 2.11.0",
545 - "cexpr",
546 - "clang-sys",
547 - "itertools",
548 - "proc-macro2",
549 - "quote",
550 - "regex",
551 - "rustc-hash 2.1.1",
552 - "shlex",
553 - "syn 2.0.116",
554 - ]
555 -
556 - [[package]]
557 531 name = "bit-set"
558 - version = "0.8.0"
532 + version = "0.9.1"
559 533 source = "registry+https://github.com/rust-lang/crates.io-index"
560 - checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
534 + checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
561 535 dependencies = [
562 536 "bit-vec",
563 537 ]
564 538
565 539 [[package]]
566 540 name = "bit-vec"
567 - version = "0.8.0"
541 + version = "0.9.1"
568 542 source = "registry+https://github.com/rust-lang/crates.io-index"
569 - checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
543 + checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
570 544
571 545 [[package]]
572 546 name = "bitflags"
@@ -576,9 +550,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
576 550
577 551 [[package]]
578 552 name = "bitflags"
579 - version = "2.11.0"
553 + version = "2.12.1"
580 554 source = "registry+https://github.com/rust-lang/crates.io-index"
581 - checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
555 + checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
582 556 dependencies = [
583 557 "serde_core",
584 558 ]
@@ -622,7 +596,7 @@ version = "0.6.2"
622 596 source = "registry+https://github.com/rust-lang/crates.io-index"
623 597 checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
624 598 dependencies = [
625 - "objc2 0.6.3",
599 + "objc2 0.6.4",
626 600 ]
627 601
628 602 [[package]]
@@ -646,9 +620,9 @@ checksum = "6332c61c205ff066246fff2cb876cdf9009557200f96e15d88d156193b16775b"
646 620
647 621 [[package]]
648 622 name = "bumpalo"
649 - version = "3.19.1"
623 + version = "3.20.3"
650 624 source = "registry+https://github.com/rust-lang/crates.io-index"
651 - checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
625 + checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
652 626
653 627 [[package]]
654 628 name = "bytemuck"
@@ -667,7 +641,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
667 641 dependencies = [
668 642 "proc-macro2",
669 643 "quote",
670 - "syn 2.0.116",
644 + "syn 2.0.117",
671 645 ]
672 646
673 647 [[package]]
@@ -694,7 +668,7 @@ version = "0.18.5"
694 668 source = "registry+https://github.com/rust-lang/crates.io-index"
695 669 checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
696 670 dependencies = [
697 - "bitflags 2.11.0",
671 + "bitflags 2.12.1",
698 672 "cairo-sys-rs",
699 673 "glib",
700 674 "libc",
@@ -719,7 +693,7 @@ version = "0.13.0"
719 693 source = "registry+https://github.com/rust-lang/crates.io-index"
720 694 checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
721 695 dependencies = [
722 - "bitflags 2.11.0",
696 + "bitflags 2.12.1",
723 697 "log",
724 698 "polling",
725 699 "rustix 0.38.44",
@@ -733,9 +707,9 @@ version = "0.14.4"
733 707 source = "registry+https://github.com/rust-lang/crates.io-index"
734 708 checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
735 709 dependencies = [
736 - "bitflags 2.11.0",
710 + "bitflags 2.12.1",
737 711 "polling",
738 - "rustix 1.1.3",
712 + "rustix 1.1.4",
739 713 "slab",
740 714 "tracing",
741 715 ]
@@ -759,16 +733,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
759 733 checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
760 734 dependencies = [
761 735 "calloop 0.14.4",
762 - "rustix 1.1.3",
736 + "rustix 1.1.4",
763 737 "wayland-backend",
764 738 "wayland-client",
765 739 ]
766 740
767 741 [[package]]
768 742 name = "cc"
769 - version = "1.2.56"
743 + version = "1.2.63"
770 744 source = "registry+https://github.com/rust-lang/crates.io-index"
771 - checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
745 + checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
772 746 dependencies = [
773 747 "find-msvc-tools",
774 748 "jobserver",
@@ -783,15 +757,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
783 757 checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
784 758
785 759 [[package]]
786 - name = "cexpr"
787 - version = "0.6.0"
788 - source = "registry+https://github.com/rust-lang/crates.io-index"
789 - checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
790 - dependencies = [
791 - "nom",
792 - ]
793 -
794 - [[package]]
795 760 name = "cfg-expr"
796 761 version = "0.15.8"
797 762 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -872,17 +837,6 @@ dependencies = [
872 837 ]
873 838
874 839 [[package]]
875 - name = "clang-sys"
876 - version = "1.8.1"
877 - source = "registry+https://github.com/rust-lang/crates.io-index"
878 - checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
879 - dependencies = [
880 - "glob",
881 - "libc",
882 - "libloading 0.8.9",
883 - ]
884 -
885 - [[package]]
886 840 name = "clipboard-win"
887 841 version = "5.4.1"
888 842 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -893,15 +847,23 @@ dependencies = [
893 847
894 848 [[package]]
895 849 name = "codespan-reporting"
896 - version = "0.11.1"
850 + version = "0.13.1"
897 851 source = "registry+https://github.com/rust-lang/crates.io-index"
898 - checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
852 + checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
899 853 dependencies = [
900 - "termcolor",
901 854 "unicode-width",
902 855 ]
903 856
904 857 [[package]]
858 + name = "color"
859 + version = "0.3.3"
860 + source = "registry+https://github.com/rust-lang/crates.io-index"
861 + checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
862 + dependencies = [
863 + "bytemuck",
864 + ]
865 +
866 + [[package]]
905 867 name = "combine"
906 868 version = "4.6.7"
907 869 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -992,32 +954,26 @@ dependencies = [
992 954
993 955 [[package]]
994 956 name = "coreaudio-rs"
995 - version = "0.11.3"
957 + version = "0.14.2"
996 958 source = "registry+https://github.com/rust-lang/crates.io-index"
997 - checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
959 + checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
998 960 dependencies = [
999 - "bitflags 1.3.2",
1000 - "core-foundation-sys",
1001 - "coreaudio-sys",
1002 - ]
1003 -
1004 - [[package]]
1005 - name = "coreaudio-sys"
1006 - version = "0.2.17"
1007 - source = "registry+https://github.com/rust-lang/crates.io-index"
1008 - checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6"
1009 - dependencies = [
1010 - "bindgen",
961 + "bitflags 2.12.1",
962 + "libc",
963 + "objc2-audio-toolbox",
964 + "objc2-core-audio",
965 + "objc2-core-audio-types",
966 + "objc2-core-foundation",
1011 967 ]
1012 968
1013 969 [[package]]
1014 970 name = "coremidi"
1015 - version = "0.8.0"
971 + version = "0.9.0"
1016 972 source = "registry+https://github.com/rust-lang/crates.io-index"
1017 - checksum = "964eb3e10ea8b0d29c797086aab3ca730f75e06dced0cb980642fd274a5cca30"
973 + checksum = "f32f5d3e1b800aa7ea10e9e83c87cbc1daef1397ee86cd599458b3c3b136428a"
1018 974 dependencies = [
1019 975 "block",
1020 - "core-foundation 0.9.4",
976 + "core-foundation 0.10.1",
1021 977 "core-foundation-sys",
1022 978 "coremidi-sys",
1023 979 ]
@@ -1033,25 +989,32 @@ dependencies = [
1033 989
1034 990 [[package]]
1035 991 name = "cpal"
1036 - version = "0.15.3"
992 + version = "0.17.3"
1037 993 source = "registry+https://github.com/rust-lang/crates.io-index"
1038 - checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
994 + checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
1039 995 dependencies = [
1040 996 "alsa",
1041 - "core-foundation-sys",
1042 997 "coreaudio-rs",
1043 998 "dasp_sample",
1044 - "jni",
999 + "jni 0.21.1",
1045 1000 "js-sys",
1046 1001 "libc",
1047 1002 "mach2",
1048 - "ndk 0.8.0",
1003 + "ndk",
1049 1004 "ndk-context",
1050 - "oboe",
1005 + "num-derive",
1006 + "num-traits",
1007 + "objc2 0.6.4",
1008 + "objc2-audio-toolbox",
1009 + "objc2-avf-audio",
1010 + "objc2-core-audio",
1011 + "objc2-core-audio-types",
1012 + "objc2-core-foundation",
1013 + "objc2-foundation 0.3.2",
1051 1014 "wasm-bindgen",
1052 1015 "wasm-bindgen-futures",
1053 1016 "web-sys",
1054 - "windows 0.54.0",
1017 + "windows",
1055 1018 ]
1056 1019
1057 1020 [[package]]
@@ -1175,32 +1138,32 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
1175 1138
1176 1139 [[package]]
1177 1140 name = "dispatch2"
1178 - version = "0.3.0"
1141 + version = "0.3.1"
1179 1142 source = "registry+https://github.com/rust-lang/crates.io-index"
1180 - checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
1143 + checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
1181 1144 dependencies = [
1182 - "bitflags 2.11.0",
1145 + "bitflags 2.12.1",
1183 1146 "block2 0.6.2",
1184 1147 "libc",
1185 - "objc2 0.6.3",
1148 + "objc2 0.6.4",
1186 1149 ]
1187 1150
1188 1151 [[package]]
1189 1152 name = "displaydoc"
1190 - version = "0.2.5"
1153 + version = "0.2.6"
1191 1154 source = "registry+https://github.com/rust-lang/crates.io-index"
1192 - checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
1155 + checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
1193 1156 dependencies = [
Lines truncated
M Cargo.toml +7 -7
@@ -12,14 +12,14 @@ audiofiles-core = { path = "crates/audiofiles-core" }
12 12 audiofiles-browser = { path = "crates/audiofiles-browser" }
13 13 audiofiles-sync = { path = "crates/audiofiles-sync" }
14 14 audiofiles-rhai = { path = "crates/audiofiles-rhai" }
15 - egui = { version = "0.31.1", default-features = false, features = ["default_fonts"] }
16 - egui_extras = { version = "0.31.1", default-features = false }
17 - eframe = { version = "0.31.1", default-features = false, features = ["default_fonts", "glow"] }
18 - cpal = "=0.15.3"
15 + egui = { version = "0.34", default-features = false, features = ["default_fonts"] }
16 + egui_extras = { version = "0.34", default-features = false }
17 + eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }
18 + cpal = "0.17"
19 19 rusqlite = { version = "0.31.0", features = ["bundled"] }
20 20 thiserror = "2.0.18"
21 21 sha2 = "0.10.9"
22 - symphonia = { version = "0.5.5", default-features = false, features = ["wav", "aiff", "mp3", "flac", "ogg", "vorbis", "pcm"] }
22 + symphonia = { version = "0.5.5", default-features = false, features = ["wav", "aiff", "mp3", "flac", "ogg", "vorbis", "pcm", "aac", "alac", "isomp4", "caf"] }
23 23 parking_lot = "0.12.5"
24 24 dirs = "6.0.0"
25 25 stratum-dsp = "=1.0.0"
@@ -34,7 +34,7 @@ hound = "3.5"
34 34 tracing = "0.1.44"
35 35 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
36 36 rhai = { version = "1.21", features = ["sync"] }
37 - tray-icon = "0.21"
37 + tray-icon = "0.22"
38 38 tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync"] }
39 39 uuid = { version = "1", features = ["v4"] }
40 40 base64 = "0.22"
@@ -45,7 +45,7 @@ semver = "1"
45 45 open = "5"
46 46 rayon = "1.10"
47 47 libc = "0.2"
48 - midir = "0.10"
48 + midir = "0.11"
49 49 docengine = { path = "../../MNW/shared/docengine" }
50 50 tagtree = { path = "../../MNW/shared/tagtree" }
51 51 theme-common = { path = "../../MNW/shared/theme-common" }
@@ -25,10 +25,11 @@ uuid = { workspace = true }
25 25 chrono = { workspace = true }
26 26 midir = { workspace = true }
27 27 rfd = { workspace = true }
28 + toml = { workspace = true }
28 29
29 30 [dev-dependencies]
30 31 tempfile = "3"
31 32
32 33 [target.'cfg(target_os = "linux")'.dependencies]
33 - eframe = { version = "0.31.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
34 + eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
34 35 gtk = "0.18"
@@ -7,7 +7,7 @@ use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL};
7 7
8 8 impl AudioFilesApp {
9 9 /// Draw the license activation screen.
10 - pub(crate) fn draw_activation_screen(&mut self, ctx: &egui::Context) {
10 + pub(crate) fn draw_activation_screen(&mut self, ui: &mut egui::Ui) {
11 11 // Poll async activation result (take from lock, then drop guard before mutating self)
12 12 let activation = self.activation_result.lock().take();
13 13 if let Some(result) = activation {
@@ -40,7 +40,7 @@ impl AudioFilesApp {
40 40 }
41 41 }
42 42
43 - egui::CentralPanel::default().show(ctx, |ui| {
43 + egui::CentralPanel::default().show_inside(ui, |ui| {
44 44 let available = ui.available_size();
45 45
46 46 ui.add_space((available.y * 0.35).max(40.0));
@@ -116,7 +116,7 @@ impl AudioFilesApp {
116 116
117 117 let can_activate = !self.activating && !self.license_key_input.trim().is_empty();
118 118 ui.horizontal(|ui| {
119 - let button_text = if self.activating { "Activating..." } else { "Activate" };
119 + let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" };
120 120 if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() {
121 121 self.start_activation();
122 122 }
@@ -37,11 +37,14 @@ pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, Str
37 37 .default_output_device()
38 38 .ok_or(AudioError::NoDevice)?;
39 39
40 - let device_name = device.name().unwrap_or_else(|_| "default".to_string());
40 + let device_name = device
41 + .description()
42 + .map(|d| d.name().to_string())
43 + .unwrap_or_else(|_| "default".to_string());
41 44 let config = device.default_output_config()?;
42 45
43 46 let channels = config.channels() as usize;
44 - let device_sample_rate = config.sample_rate().0;
47 + let device_sample_rate = config.sample_rate();
45 48
46 49 let stream = match config.sample_format() {
47 50 cpal::SampleFormat::F32 => build_stream::<f32>(
@@ -142,21 +142,9 @@ fn main() -> eframe::Result<()> {
142 142 const SYNCKIT_TOML: &str = include_str!("../../../synckit.toml");
143 143
144 144 /// Extract the api_key value from the bundled synckit.toml.
145 - fn parse_synckit_toml_key() -> Option<&'static str> {
146 - for line in SYNCKIT_TOML.lines() {
147 - let line = line.trim();
148 - if let Some(rest) = line.strip_prefix("api_key") {
149 - let rest = rest.trim_start();
150 - if let Some(rest) = rest.strip_prefix('=') {
151 - let rest = rest.trim();
152 - let rest = rest.trim_matches('"');
153 - if !rest.is_empty() {
154 - return Some(rest);
155 - }
156 - }
157 - }
158 - }
159 - None
145 + fn parse_synckit_toml_key() -> Option<String> {
146 + let table: std::collections::HashMap<String, String> = toml::from_str(SYNCKIT_TOML).ok()?;
147 + table.get("api_key").filter(|s| !s.is_empty()).cloned()
160 148 }
161 149
162 150 /// Load a saved API key from the data directory, falling back to env vars and bundled toml.
@@ -447,7 +435,9 @@ fn init_browser(data_dir: &Path, shared: Arc<SharedState>, vault_name: &str) ->
447 435
448 436 impl eframe::App for AudioFilesApp {
449 437 #[allow(unused_variables)]
450 - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
438 + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
439 + let ctx = ui.ctx().clone();
440 + let ctx = &ctx;
451 441 // Pump GTK events so libappindicator (tray) stays responsive on Linux.
452 442 #[cfg(target_os = "linux")]
453 443 if self.gtk_ok {
@@ -469,13 +459,13 @@ impl eframe::App for AudioFilesApp {
469 459
470 460 match self.screen {
471 461 AppScreen::Activation => {
472 - self.draw_activation_screen(ctx);
462 + self.draw_activation_screen(ui);
473 463 }
474 464 AppScreen::VaultSetup => {
475 - self.draw_vault_setup_screen(ctx);
465 + self.draw_vault_setup_screen(ui);
476 466 }
477 467 AppScreen::Browser => {
478 - self.update_browser(ctx);
468 + self.update_browser(ui);
479 469 }
480 470 }
481 471
@@ -519,7 +509,9 @@ impl eframe::App for AudioFilesApp {
519 509
520 510 impl AudioFilesApp {
521 511 /// All browser-mode update logic (tray, sync, drops, draw).
522 - fn update_browser(&mut self, ctx: &egui::Context) {
512 + fn update_browser(&mut self, ui: &mut egui::Ui) {
513 + let ctx = ui.ctx().clone();
514 + let ctx = &ctx;
523 515 // Poll tray menu events
524 516 if let Some(ref tray) = self.tray {
525 517 if let Some(action) = tray.poll() {
@@ -716,7 +708,7 @@ impl AudioFilesApp {
716 708 }
717 709 }
718 710 }
719 - audiofiles_browser::editor::draw_browser(ctx, browser, self.sync_manager.as_ref());
711 + audiofiles_browser::editor::draw_browser(ui, browser, self.sync_manager.as_ref());
720 712
721 713 // Drop target indicator: while files are hovering, paint a clear
722 714 // border on top of the whole window plus a centered label. This is
@@ -724,7 +716,7 @@ impl AudioFilesApp {
724 716 // us on Linux/Windows. Rendered as a foreground layer so it sits
725 717 // above panel chrome but doesn't intercept clicks.
726 718 if hovered_count > 0 {
727 - let screen = ctx.screen_rect();
719 + let screen = ctx.content_rect();
728 720 let rect = screen.shrink(theme::space::MD);
729 721 let painter = ctx.layer_painter(egui::LayerId::new(
730 722 egui::Order::Foreground,
@@ -754,7 +746,7 @@ impl AudioFilesApp {
754 746 );
755 747 }
756 748 } else {
757 - self.draw_db_error_screen(ctx);
749 + self.draw_db_error_screen(ui);
758 750 }
759 751 }
760 752 }
@@ -763,8 +755,8 @@ impl AudioFilesApp {
763 755 /// Render the "vault failed to open" recovery surface. Replaces the prior
764 756 /// dead-end label with explicit recovery actions: Retry the same path,
765 757 /// Choose a different vault, or open the data folder for manual triage.
766 - fn draw_db_error_screen(&mut self, ctx: &egui::Context) {
767 - egui::CentralPanel::default().show(ctx, |ui| {
758 + fn draw_db_error_screen(&mut self, ui: &mut egui::Ui) {
759 + egui::CentralPanel::default().show_inside(ui, |ui| {
768 760 ui.add_space(48.0);
769 761 ui.vertical_centered(|ui| {
770 762 ui.heading("audiofiles");
@@ -12,7 +12,7 @@ const DEFAULT_VAULT_NAME: &str = "Library";
12 12
13 13 impl AudioFilesApp {
14 14 /// Draw the vault setup screen (first-open flow, after activation).
15 - pub(crate) fn draw_vault_setup_screen(&mut self, ctx: &egui::Context) {
15 + pub(crate) fn draw_vault_setup_screen(&mut self, ui: &mut egui::Ui) {
16 16 let default_path = vault::default_vault_path();
17 17 let existing_db = default_path.join("audiofiles.db").exists();
18 18
@@ -21,7 +21,7 @@ impl AudioFilesApp {
21 21 self.vault_setup_name = DEFAULT_VAULT_NAME.to_string();
22 22 }
23 23
24 - egui::CentralPanel::default().show(ctx, |ui| {
24 + egui::CentralPanel::default().show_inside(ui, |ui| {
25 25 let available = ui.available_size();
26 26 ui.add_space((available.y * 0.20).max(40.0));
27 27
@@ -10,10 +10,12 @@ use audiofiles_core::vfs::NodeType;
10 10 ///
11 11 /// Pass `sync_manager: None` when sync is not configured (e.g. from the CLAP plugin).
12 12 pub fn draw_browser(
13 - ctx: &egui::Context,
13 + ui: &mut egui::Ui,
14 14 state: &mut BrowserState,
15 15 sync_manager: Option<&audiofiles_sync::SyncManager>,
16 16 ) {
17 + let ctx = ui.ctx().clone();
18 + let ctx = &ctx;
17 19 theme::apply_theme(ctx);
18 20
19 21 match &state.import_mode {
@@ -23,10 +25,10 @@ pub fn draw_browser(
23 25 ctx.request_repaint();
24 26 }
25 27 handle_keyboard(ctx, state);
26 - draw_normal_browser(ctx, state, sync_manager);
28 + draw_normal_browser(ui, state, sync_manager);
27 29 }
28 30 ImportMode::ConfigureImport { .. } => {
29 - import_screens::draw_configure_import(ctx, state);
31 + import_screens::draw_configure_import(ui, state);
30 32 }
31 33 ImportMode::Importing { .. }
32 34 | ImportMode::Analyzing { .. }
@@ -37,56 +39,56 @@ pub fn draw_browser(
37 39 }
38 40 match &state.import_mode {
39 41 ImportMode::Importing { .. } => {
40 - import_screens::draw_import_progress(ctx, state);
42 + import_screens::draw_import_progress(ui, state);
41 43 }
42 44 ImportMode::Analyzing { .. } => {
43 - import_screens::draw_analysis_progress(ctx, state);
45 + import_screens::draw_analysis_progress(ui, state);
44 46 }
45 47 ImportMode::Exporting { .. } => {
46 - export_screens::draw_export_progress(ctx, state);
48 + export_screens::draw_export_progress(ui, state);
47 49 }
48 50 ImportMode::Cleaning { .. } => {
49 - import_screens::draw_cleanup_progress(ctx, state);
51 + import_screens::draw_cleanup_progress(ui, state);
50 52 }
51 53 // poll_workers may have transitioned the mode
52 54 ImportMode::TagFolders { .. } => {
53 - import_screens::draw_tag_folders(ctx, state);
55 + import_screens::draw_tag_folders(ui, state);
54 56 }
55 57 ImportMode::ConfigureAnalysis { .. } => {
56 - import_screens::draw_configure_analysis(ctx, state);
58 + import_screens::draw_configure_analysis(ui, state);
57 59 }
58 60 ImportMode::ReviewSuggestions { .. } => {
59 - import_screens::draw_review_suggestions(ctx, state);
61 + import_screens::draw_review_suggestions(ui, state);
60 62 }
61 63 ImportMode::ExportComplete { .. } => {
62 - export_screens::draw_export_complete(ctx, state);
64 + export_screens::draw_export_complete(ui, state);
63 65 }
64 66 ImportMode::ReviewErrors => {
65 - import_screens::draw_review_errors(ctx, state);
67 + import_screens::draw_review_errors(ui, state);
66 68 }
67 69 _ => {}
68 70 }
69 71 }
70 72 ImportMode::TagFolders { .. } => {
71 - import_screens::draw_tag_folders(ctx, state);
73 + import_screens::draw_tag_folders(ui, state);
72 74 }
73 75 ImportMode::ConfigureAnalysis { .. } => {
74 - import_screens::draw_configure_analysis(ctx, state);
76 + import_screens::draw_configure_analysis(ui, state);
75 77 }
76 78 ImportMode::ReviewSuggestions { .. } => {
77 - import_screens::draw_review_suggestions(ctx, state);
79 + import_screens::draw_review_suggestions(ui, state);
78 80 }
79 81 ImportMode::ConfigureExport { .. } => {
80 - export_screens::draw_configure_export(ctx, state);
82 + export_screens::draw_configure_export(ui, state);
81 83 }
82 84 ImportMode::ExportComplete { .. } => {
83 - export_screens::draw_export_complete(ctx, state);
85 + export_screens::draw_export_complete(ui, state);
84 86 }
85 87 ImportMode::ReviewErrors => {
86 - import_screens::draw_review_errors(ctx, state);
88 + import_screens::draw_review_errors(ui, state);
87 89 }
88 90 ImportMode::OperationCancelled { .. } => {
89 - import_screens::draw_operation_cancelled(ctx, state);
91 + import_screens::draw_operation_cancelled(ui, state);
90 92 }
91 93 }
92 94
@@ -141,42 +143,45 @@ pub fn draw_browser(
141 143
142 144 /// Draw the main browser layout: toolbar, footer, sidebar, detail panel, and file list.
143 145 fn draw_normal_browser(
144 - ctx: &egui::Context,
146 + ui: &mut egui::Ui,
145 147 state: &mut BrowserState,
146 148 sync_manager: Option<&audiofiles_sync::SyncManager>,
147 149 ) {
150 + let ctx = ui.ctx().clone();
151 +
148 152 // Top toolbar (breadcrumb + search)
149 - egui::TopBottomPanel::top("toolbar")
150 - .exact_height(56.0)
151 - .show(ctx, |ui| {
153 + egui::Panel::top("toolbar")
154 + .exact_size(56.0)
155 + .show_inside(ui, |ui| {
152 156 toolbar::draw_toolbar(ui, state, sync_manager);
153 157 });
154 158
155 159 // Bottom footer
156 - egui::TopBottomPanel::bottom("footer").show(ctx, |ui| {
157 - footer::draw_footer(ui, ctx, state);
160 + egui::Panel::bottom("footer").show_inside(ui, |ui| {
161 + let ctx = ui.ctx().clone();
162 + footer::draw_footer(ui, &ctx, state);
158 163 });
159 164
160 165 // Floating MIDI/instrument window
161 166 if state.show_midi_window {
162 - instrument_panel::draw_midi_window(ctx, state);
167 + instrument_panel::draw_midi_window(&ctx, state);
163 168 }
164 169
165 170 // Left sidebar (or filter panel)
166 171 if state.filter_panel_open {
167 - egui::SidePanel::left("filter_panel")
168 - .default_width(200.0)
169 - .width_range(160.0..=300.0)
170 - .show(ctx, |ui| {
172 + egui::Panel::left("filter_panel")
173 + .default_size(200.0)
174 + .size_range(160.0..=300.0)
175 + .show_inside(ui, |ui| {
171 176 egui::ScrollArea::vertical().show(ui, |ui| {
172 177 filter_panel::draw_filter_panel(ui, state);
173 178 });
174 179 });
175 180 } else if state.sidebar_visible {
176 - egui::SidePanel::left("sidebar")
177 - .default_width(180.0)
178 - .width_range(120.0..=280.0)
179 - .show(ctx, |ui| {
181 + egui::Panel::left("sidebar")
182 + .default_size(180.0)
183 + .size_range(120.0..=280.0)
184 + .show_inside(ui, |ui| {
180 185 sidebar::draw_sidebar(ui, state);
181 186 });
182 187 }
@@ -184,22 +189,19 @@ fn draw_normal_browser(
184 189 // Right detail panel (auto-hide below 700px).
185 190 // 700.0 is the minimum window width at which the detail panel is shown.
186 191 // Below this, the file list alone needs the full width to remain usable.
187 - if state.detail_visible {
188 - let available = ctx.screen_rect().width();
189 - if available >= 700.0 {
190 - egui::SidePanel::right("detail")
191 - .default_width(250.0)
192 - .width_range(200.0..=400.0)
193 - .show(ctx, |ui| {
194 - egui::ScrollArea::vertical().show(ui, |ui| {
195 - detail::draw_detail(ui, state);
196 - });
192 + if state.detail_visible && ctx.content_rect().width() >= 700.0 {
193 + egui::Panel::right("detail")
194 + .default_size(250.0)
195 + .size_range(200.0..=400.0)
196 + .show_inside(ui, |ui| {
197 + egui::ScrollArea::vertical().show(ui, |ui| {
198 + detail::draw_detail(ui, state);
197 199 });
198 - }
200 + });
199 201 }
200 202
201 203 // Central file list
202 - egui::CentralPanel::default().show(ctx, |ui| {
204 + egui::CentralPanel::default().show_inside(ui, |ui| {
203 205 file_list::draw_file_list(ui, state, sync_manager);
204 206 });
205 207 }
@@ -590,7 +590,7 @@ fn draw_multi_summary(ui: &mut egui::Ui, state: &mut BrowserState) {
590 590 .filter_map(|n| n.node.sample_hash.as_ref().map(|h| h.to_string()))
591 591 .collect();
592 592 pending_apply = Some((tag.clone(), targets));
593 - ui.close_menu();
593 + ui.close();
594 594 }
595 595 }
596 596 let remove_label = if full {
@@ -605,7 +605,7 @@ fn draw_multi_summary(ui: &mut egui::Ui, state: &mut BrowserState) {
605 605 .filter_map(|n| n.node.sample_hash.as_ref().map(|h| h.to_string()))
606 606 .collect();
607 607 pending_remove = Some((tag.clone(), targets));
608 - ui.close_menu();
608 + ui.close();
609 609 }
610 610 });
611 611 }
@@ -75,7 +75,7 @@ fn bytes_per_sec_for_config(config: &ExportConfig) -> u64 {
75 75 }
76 76
77 77 /// Draw the export configuration screen.
78 - pub fn draw_configure_export(ctx: &egui::Context, state: &mut BrowserState) {
78 + pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
79 79 let (item_count, profile_count) = match &state.import_mode {
80 80 ImportMode::ConfigureExport {
81 81 items,
@@ -85,7 +85,7 @@ pub fn draw_configure_export(ctx: &egui::Context, state: &mut BrowserState) {
85 85 _ => return,
86 86 };
87 87
88 - egui::TopBottomPanel::bottom("export_footer").show(ctx, |ui| {
88 + egui::Panel::bottom("export_footer").show_inside(ui, |ui| {
89 89 ui.add_space(theme::space::SM);
90 90
91 91 // Warnings
@@ -194,7 +194,7 @@ pub fn draw_configure_export(ctx: &egui::Context, state: &mut BrowserState) {
194 194 ui.add_space(theme::space::XS);
195 195 });
196 196
197 - egui::CentralPanel::default().show(ctx, |ui| {
197 + egui::CentralPanel::default().show_inside(ui, |ui| {
198 198 egui::ScrollArea::vertical().show(ui, |ui| {
199 199 ui.heading("Export Samples");
200 200 ui.add_space(theme::space::SM);
@@ -319,6 +319,23 @@ pub fn draw_configure_export(ctx: &egui::Context, state: &mut BrowserState) {
319 319 if ui.radio(is_aiff, "AIFF (decode and re-encode)").clicked() && !is_aiff {
320 320 config.format = ExportFormat::Aiff;
321 321 }
322 +
323 + // Heads-up: WAV/AIFF re-encode writes only fmt+data /
324 + // COMM+SSND. Embedded BWF (bext), iXML, smpl loop points,
325 + // cue markers, and ID3 tags are not preserved. Choose
326 + // Original to keep them intact.
327 + if config.format != ExportFormat::Original {
328 + ui.add_space(theme::space::XS);
329 + ui.label(
330 + egui::RichText::new(
331 + "Re-encoding strips embedded metadata chunks \
332 + (BWF, iXML, loop points, cue markers, ID3). \
333 + Choose Original to preserve them.",
334 + )
335 + .small()
336 + .color(theme::accent_yellow()),
337 + );
338 + }
322 339 }
323 340 ui.add_space(theme::space::MD);
324 341
@@ -516,7 +533,7 @@ pub fn draw_configure_export(ctx: &egui::Context, state: &mut BrowserState) {
516 533 }
517 534
518 535 /// Draw the export progress screen.
519 - pub fn draw_export_progress(ctx: &egui::Context, state: &mut BrowserState) {
536 + pub fn draw_export_progress(ui: &mut egui::Ui, state: &mut BrowserState) {
520 537 let (completed, total, current_name) = match &state.import_mode {
521 538 ImportMode::Exporting {
522 539 completed,
@@ -526,7 +543,7 @@ pub fn draw_export_progress(ctx: &egui::Context, state: &mut BrowserState) {
526 543 _ => return,
527 544 };
528 545
529 - egui::CentralPanel::default().show(ctx, |ui| {
546 + egui::CentralPanel::default().show_inside(ui, |ui| {
530 547 ui.heading("Exporting...");
531 548 ui.add_space(theme::space::SECTION);
532 549
@@ -561,13 +578,13 @@ pub fn draw_export_progress(ctx: &egui::Context, state: &mut BrowserState) {
561 578 }
562 579
563 580 /// Draw the export complete screen with summary and error list.
564 - pub fn draw_export_complete(ctx: &egui::Context, state: &mut BrowserState) {
581 + pub fn draw_export_complete(ui: &mut egui::Ui, state: &mut BrowserState) {
565 582 let (total, error_count) = match &state.import_mode {
566 583 ImportMode::ExportComplete { total, errors } => (*total, errors.len()),
567 584 _ => return,
568 585 };
569 586
570 - egui::CentralPanel::default().show(ctx, |ui| {
587 + egui::CentralPanel::default().show_inside(ui, |ui| {
571 588 ui.heading("Export Complete");
572 589 ui.add_space(theme::space::LG);
573 590
@@ -49,7 +49,7 @@ pub fn draw_context_menu(
49 49 state.status =
50 50 "Sync not ready — open the Sync panel first".to_string();
51 51 }
52 - ui.close_menu();
52 + ui.close();
53 53 }
54 54 }
55 55 }
@@ -60,14 +60,14 @@ pub fn draw_context_menu(
60 60 let hash = hash.clone();
61 61 state.trigger_preview(&hash);
62 62 }
63 - ui.close_menu();
63 + ui.close();
64 64 }
65 65 if ui.button("Copy Path").clicked() {
66 66 if let Some(path) = state.selected_sample_path() {
67 67 state.status = format!("Copied: {path}");
68 68 ui.ctx().copy_text(path);
69 69 }
70 - ui.close_menu();
70 + ui.close();
71 71 }
72 72 // M-6: one-click jump to the file in the system file manager.
73 73 // macOS / Windows highlight the file itself; Linux falls back to
@@ -95,21 +95,21 @@ pub fn draw_context_menu(
95 95 let _ = std::process::Command::new("xdg-open").arg(&parent).spawn();
96 96 }
97 97 }
98 - ui.close_menu();
98 + ui.close();
99 99 }
100 100 if ui.button("Find Similar (Shift+F)").clicked() {
101 101 if let Some(hash) = &node.node.sample_hash {
102 102 let hash = hash.clone();
103 103 state.find_similar(&hash);
104 104 }
105 - ui.close_menu();
105 + ui.close();
106 106 }
107 107 if ui.button("Find Duplicates (Shift+D)").clicked() {
108 108 if let Some(hash) = &node.node.sample_hash {
109 109 let hash = hash.clone();
110 110 state.find_near_duplicates(&hash);
111 111 }
112 - ui.close_menu();
112 + ui.close();
113 113 }
114 114 // Add to Collection submenu
115 115 if let Some(hash) = &node.node.sample_hash {
@@ -123,7 +123,7 @@ pub fn draw_context_menu(
123 123 let _ = state.backend.add_to_collection(coll.id, &hash_clone);
124 124 state.refresh_collections();
125 125 state.status = format!("Added to {}", coll.name);
126 - ui.close_menu();
126 + ui.close();
127 127 }
128 128 }
129 129 });
@@ -134,7 +134,7 @@ pub fn draw_context_menu(
134 134 let _ = state.backend.remove_from_collection(active_id, &hash_clone);
135 135 state.refresh_collections();
136 136 state.activate_collection(active_id);
137 - ui.close_menu();
137 + ui.close();
138 138 }
139 139 }
140 140 }
@@ -144,7 +144,7 @@ pub fn draw_context_menu(
144 144 let hash_clone = hash.clone();
145 145 if ui.button("Edit... (E)").clicked() {
146 146 state.open_edit_window(&hash_clone);
147 - ui.close_menu();
147 + ui.close();
148 148 }
149 149 }
150 150 if ui.button("Play as Instrument").clicked() {
@@ -156,12 +156,12 @@ pub fn draw_context_menu(
156 156 state.show_midi_window = true;
157 157 state.status = format!("Instrument: {name}");
158 158 }
159 - ui.close_menu();
159 + ui.close();
160 160 }
161 161 if ui.button("Export...").clicked() {
162 162 state.selection.set_single(row_idx);
163 163 state.start_export_flow(Some(vec![node.node.id]));
164 - ui.close_menu();
164 + ui.close();
165 165 }
166 166 // M-7: single-row Re-analyze parity with the multi-row menu.
167 167 // Reuses ReanalyzeOverwrite with a one-element vec so the
@@ -188,41 +188,41 @@ pub fn draw_context_menu(
188 188 }
189 189 }
190 190 }
191 - ui.close_menu();
191 + ui.close();
192 192 }
193 193 }
194 194 ui.separator();
195 195 if widgets::danger_button(ui, "Delete").clicked() {
196 196 state.selection.set_single(row_idx);
197 197 state.confirm_delete_selected();
198 - ui.close_menu();
198 + ui.close();
199 199 }
200 200 }
201 201 NodeType::Directory => {
202 202 if ui.button("Open").clicked() {
203 203 state.selection.set_single(row_idx);
204 204 state.enter_directory();
205 - ui.close_menu();
205 + ui.close();
206 206 }
207 207 if ui.button("New Folder").clicked() {
208 208 state.show_dir_create = true;
209 209 state.dir_create_input.clear();
210 - ui.close_menu();
210 + ui.close();
211 211 }
212 212 if ui.button("Rename").clicked() {
213 213 state.dir_rename_target = Some((node.node.id, node.node.name.clone()));
214 - ui.close_menu();
214 + ui.close();
215 215 }
216 216 if ui.button("Export...").clicked() {
217 217 state.selection.set_single(row_idx);
218 218 state.start_export_flow(Some(vec![node.node.id]));
219 - ui.close_menu();
219 + ui.close();
220 220 }
221 221 ui.separator();
222 222 if widgets::danger_button(ui, "Delete").clicked() {
223 223 state.selection.set_single(row_idx);
224 224 state.confirm_delete_selected();
225 - ui.close_menu();
225 + ui.close();
226 226 }
227 227 }
228 228 }
@@ -236,14 +236,14 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
236 236
237 237 if ui.button("Invert Selection (Cmd+Shift+I)").clicked() {
238 238 state.invert_selection();
239 - ui.close_menu();
239 + ui.close();
240 240 }
241 241
242 242 ui.separator();
243 243
244 244 if ui.button("Tag... (Cmd+T)").clicked() {
245 245 state.open_bulk_tag_modal();
246 - ui.close_menu();
246 + ui.close();
247 247 }
248 248 // m-16: Cmd+M conflicts with the macOS minimize-window shortcut. Label
249 249 // advertises Cmd+Shift+M; the actual key binding lives in
@@ -251,16 +251,16 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
251 251 // there to match.
252 252 if ui.button("Move to... (Cmd+Shift+M)").clicked() {
253 253 state.open_bulk_move_modal();
254 - ui.close_menu();
254 + ui.close();
255 255 }
256 256 if ui.button("Rename... (F2)").clicked() {
257 257 state.open_bulk_rename_modal();
258 - ui.close_menu();
258 + ui.close();
259 259 }
260 260 if ui.button("Export...").clicked() {
261 261 let node_ids = state.selected_node_ids();
262 262 state.start_export_flow(Some(node_ids));
263 - ui.close_menu();
263 + ui.close();
264 264 }
265 265
266 266 // Add to Collection submenu (bulk)
@@ -277,7 +277,7 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
277 277 }
278 278 state.refresh_collections();
279 279 state.status = format!("Added {} items to {}", nodes.len(), coll.name);
280 - ui.close_menu();
280 + ui.close();
281 281 }
282 282 }
283 283 });
@@ -294,7 +294,7 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
294 294 }
295 295 state.refresh_collections();
296 296 state.activate_collection(active_id);
297 - ui.close_menu();
297 + ui.close();
298 298 }
299 299 }
300 300
@@ -325,7 +325,7 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
325 325 } else {
326 326 state.start_analysis_flow(hashes);
327 327 }
328 - ui.close_menu();
328 + ui.close();
329 329 }
330 330
331 331 // Copy tags from focused sample to all selected
@@ -351,7 +351,7 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
351 351 state.status = format!("Copied {} tags to {} samples", src_tags.len(), applied);
352 352 state.refresh_selected_tags();
353 353 }
354 - ui.close_menu();
354 + ui.close();
355 355 }
356 356 }
357 357 }
@@ -383,12 +383,12 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
383 383 };
384 384 ui.ctx().copy_text(paths.join("\n"));
385 385 }
386 - ui.close_menu();
386 + ui.close();
387 387 }
388 388
389 389 if widgets::danger_button(ui, "Delete").clicked() {
390 390 state.confirm_delete_selected();
391 - ui.close_menu();
391 + ui.close();
392 392 }
393 393 }
394 394
@@ -397,7 +397,7 @@ pub fn draw_background_context_menu(ui: &mut egui::Ui, state: &mut BrowserState)
397 397 if ui.button("New Folder").clicked() {
398 398 state.show_dir_create = true;
399 399 state.dir_create_input.clear();
400 - ui.close_menu();
400 + ui.close();
401 401 }
402 402 if ui.button("Import files...").clicked() {
403 403 if let Some(paths) = rfd::FileDialog::new()
@@ -409,7 +409,7 @@ pub fn draw_background_context_menu(ui: &mut egui::Ui, state: &mut BrowserState)
409 409 state.import_path(&path);
410 410 }
411 411 }
412 - ui.close_menu();
412 + ui.close();
413 413 }
414 414 // C-2: matches the toolbar's "Import folder..." (wizard path). The quick
415 415 // import shortcut is only offered from the toolbar to keep this menu
@@ -418,7 +418,7 @@ pub fn draw_background_context_menu(ui: &mut egui::Ui, state: &mut BrowserState)
418 418 if let Some(path) = rfd::FileDialog::new().pick_folder() {
419 419 state.show_import_options(path);
420 420 }
421 - ui.close_menu();
421 + ui.close();
422 422 }
423 423 if state.selection.count() > 0 {
424 424 ui.separator();
@@ -427,11 +427,11 @@ pub fn draw_background_context_menu(ui: &mut egui::Ui, state: &mut BrowserState)
427 427 state.selection.clear();
428 428 state.refresh_selected_tags();
429 429 state.refresh_selected_detail();
430 - ui.close_menu();
430 + ui.close();
431 431 }
432 432 if ui.button("Invert Selection (Cmd+Shift+I)").clicked() {
433 433 state.invert_selection();
434 - ui.close_menu();
434 + ui.close();
435 435 }
436 436 }
437 437 }
@@ -35,7 +35,7 @@ fn dot(ui: &mut egui::Ui) {
35 35 pub fn draw_footer(ui: &mut egui::Ui, ctx: &egui::Context, state: &mut BrowserState) {
36 36 ui.add_space(theme::space::SM);
37 37
38 - let narrow = ctx.screen_rect().width() < 1000.0;
38 + let narrow = ctx.content_rect().width() < 1000.0;
39 39
40 40 // Transport row
41 41 ui.horizontal(|ui| {
@@ -7,7 +7,7 @@ use crate::state::{BrowserState, ImportMode};
7 7 const STEPS: &[&str] = &["Configure", "Tag folders", "Analyze", "Review"];
8 8
9 9 /// Draw the import configuration screen with strategy radio buttons.
10 - pub fn draw_configure_import(ctx: &egui::Context, state: &mut BrowserState) {
10 + pub fn draw_configure_import(ui: &mut egui::Ui, state: &mut BrowserState) {
11 11 let (source_display, file_count) = match &state.import_mode {
12 12 ImportMode::ConfigureImport { source, audio_file_count, .. } => {
13 13 (source.display().to_string(), *audio_file_count)
@@ -15,7 +15,7 @@ pub fn draw_configure_import(ctx: &egui::Context, state: &mut BrowserState) {
15 15 _ => return,
16 16 };
17 17
18 - egui::CentralPanel::default().show(ctx, |ui| {
18 + egui::CentralPanel::default().show_inside(ui, |ui| {
19 19 widgets::wizard_steps(ui, STEPS, 0);
20 20 ui.heading("Import Folder");
21 21 ui.add_space(theme::space::MD);
@@ -256,7 +256,7 @@ pub fn draw_configure_import(ctx: &egui::Context, state: &mut BrowserState) {
256 256 }
257 257
258 258 /// Draw the analysis configuration screen.
259 - pub fn draw_configure_analysis(ctx: &egui::Context, state: &mut BrowserState) {
259 + pub fn draw_configure_analysis(ui: &mut egui::Ui, state: &mut BrowserState) {
260 260 let (sample_count, mut config) = match &state.import_mode {
261 261 ImportMode::ConfigureAnalysis {
262 262 sample_hashes,
@@ -265,7 +265,7 @@ pub fn draw_configure_analysis(ctx: &egui::Context, state: &mut BrowserState) {
265 265 _ => return,
266 266 };
267 267
268 - egui::CentralPanel::default().show(ctx, |ui| {
268 + egui::CentralPanel::default().show_inside(ui, |ui| {
269 269 widgets::wizard_steps(ui, STEPS, 2);
270 270 ui.heading("Configure Analysis");
271 271 ui.add_space(theme::space::MD);
@@ -90,7 +90,8 @@ fn draw_rate_and_eta(
90 90 }
91 91
92 92 /// Draw the folder import progress screen.
93 - pub fn draw_import_progress(ctx: &egui::Context, state: &mut BrowserState) {
93 + pub fn draw_import_progress(ui: &mut egui::Ui, state: &mut BrowserState) {
94 + let ctx = ui.ctx().clone();
94 95 let (total, completed, current_name, walking, walking_count, total_bytes, loose_files) = match &state.import_mode {
95 96 ImportMode::Importing {
96 97 total,
@@ -104,7 +105,7 @@ pub fn draw_import_progress(ctx: &egui::Context, state: &mut BrowserState) {
104 105 _ => return,
105 106 };
106 107
107 - egui::CentralPanel::default().show(ctx, |ui| {
108 + egui::CentralPanel::default().show_inside(ui, |ui| {
108 109 ui.heading("Importing Folder...");
109 110 ui.add_space(theme::space::LG);
110 111
@@ -203,7 +204,8 @@ pub fn draw_import_progress(ctx: &egui::Context, state: &mut BrowserState) {
203 204 }
204 205
205 206 /// Draw the cleanup (orphaned sample removal) progress screen.
206 - pub fn draw_cleanup_progress(ctx: &egui::Context, state: &mut BrowserState) {
207 + pub fn draw_cleanup_progress(ui: &mut egui::Ui, state: &mut BrowserState) {
208 + let ctx = ui.ctx().clone();
207 209 let (completed, total, current_name) = match &state.import_mode {
208 210 ImportMode::Cleaning {
209 211 completed,
@@ -213,7 +215,7 @@ pub fn draw_cleanup_progress(ctx: &egui::Context, state: &mut BrowserState) {
213 215 _ => return,
214 216 };
215 217
216 - egui::CentralPanel::default().show(ctx, |ui| {
218 + egui::CentralPanel::default().show_inside(ui, |ui| {
217 219 ui.heading("Cleaning Up Samples...");
218 220 ui.add_space(theme::space::LG);
219 221
@@ -246,7 +248,8 @@ pub fn draw_cleanup_progress(ctx: &egui::Context, state: &mut BrowserState) {
246 248 }
247 249
248 250 /// Draw the analysis progress screen.
249 - pub fn draw_analysis_progress(ctx: &egui::Context, state: &mut BrowserState) {
251 + pub fn draw_analysis_progress(ui: &mut egui::Ui, state: &mut BrowserState) {
252 + let ctx = ui.ctx().clone();
250 253 let (completed, total, current_name) = match &state.import_mode {
251 254 ImportMode::Analyzing {
252 255 completed,
@@ -256,7 +259,7 @@ pub fn draw_analysis_progress(ctx: &egui::Context, state: &mut BrowserState) {
256 259 _ => return,
257 260 };
258 261
259 - egui::CentralPanel::default().show(ctx, |ui| {
262 + egui::CentralPanel::default().show_inside(ui, |ui| {
260 263 ui.heading("Analyzing Samples...");
261 264 ui.add_space(theme::space::LG);
262 265
@@ -308,7 +311,7 @@ pub fn draw_analysis_progress(ctx: &egui::Context, state: &mut BrowserState) {
308 311 /// Acknowledgement screen shown after the user cancels a long-running import,
309 312 /// analysis, or export. Phase-5 C-3: cancelling shouldn't drop straight to
310 313 /// `None` — the user needs to know what landed vs what was discarded.
311 - pub fn draw_operation_cancelled(ctx: &egui::Context, state: &mut BrowserState) {
314 + pub fn draw_operation_cancelled(ui: &mut egui::Ui, state: &mut BrowserState) {
312 315 let (kind, completed, total, destination) = match &state.import_mode {
313 316 ImportMode::OperationCancelled {
314 317 kind, completed, total, destination,
@@ -334,7 +337,7 @@ pub fn draw_operation_cancelled(ctx: &egui::Context, state: &mut BrowserState) {
334 337 ),
335 338 };
336 339
337 - egui::CentralPanel::default().show(ctx, |ui| {
340 + egui::CentralPanel::default().show_inside(ui, |ui| {
338 341 ui.heading(heading);
339 342 ui.add_space(theme::space::LG);
340 343 ui.label(
@@ -5,12 +5,12 @@ use crate::state::{BrowserState, ConfirmAction, ImportMode};
5 5 use super::super::{theme, widgets};
6 6
7 7 /// Draw the post-import error review screen.
8 - pub fn draw_review_errors(ctx: &egui::Context, state: &mut BrowserState) {
8 + pub fn draw_review_errors(ui: &mut egui::Ui, state: &mut BrowserState) {
9 9 if !matches!(state.import_mode, ImportMode::ReviewErrors) {
10 10 return;
11 11 }
12 12
13 - egui::CentralPanel::default().show(ctx, |ui| {
13 + egui::CentralPanel::default().show_inside(ui, |ui| {
14 14 ui.heading("Import Summary");
15 15 ui.add_space(theme::space::LG);
16 16
@@ -10,7 +10,7 @@ const STEPS: &[&str] = &["Configure", "Tag folders", "Analyze", "Review"];
10 10
11 11
12 12 /// Draw the post-import folder tagging screen.
13 - pub fn draw_tag_folders(ctx: &egui::Context, state: &mut BrowserState) {
13 + pub fn draw_tag_folders(ui: &mut egui::Ui, state: &mut BrowserState) {
14 14 let (entry_count, total_samples, all_empty) = match &state.import_mode {
15 15 ImportMode::TagFolders { entries, .. } => {
16 16 let total: usize = entries.iter().map(|e| e.folder.samples.len()).sum();
@@ -20,7 +20,7 @@ pub fn draw_tag_folders(ctx: &egui::Context, state: &mut BrowserState) {
20 20 _ => return,
21 21 };
22 22
23 - egui::TopBottomPanel::bottom("tag_folders_footer").show(ctx, |ui| {
23 + egui::Panel::bottom("tag_folders_footer").show_inside(ui, |ui| {
24 24 ui.add_space(theme::space::SM);
25 25 ui.horizontal(|ui| {
26 26 if ui.button("Skip").clicked() {
@@ -40,7 +40,7 @@ pub fn draw_tag_folders(ctx: &egui::Context, state: &mut BrowserState) {
40 40 ui.add_space(theme::space::XS);
41 41 });
42 42
43 - egui::CentralPanel::default().show(ctx, |ui| {
43 + egui::CentralPanel::default().show_inside(ui, |ui| {
44 44 // m-6: the breadcrumb is decorative per C-1 — the wizard has no
45 45 // cross-step navigation post-Skip, so the step-1 "Tag folders" cell
46 46 // staying highlighted after Skip is cosmetic only. `wizard_steps` has
@@ -134,7 +134,8 @@ pub fn draw_tag_folders(ctx: &egui::Context, state: &mut BrowserState) {
134 134 }
135 135
136 136 /// Draw the tag review screen.
137 - pub fn draw_review_suggestions(ctx: &egui::Context, state: &mut BrowserState) {
137 + pub fn draw_review_suggestions(ui: &mut egui::Ui, state: &mut BrowserState) {
138 + let ctx = ui.ctx().clone();
138 139 let (item_count, total_suggestions, accepted_count, _current_idx) =
139 140 match &state.import_mode {
140 141 ImportMode::ReviewSuggestions { items, current_idx, .. } => {
@@ -149,7 +150,7 @@ pub fn draw_review_suggestions(ctx: &egui::Context, state: &mut BrowserState) {
149 150 _ => return,
150 151 };
151 152
152 - egui::TopBottomPanel::top("review_header").show(ctx, |ui| {
153 + egui::Panel::top("review_header").show_inside(ui, |ui| {
153 154 widgets::wizard_steps(ui, STEPS, 3);
154 155 ui.horizontal(|ui| {
155 156 ui.heading("Review Tag Suggestions");
@@ -229,7 +230,7 @@ pub fn draw_review_suggestions(ctx: &egui::Context, state: &mut BrowserState) {
229 230 }
230 231 });
231 232
232 - egui::TopBottomPanel::bottom("review_footer").show(ctx, |ui| {
233 + egui::Panel::bottom("review_footer").show_inside(ui, |ui| {
233 234 ui.add_space(theme::space::SM);
234 235 ui.horizontal(|ui| {
235 236 if ui.button("Cancel").clicked() {
@@ -271,10 +272,10 @@ pub fn draw_review_suggestions(ctx: &egui::Context, state: &mut BrowserState) {
271 272 }
272 273 };
273 274
274 - egui::SidePanel::left("review_samples")
275 + egui::Panel::left("review_samples")
275 276 .resizable(true)
276 - .default_width(220.0)
277 - .show(ctx, |ui| {
277 + .default_size(220.0)
278 + .show_inside(ui, |ui| {
278 279 // p-3: sort selector at the top of the side panel. The sort
279 280 // doesn't mutate `items` — `display_order` below maps display
280 281 // position → item index so `current_idx` keeps pointing at the
@@ -351,7 +352,7 @@ pub fn draw_review_suggestions(ctx: &egui::Context, state: &mut BrowserState) {
351 352 });
352 353 });
353 354
354 - egui::CentralPanel::default().show(ctx, |ui| {
355 + egui::CentralPanel::default().show_inside(ui, |ui| {
355 356 if let ImportMode::ReviewSuggestions {
356 357 ref mut items,
357 358 current_idx,
@@ -539,17 +539,18 @@ fn draw_piano_keyboard(ui: &mut egui::Ui, state: &mut BrowserState) {
539 539 2.0,
540 540 theme::accent_blue().linear_multiply(0.3),
541 541 );
542 - egui::show_tooltip_at_pointer(
543 - ui.ctx(),
542 + egui::Tooltip::always_open(
543 + ui.ctx().clone(),
544 544 ui.layer_id(),
545 545 egui::Id::new("piano_drop_hint"),
546 - |ui| {
547 - ui.label(format!(
548 - "Drop to create a zone centered on {}",
549 - note_name(root_note),
550 - ));
551 - },
552 - );
546 + egui::PopupAnchor::Pointer,
547 + )
548 + .show(|ui| {
549 + ui.label(format!(
550 + "Drop to create a zone centered on {}",
551 + note_name(root_note),
552 + ));
553 + });
553 554 }
554 555 }
555 556 }
@@ -68,13 +68,13 @@ fn tag_context_menu(response: egui::Response, tag: &str, state: &mut BrowserStat
68 68 .cloned()
69 69 .collect();
70 70 state.tag_rename_preview = Some((count, descendants));
71 - ui.close_menu();
71 + ui.close();
72 72 }
73 73 if widgets::danger_button(ui, "Remove from all samples…").clicked() {
74 74 state.pending_confirm = Some(crate::state::ConfirmAction::RemoveTagGlobally {
75 75 tag: tag.to_string(),
76 76 });
77 - ui.close_menu();
77 + ui.close();
78 78 }
79 79 });
80 80 }
@@ -280,7 +280,7 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
280 280 resp.context_menu(|ui| {
281 281 if ui.button("Rename").clicked() {
282 282 state.vfs_rename_target = Some((vfs_id, vfs_name.clone()));
283 - ui.close_menu();
283 + ui.close();
284 284 }
285 285 // Always render Delete so the user can see the capability exists;
286 286 // disable when removing it would leave zero vaults.
@@ -298,7 +298,7 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
298 298 };
299 299 if delete_resp.clicked() {
300 300 state.pending_confirm = Some(crate::state::ConfirmAction::DeleteVfs { vfs_id, vfs_name });
301 - ui.close_menu();
301 + ui.close();
302 302 }
303 303 });
304 304 }
@@ -352,11 +352,11 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
352 352 resp.context_menu(|ui| {
353 353 if ui.button("Rename").clicked() {
354 354 state.collection_rename_target = Some((coll_id, coll_name.clone()));
355 - ui.close_menu();
355 + ui.close();
356 356 }
357 357 if widgets::danger_button(ui, "Delete").clicked() {
358 358 delete_id = Some((coll_id, coll_name.clone()));
359 - ui.close_menu();
359 + ui.close();
360 360 }
361 361 });
362 362 }
@@ -571,12 +571,12 @@ pub fn apply_theme(ctx: &egui::Context) {
571 571 ctx.set_visuals(visuals);
572 572
573 573 // Apply theme spacing
574 - let mut style = (*ctx.style()).clone();
574 + let mut style = (*ctx.global_style()).clone();
575 575 style.spacing.item_spacing = egui::vec2(spacing_x, spacing_y);
576 576 style.spacing.button_padding = egui::vec2(btn_pad_x, btn_pad_y);
577 577 style.spacing.window_margin = egui::vec2(win_margin, win_margin).into();
578 578 style.spacing.indent = indent;
579 - ctx.set_style(style);
579 + ctx.set_global_style(style);
580 580 }
581 581
582 582 #[cfg(test)]
@@ -76,9 +76,13 @@ pub fn draw_toolbar(
76 76 if state.collection_filter_name_input.is_empty() {
77 77 state.collection_filter_name_input = state.search_filter.describe();
78 78 }
79 - ui.memory_mut(|m| m.toggle_popup(save_id));
79 + egui::Popup::toggle_id(ui.ctx(), save_id);
80 80 }
81 - egui::popup_below_widget(ui, save_id, &save_btn, egui::PopupCloseBehavior::CloseOnClickOutside, |ui| {
81 + egui::Popup::from_response(&save_btn)
82 + .id(save_id)
83 + .open_memory(None)
84 + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
85 + .show(|ui| {
82 86 ui.set_min_width(200.0);
83 87 ui.label(egui::RichText::new("Save as Collection").strong());
84 88 ui.add_space(theme::space::SM);
@@ -94,7 +98,7 @@ pub fn draw_toolbar(
94 98 if ui.add_enabled(!name.is_empty(), egui::Button::new("Save Collection")).clicked() {
95 99 state.save_dynamic_collection(&name);
96 100 state.collection_filter_name_input.clear();
97 - ui.memory_mut(|m| m.close_popup());
101 + egui::Popup::close_id(ui.ctx(), save_id);
98 102 }
99 103 });
100 104 }
@@ -113,7 +117,7 @@ pub fn draw_toolbar(
113 117 // window is too narrow to host them inline. Threshold ~900px keeps the
114 118 // expanded row on common desktop widths but rescues half-screen / DAW
115 119 // companion layouts. Same actions, single dropdown.
116 - let screen_w = ui.ctx().screen_rect().width();
120 + let screen_w = ui.ctx().content_rect().width();
117 121 let collapse_toggles = screen_w < 900.0;
118 122 // M-13 input (shared by both layouts).
119 123 let detail_too_narrow = screen_w < 700.0;
@@ -202,7 +206,7 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo
202 206 .clicked()
203 207 {
204 208 state.sidebar_visible = !state.sidebar_visible;
205 - ui.close_menu();
209 + ui.close();
206 210 }
207 211 let detail_label = if detail_hidden {
208 212 format!("{}Detail (D) \u{2014} hidden (widen window)", active_dot(state.detail_visible))
@@ -211,28 +215,28 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo
211 215 };
212 216 if ui.button(detail_label).clicked() {
213 217 state.detail_visible = !state.detail_visible;
214 - ui.close_menu();
218 + ui.close();
215 219 }
216 220 if ui
217 221 .button(format!("{}Editor (E)", active_dot(state.edit.show_window)))
218 222 .clicked()
219 223 {
220 224 toggle_edit_window(state);
221 - ui.close_menu();
225 + ui.close();
222 226 }
223 227 if ui
224 228 .button(format!("{}Instrument (I)", active_dot(state.show_midi_window)))
225 229 .clicked()
226 230 {
227 231 state.show_midi_window = !state.show_midi_window;
228 - ui.close_menu();
232 + ui.close();
229 233 }
230 234 if ui
231 235 .button(format!("{}Loop (L)", active_dot(state.loop_enabled)))
232 236 .clicked()
233 237 {
234 238 state.toggle_loop();
235 - ui.close_menu();
239 + ui.close();
236 240 }
237 241 let filters_label = if filter_count > 0 {
238 242 format!(
@@ -245,7 +249,7 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo
245 249 };
246 250 if ui.button(filters_label).clicked() {
247 251 state.filter_panel_open = !state.filter_panel_open;
248 - ui.close_menu();
252 + ui.close();
249 253 }
250 254 });
251 255 }
@@ -376,11 +380,27 @@ fn draw_breadcrumb(
376 380 // Import + Export buttons + Sync + theme selector (right-aligned)
377 381 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
378 382 let import_id = ui.make_persistent_id("import_menu");
379 - let import_btn = ui.button("Import");
383 + // Empty-library hint: Import is the one action that does anything when
384 + // the user has no samples yet. Bold the label so it stands out from the
385 + // (functional but currently useless) Export/Sync/Settings/Help row.
386 + let library_empty = state.contents.is_empty()
387 + && state.current_dir.is_none()
388 + && state.search_query.is_empty()
389 + && !state.search_filter.is_active();
390 + let import_label = if library_empty {
391 + egui::RichText::new("Import").strong()
392 + } else {
393 + egui::RichText::new("Import")
394 + };
395 + let import_btn = ui.button(import_label);
380 396 if import_btn.clicked() {
381 - ui.memory_mut(|m| m.toggle_popup(import_id));
397 + egui::Popup::toggle_id(ui.ctx(), import_id);
382 398 }
383 - egui::popup_below_widget(ui, import_id, &import_btn, egui::PopupCloseBehavior::CloseOnClick, |ui| {
399 + egui::Popup::from_response(&import_btn)
400 + .id(import_id)
401 + .open_memory(None)
402 + .close_behavior(egui::PopupCloseBehavior::CloseOnClick)
403 + .show(|ui| {
384 404 ui.set_min_width(180.0);
385 405 // C-2: label every entry point with the action it performs.
386 406 // "Import folder..." now consistently means *the wizard* (strategy
M todo.md +25 -22