Skip to main content

max / audiofiles

Flip the help overlay to the described screen, and delete the second table `ui::overlays::draw_shortcuts_tab` and its twenty-six rows in seven hand-grouped arrays are gone, `draw_features_tab` with them, and so is the shortcut half of `editor::handle_keyboard`. `quasi::help::chrome` is the only shortcuts table now: the described shell binds it so the keys work, the described help lists it so the help is what the keys are, and neither reads the other's copy because there is not one. That was the whole point of the port, and this is the commit that makes it true. WHERE THE THIRTEEN WENT, and it is not "deleted". f1, f2, ctrl+t, ctrl+z, ctrl+shift+m, shift+f, shift+d, s, d, e, i, l and f are `chrome`'s, one for one, and were already bound there before this flip. WHAT STAYED IN THE HOST, and why each is not chrome. Escape walks a priority order over eight bits of app state rather than naming an address. j, k, Enter, Backspace and Space walk a list and play what is under the cursor, which is egui's own reach. Delete, cmd+A, cmd+shift+I and Tab act on the selection or the focus, and a `Binding` carries an address with no payload. `/` is the one that is a gap rather than a shape: focus belongs to the renderer and nothing in the vocabulary says "put the caret in that field". None of the eleven was dropped; `handle_keyboard`'s header is where the reason for each now lives. The overlay's three state fields went with it -- whether it is open, which tab it is on, what its filter box holds are the runtime's layer stack now -- along with `segmented_control`, which nothing called once the tabs were described, and an orphaned doc comment that had been silently documenting the test module. FOUND, NOT CAUSED, AND FILED: f1 opens the overlay and cannot close it. `pressed_binding` fires whether or not a layer is up and `Outcome::Over` stacks unconditionally, so a second press is a second copy and a second Escape. A route cannot see the layer stack, so `/help` cannot answer "already showing". The binding predates this flip; the flip only made it the sole path. quasicoherent `900865dd`. 1551 tests pass.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-23 13:40 UTC
Commit: aaf4cb20063cc8cbebf61e7fab1037179b239c0f
Parent: 9b70219
6 files changed, +65 insertions, -432 deletions
@@ -85,7 +85,6 @@
85 85 // input to the live UI underneath (P2).
86 86 let modal_active = state.overlay.pending_confirm.is_some()
87 87 || state.bulk_modal.is_some()
88 - || state.overlay.show_help
89 88 || state.vfs_modal.show_vfs_create
90 89 || state.vfs_modal.vfs_rename_target.is_some()
91 90 || state.vfs_modal.show_dir_create
@@ -114,9 +113,6 @@
114 113 }
115 114 None => {}
116 115 }
117 - if state.overlay.show_help {
118 - overlays::draw_help_overlay(ctx, state);
119 - }
120 116 // The four name modals, all of them `quasi::naming`. Which one is showing is
121 117 // still the app's own flag, so the address is built here rather than read
122 118 // off anything a route knows.
@@ -250,7 +246,33 @@
250 246 });
251 247 }
252 248
253 - /// Process keyboard shortcuts.
249 + /// The keys that are not chrome.
250 + ///
251 + /// **Thirteen shortcuts left here on 2026-08-24 and did not go missing: they
252 + /// moved to `quasi::help::chrome`,** which the described shell binds and the
253 + /// described help screen lists, so the table exists once. What this function
254 + /// held before that flip was the second copy `Binding`'s header warns about,
255 + /// twenty-six rows of it, several hundred lines from the tab that described
256 + /// them and checked against it by nothing.
257 + ///
258 + /// What stays is what a `Chrome` cannot say, and each is a different reason:
259 + ///
260 + /// - **Escape.** Not one key. It walks a priority order over eight bits of app
261 + /// state and stops at the first thing open, which is a policy about this
262 + /// app's windows rather than an address to press.
263 + /// - **`j` / `k` / Enter / Backspace / Space.** Walking a list and playing what
264 + /// is under the cursor. egui's own reach already walks its widgets, and a
265 + /// second party moving the keyboard is what `quasi_immediate::runtime`'s
266 + /// header says this renderer does not do.
267 + /// - **Delete, `cmd+A`, `cmd+shift+I`, Tab.** All four act on the selection or
268 + /// the focus, and a `Binding` carries an address with no payload. The bulk
269 + /// acts that need the ticked set reach it through their own screens.
270 + /// - **`/`.** The one absence here that is a gap rather than a shape: focus
271 + /// belongs to the renderer and nothing in the vocabulary says "put the caret
272 + /// in that field". Recorded in `quasi::help`'s header.
273 + ///
274 + /// So this is a shorter list on purpose, and it is not the shortcuts table. The
275 + /// shortcuts table is `quasi::help::chrome`, and there is one of it.
254 276 fn handle_keyboard(ctx: &egui::Context, state: &mut BrowserState) {
255 277 // Don't handle keyboard shortcuts if a text field has focus
256 278 if ctx.memory(|m| m.focused().is_some()) {
@@ -281,8 +303,6 @@
281 303 state.cancel_import_preflight();
282 304 } else if state.overlay.pending_confirm.is_some() {
283 305 state.dismiss_confirm();
284 - } else if state.overlay.show_help {
285 - state.overlay.show_help = false;
286 306 } else if matches!(
287 307 state.import_wf.import_mode,
288 308 ImportMode::ConfigureImport { .. }
@@ -311,17 +331,6 @@
311 331 return;
312 332 }
313 333
314 - if input.key_pressed(egui::Key::F1) {
315 - state.overlay.show_help = !state.overlay.show_help;
316 - }
317 -
318 - if input.key_pressed(egui::Key::F2) && state.nav.selection.count() > 1 {
319 - state.open_bulk_rename_modal();
320 - if state.bulk_modal.is_some() {
321 - state.update_rename_previews();
322 - }
323 - }
324 -
325 334 if input.key_pressed(egui::Key::Delete) {
326 335 state.confirm_delete_selected();
327 336 }
@@ -350,20 +359,6 @@
350 359 return;
351 360 }
352 361
353 - // Cmd+Z: undo
354 - if input.modifiers.command && input.key_pressed(egui::Key::Z) {
355 - state.undo();
356 - return;
357 - }
358 -
359 - // Cmd+T: bulk tag
360 - if input.modifiers.command && input.key_pressed(egui::Key::T) {
361 - if state.nav.selection.count() > 1 {
362 - state.open_bulk_tag_modal();
363 - }
364 - return;
365 - }
366 -
367 362 let shift = input.modifiers.shift;
368 363
369 364 if input.key_pressed(egui::Key::ArrowDown) || input.key_pressed(egui::Key::J) {
@@ -418,69 +413,5 @@
418 413 if input.key_pressed(egui::Key::Slash) {
419 414 state.focus_search = true;
420 415 }
421 - // "I" toggles floating MIDI/instrument window
422 - if input.key_pressed(egui::Key::I) {
423 - state.preview.show_midi_window = !state.preview.show_midi_window;
424 - }
425 - // "E" toggles floating sample editor window
426 - if input.key_pressed(egui::Key::E) {
427 - if state.edit.show_window {
428 - state.close_edit_window();
429 - } else if let Some(node) = state.selected_node()
430 - && let Some(hash) = &node.node.sample_hash
431 - {
432 - let hash = hash.clone();
433 - state.open_edit_window(&hash);
434 - }
435 - }
436 - // "F" toggles the floating Sample Forge window for the selected sample
437 - if input.key_pressed(egui::Key::F) && !shift {
438 - if state.forge.show_window {
439 - state.close_forge_window();
440 - } else if let Some(node) = state.selected_node()
441 - && let Some(hash) = &node.node.sample_hash
442 - {
443 - let hash = hash.clone();
444 - state.open_forge_window(&hash);
445 - }
446 - }
447 - // "L" toggles loop
448 - if input.key_pressed(egui::Key::L) {
449 - state.toggle_loop();
450 - }
451 - // "S" toggles sidebar
452 - if input.key_pressed(egui::Key::S) {
453 - state.toggle_sidebar();
454 - }
455 - // "D" toggles detail panel
456 - if input.key_pressed(egui::Key::D) && !shift {
457 - state.toggle_detail();
458 - }
459 - // Shift+F: find similar
460 - if shift
461 - && input.key_pressed(egui::Key::F)
462 - && let Some(node) = state.selected_node()
463 - && let Some(hash) = &node.node.sample_hash
464 - {
465 - let hash = hash.clone();
466 - state.find_similar(&hash);
467 - }
468 - // Shift+D: find duplicates
469 - if shift
470 - && input.key_pressed(egui::Key::D)
471 - && let Some(node) = state.selected_node()
472 - && let Some(hash) = &node.node.sample_hash
473 - {
474 - let hash = hash.clone();
475 - state.find_near_duplicates(&hash);
476 - }
477 - // Cmd+Shift+M: bulk move (Cmd+M alone conflicts with macOS minimize)
478 - if input.modifiers.command
479 - && input.modifiers.shift
480 - && input.key_pressed(egui::Key::M)
481 - && state.nav.selection.count() > 1
482 - {
483 - state.open_bulk_move_modal();
484 - }
485 416 });
486 417 }
@@ -10,13 +10,17 @@
10 10 //! `Binding`'s own header says what this file is for — "a help overlay that
11 11 //! lists the bindings is otherwise a second, hand-written copy of them, free to
12 12 //! drift from what the keys actually do" — and `ui::overlays::draw_shortcuts_tab`
13 - //! is that second copy, twenty-six rows of it, in seven hand-grouped arrays. The
14 - //! keys it names are handled in `editor::handle_keyboard`, several hundred lines
15 - //! away, and nothing checks that the two agree.
13 + //! *was* that second copy, twenty-six rows of it in seven hand-grouped arrays,
14 + //! naming keys handled in `editor::handle_keyboard` several hundred lines away
15 + //! with nothing checking that the two agreed.
16 16 //!
17 - //! Here [`chrome`] is the only table. The host binds it, so the keys work; the
18 - //! help screen lists it, so the help is what the keys are. Neither reads the
19 - //! other's copy because there is not one.
17 + //! **It is deleted as of 2026-08-24, and so is the shortcut half of
18 + //! `handle_keyboard`.** [`chrome`] is the only table now. The host binds it, so
19 + //! the keys work; the help screen lists it, so the help is what the keys are.
20 + //! Neither reads the other's copy because there is not one.
21 + //!
22 + //! The eleven keys `handle_keyboard` kept are the ones a `Chrome` cannot say,
23 + //! and its header is where the reason for each of them lives.
20 24 //!
21 25 //! # THE FINDING, and what it looks like answered
22 26 //!
@@ -41,7 +45,8 @@
41 45 //!
42 46 //! # What is still not here, and why
43 47 //!
44 - //! Nine of the sixteen are declared. The rest are not the guard's business:
48 + //! Nine of the sixteen are declared. The rest are not the guard's business, and
49 + //! all of them stayed in `editor::handle_keyboard` rather than being dropped:
45 50 //!
46 51 //! - **`/` to focus the search box.** Focus is the renderer's, and nothing in
47 52 //! the vocabulary says "put the caret in that field". The one shortcut here
@@ -80,6 +85,18 @@
80 85 //! answers `Outcome::Fragment`, which is the same shape the rename preview
81 86 //! landed on. Two consumers now for `63cb3462`: **a tabbed overlay is not
82 87 //! buildable without fragments, and nothing says so.**
88 + //!
89 + //! # And one this screen inherits rather than causes
90 + //!
91 + //! `f1` opens this overlay and cannot close it. `Runtime::pressed_binding`
92 + //! fires whether or not a layer is already up, and `Outcome::Over` stacks
93 + //! unconditionally, so a second press is a second copy and a second Escape. The
94 + //! shipped `F1` toggled; the described one cannot, because a route cannot see
95 + //! the layer stack and so `/help` has no way to answer "already showing".
96 + //!
97 + //! Filed as quasicoherent `900865dd` with the three shapes an answer could
98 + //! take. Untidy rather than broken, and it is what shipped: the binding
99 + //! predates this flip, which only made it the sole path to the overlay.
83 100
84 101 use quasi_router::layout::Selector;
85 102 use quasi_router::{
@@ -1875,12 +1875,6 @@
1875 1875 assert!(state.sidebar_visible);
1876 1876 }
1877 1877
1878 - #[test]
1879 - fn show_help_defaults_false() {
1880 - let (state, _dir) = make_state();
1881 - assert!(!state.overlay.show_help);
1882 - }
1883 -
1884 1878 #[test]
1885 1879 fn focus_search_defaults_false() {
1886 1880 let (state, _dir) = make_state();
@@ -706,19 +706,19 @@
706 706 pub collection_filter_name_input: String,
707 707 }
708 708
709 - /// Help/about overlay and pending-confirmation dialog state.
709 + /// About-overlay and pending-confirmation dialog state.
710 + ///
711 + /// The help overlay's three fields left on 2026-08-24 with the overlay itself:
712 + /// whether it is open, which tab it is on and what its filter box holds are all
713 + /// the described runtime's now, held in its layer stack rather than in app
714 + /// state. See `quasi::help`.
710 715 #[derive(Default)]
711 716 pub struct OverlayUiState {
712 - pub show_help: bool,
713 - /// Help overlay tab: 0 = Shortcuts, 1 = Features.
714 - pub help_tab: u8,
715 717 /// Set by the toolbar's Help menu when the user picks "About". The app
716 718 /// layer polls this each frame and flips its own `show_about`. Lives in
717 719 /// browser state (not app state) because the browser owns the toolbar.
718 720 pub about_requested: bool,
719 721 pub pending_confirm: Option<ConfirmAction>,
720 - /// M-2: search input on the Shortcuts help tab; filters the grid live.
721 - pub help_shortcut_search: String,
722 722 }
723 723
724 724 /// VFS/directory create + rename modal inputs and targets.
@@ -1,256 +1,17 @@
1 - //! Overlay windows: help dialog, delete confirmation, and bulk operation modals.
1 + //! Overlay windows: the delete confirmation, and what is left around it.
2 + //!
3 + //! **The help dialog left on 2026-08-24** for `quasi::help`, and it took the
4 + //! app's second shortcuts table with it. What stood here was twenty-six rows in
5 + //! seven hand-grouped arrays, describing keys handled in `editor::handle_keyboard`
6 + //! with nothing checking that the two agreed; the described screen lists
7 + //! `quasi::help::chrome`, which is the same table the shell binds, so they
8 + //! cannot disagree.
2 9
3 - use super::theme;
4 10 use super::widgets::{self, ConfirmOutcome, ConfirmSpec};
5 11 use egui;
6 12
7 13 use crate::state::{BrowserState, ConfirmAction};
8 14
9 - // p-5: render platform-correct modifier name in shortcut copy. Sticking with
10 - // the text "Cmd" rather than the glyph keeps us inside the unicode allowlist.
11 - fn cmd_key() -> &'static str {
12 - #[cfg(target_os = "macos")]
13 - return "Cmd";
14 - #[cfg(not(target_os = "macos"))]
15 - return "Ctrl";
16 - }
17 -
18 - /// Draw the help overlay showing keyboard shortcuts and feature guide.
19 - pub fn draw_help_overlay(ctx: &egui::Context, state: &mut BrowserState) {
20 - // M-2/M-3: shuttle show_help via a local so the closure can borrow state
21 - // freely for the tab body (which now reads/writes other fields).
22 - let mut open = state.overlay.show_help;
23 - widgets::modal_window_with_open(
24 - ctx,
25 - "audiofiles",
26 - Some(&mut open),
27 - false,
28 - Some(400.0),
29 - |ui| {
30 - // m-12: the segmented control gives the two tabs a stronger affordance
31 - // contract than bare selectable_value (which reads as radio).
32 - if let Some(next) = widgets::segmented_control(
33 - ui,
34 - &state.overlay.help_tab,
35 - &[
36 - (0u8, "Shortcuts", "Keyboard shortcuts"),
37 - (1u8, "Features", "Feature guide"),
38 - ],
39 - ) {
40 - state.overlay.help_tab = next;
41 - }
42 - ui.separator();
43 -
44 - if state.overlay.help_tab == 0 {
45 - draw_shortcuts_tab(ui, state);
46 - } else {
47 - draw_features_tab(ui, state);
48 - }
49 - },
50 - );
51 - // M-3: a link click inside the body may have closed the help dialog
52 - // already. Honour both close paths; the X-button win is `open == false`.
53 - if state.overlay.show_help {
54 - state.overlay.show_help = open;
55 - }
56 - }
57 -
58 - // M-2: shortcut groups with muted section labels + live substring filter.
59 - // Group order is roughly first-encounter readable: navigation first, then
60 - // selection, then progressively heavier ops.
61 - fn draw_shortcuts_tab(ui: &mut egui::Ui, state: &mut BrowserState) {
62 - // M-2: search input at the top filters both columns case-insensitively.
63 - ui.horizontal(|ui| {
64 - ui.label("Filter:");
65 - widgets::text_field(
66 - ui,
67 - egui::TextEdit::singleline(&mut state.overlay.help_shortcut_search)
68 - .hint_text("e.g. tag, search, Cmd")
69 - .desired_width(220.0),
70 - );
71 - if !state.overlay.help_shortcut_search.is_empty() && ui.small_button("Clear").clicked() {
72 - state.overlay.help_shortcut_search.clear();
73 - }
74 - });
75 - ui.add_space(theme::space::bound());
76 -
77 - let query = state.overlay.help_shortcut_search.trim().to_lowercase();
78 - // p-5: build chord strings with the platform modifier name. m-2 audit:
79 - // slashes for alternative bindings, plus for chord (hold both) bindings.
80 - let cmd = cmd_key();
81 - let nav: &[(String, &str)] = &[
82 - ("j / Down".to_string(), "Move down"),
83 - ("k / Up".to_string(), "Move up"),
84 - ("Enter / Right".to_string(), "Open / preview"),
85 - (
86 - "Backspace / Left".to_string(),
87 - "Go up / back one similarity step",
88 - ),
89 - ("Space".to_string(), "Play / pause"),
90 - ];
91 - let selection: &[(String, &str)] = &[
92 - (format!("{cmd}+A"), "Select all"),
93 - ("Shift+Click".to_string(), "Range select"),
94 - (format!("{cmd}+Click"), "Toggle select"),
95 - ];
96 - let bulk: &[(String, &str)] = &[
97 - (format!("{cmd}+T"), "Bulk tag (multi-select)"),
98 - (format!("{cmd}+Shift+M"), "Bulk move (multi-select)"),
99 - ("F2".to_string(), "Bulk rename (multi-select)"),
100 - ("Delete".to_string(), "Delete selected"),
101 - ];
102 - let search: &[(String, &str)] = &[("/".to_string(), "Focus search")];
103 - let discovery: &[(String, &str)] = &[
104 - ("Shift+F".to_string(), "Find similar samples"),
105 - ("Shift+D".to_string(), "Find duplicates"),
106 - ];
107 - let toggles: &[(String, &str)] = &[
108 - ("E".to_string(), "Toggle sample editor"),
109 - (
110 - "F".to_string(),
111 - "Toggle sample forge (chop / conform / batch)",
112 - ),
113 - ("I".to_string(), "Toggle instrument panel"),
114 - ("L".to_string(), "Toggle loop"),
115 - ("S".to_string(), "Toggle sidebar"),
116 - ("D".to_string(), "Toggle detail panel"),
117 - ];
118 - let system: &[(String, &str)] = &[
119 - ("F1".to_string(), "Toggle this help"),
120 - ("Escape".to_string(), "Close dialog / clear search"),
121 - (format!("{cmd}+Z"), "Undo last bulk action"),
122 - ];
123 - let groups: &[(&str, &[(String, &str)])] = &[
124 - ("Navigation", nav),
125 - ("Selection", selection),
126 - ("Bulk", bulk),
127 - ("Search", search),
128 - ("Discovery", discovery),
129 - ("Toggles", toggles),
130 - ("System", system),
131 - ];
132 -
133 - let matches = |key: &str, desc: &str| -> bool {
134 - if query.is_empty() {
135 - return true;
136 - }
137 - key.to_lowercase().contains(&query) || desc.to_lowercase().contains(&query)
138 - };
139 -
140 - egui::ScrollArea::vertical()
141 - .max_height(420.0)
142 - .show(ui, |ui| {
143 - for (group_name, rows) in groups {
144 - let visible: Vec<&(String, &str)> =
145 - rows.iter().filter(|(k, d)| matches(k, d)).collect();
146 - if visible.is_empty() {
147 - continue;
148 - }
149 - ui.add_space(theme::space::bound());
150 - ui.label(
151 - egui::RichText::new(*group_name)
152 - .small()
153 - .strong()
154 - .color(theme::content_muted()),
155 - );
156 - egui::Grid::new(format!("shortcuts_{group_name}")).show(ui, |ui| {
157 - for (key, desc) in visible {
158 - ui.label(key.as_str());
159 - ui.label(*desc);
160 - ui.end_row();
161 - }
162 - });
163 - }
164 - });
165 - }
166 -
167 - // M-3: shortcut references in feature copy render as clickable links that
168 - // close the help dialog and dispatch the underlying action. Mouse-only or
169 - // data-dependent flows (right-click menus) stay as plain text.
170 - fn draw_features_tab(ui: &mut egui::Ui, state: &mut BrowserState) {
171 - egui::ScrollArea::vertical().max_height(400.0).show(ui, |ui| {
172 - ui.heading("Search & Filter");
173 - ui.horizontal_wrapped(|ui| {
174 - ui.label("Use");
175 - // M-3: focus the search bar.
176 - if ui.link("/").clicked() {
177 - state.overlay.show_help = false;
178 - state.focus_search = true;
179 - }
180 - ui.label("to focus the search bar. Filter by BPM range, duration, loudness, key, and tags from the filter panel (hamburger icon). Save any filter combination as a dynamic collection.");
181 - });
182 - ui.add_space(theme::space::peer());
183 -
184 - ui.heading("Collections");
185 - ui.label("Manual collections: right-click samples \u{2192} Add to Collection. Dynamic collections: set filters, then click Save. Dynamic collections update automatically when new samples match.");
186 - ui.add_space(theme::space::peer());
187 -
188 - ui.heading("Tags");
189 - ui.horizontal_wrapped(|ui| {
190 - ui.label("Use dot-notation for hierarchy (e.g. drums.kick, genre.house). Filter by tag in the sidebar tag tree. Bulk-tag with");
191 - // M-3: opens the bulk tag modal for the current selection. If
192 - // nothing is selected, open_bulk_tag_modal is a no-op (it early-
193 - // returns on empty hashes), which is the right behaviour.
194 - if ui.link(format!("{}+T", cmd_key())).clicked() {
195 - state.overlay.show_help = false;
196 - state.open_bulk_tag_modal();
197 - }
198 - ui.label(". Tag suggestions appear in the detail panel, from similar samples you have already tagged.");
199 - });
200 - ui.add_space(theme::space::peer());
201 -
202 - ui.heading("Import");
203 - ui.label("Quick Import: choose a folder and audiofiles indexes + analyzes everything. Files stay where they are (not copied). Duplicates are auto-skipped via content hashing.");
204 - ui.add_space(theme::space::peer());
205 -
206 - ui.heading("Export");
207 - ui.label("Export to hardware samplers with device profiles (SP-404, Digitakt, MPC, etc.). Profiles auto-set format, sample rate, naming rules. Or export manually with custom settings.");
208 - ui.add_space(theme::space::peer());
209 -
210 - ui.heading("Instrument / MIDI");
211 - ui.horizontal_wrapped(|ui| {
212 - ui.label("Press");
213 - // M-3: toggles the floating MIDI/instrument window.
214 - if ui.link("I").clicked() {
215 - state.overlay.show_help = false;
216 - state.preview.show_midi_window = !state.preview.show_midi_window;
217 - }
218 - ui.label("to open the instrument panel. Right-click a sample \u{2192} Play as Instrument to load it. Click piano keys to play chromatically. Right-click a key to set the root note. Connect a MIDI controller for external playback.");
219 - });
220 - ui.add_space(theme::space::peer());
221 -
222 - ui.heading("Sample Editor");
223 - ui.horizontal_wrapped(|ui| {
224 - ui.label("Press");
225 - // M-3: open the sample editor for the focused sample if any.
226 - if ui.link("E").clicked() {
227 - state.overlay.show_help = false;
228 - // M-3: mirror toolbar's Edit toggle, open the editor for the
229 - // currently selected sample if any.
230 - if state.edit.show_window {
231 - state.close_edit_window();
232 - } else if let Some(node) = state.selected_node()
233 - && let Some(hash) = node.node.sample_hash.clone() {
234 - state.open_edit_window(&hash);
235 - }
236 - }
237 - ui.label("to open the editor. Trim, normalize (peak/LUFS), gain, reverse, fade in/out. Select multiple samples for batch normalize/gain/reverse. Result mode: replace original or create sibling.");
238 - });
239 - ui.add_space(theme::space::peer());
240 -
241 - ui.heading("Drag & Drop");
242 - ui.label("Drag samples from the file list directly into your DAW or Finder/Explorer. Drop audio files or folders onto the window to import them.");
243 - ui.add_space(theme::space::peer());
244 -
245 - ui.heading("Cloud Sync");
246 - ui.label("Sync metadata (tags, organization) across devices. Set up in the Sync panel (toolbar). Metadata sync is free. Blob sync (sample files) is tiered by storage.");
247 - ui.add_space(theme::space::peer());
248 -
249 - ui.heading("System Tray");
250 - ui.label("audiofiles runs in the system tray when you close the window. Right-click the tray icon for Show Window and Quit. Playback continues in the background while minimized.");
251 - });
252 - }
253 -
254 15 /// Draw the delete confirmation dialog.
255 16 pub fn draw_confirm_dialog(ctx: &egui::Context, state: &mut BrowserState) {
256 17 // Per-variant prompt, confirm label, detail, and danger styling. Most
@@ -371,18 +132,3 @@
371 132 ConfirmOutcome::None => {}
372 133 }
373 134 }
374 -
375 - /// Draw the Quick-Import preflight: confirms the file count and size with the
376 - /// user before any files are touched. Triggered only for large imports
377 - #[cfg(test)]
378 - mod tests {
379 - use super::*;
380 -
381 - #[test]
382 - fn cmd_key_matches_platform() {
383 - #[cfg(target_os = "macos")]
384 - assert_eq!(cmd_key(), "Cmd");
385 - #[cfg(not(target_os = "macos"))]
386 - assert_eq!(cmd_key(), "Ctrl");
387 - }
388 - }
@@ -762,61 +762,6 @@
762 762 response.on_hover_text(tooltip).clicked()
763 763 }
764 764
765 - /// Mutually-exclusive segmented control.
766 - ///
767 - /// `options` is a list of `(value, label, tooltip)` triples. Returns
768 - /// `Some(value)` if a non-current option was clicked, `None` otherwise.
769 - /// Caller assigns the returned value to its state.
770 - ///
771 - /// The segments abut and each carries its own bevel, so the control reads as
772 - /// one object with one part pushed in. Two spaced pills read as two unrelated
773 - /// buttons that happen to sit near each other, which is what this was before
774 - /// and why nothing about it said "pick exactly one of these".
775 - pub fn segmented_control<T: Clone + PartialEq>(
776 - ui: &mut egui::Ui,
777 - current: &T,
778 - options: &[(T, &str, &str)],
779 - ) -> Option<T> {
780 - let mut chosen = None;
781 - ui.horizontal(|ui| {
782 - // No gap between segments. The gap is the whole difference between a
783 - // segmented control and a row of buttons.
784 - ui.spacing_mut().item_spacing.x = 0.0;
785 -
786 - for (value, label, tooltip) in options {
787 - let is_active = value == current;
788 - let (fill, text) = if is_active {
789 - (theme::surface_sunken(), theme::content())
790 - } else {
791 - (theme::surface_raised(), theme::content_secondary())
792 - };
793 -
794 - let resp = ui
795 - .add(
796 - egui::Button::new(egui::RichText::new(*label).color(text))
797 - .corner_radius(theme::radius_container())
798 - .fill(fill),
799 - )
800 - .on_hover_text(*tooltip);
801 -
802 - // The selected segment is held in, and a segment under a held
803 - // pointer shows the same thing. One rule, two reasons to be inset:
804 - // this is what the light model buys over a colour swap.
805 - let kind = if is_active {
806 - theme::bevel::Bevel::Inset
807 - } else {
808 - press_bevel(&resp)
809 - };
810 - theme::bevel::paint(ui.painter(), resp.rect, kind);
811 -
812 - if resp.clicked() && !is_active {
813 - chosen = Some(value.clone());
814 - }
815 - }
816 - });
817 - chosen
818 - }
819 -
820 765 // --- Button hierarchy --------------------------------------------------------
821 766 //
822 767 // Three button weights: