max / audiofiles
1 file changed,
+55 insertions,
-26 deletions
| @@ -13,21 +13,23 @@ use super::widgets; | |||
| 13 | 13 | pub fn draw_edit_window(ctx: &egui::Context, state: &mut BrowserState) { | |
| 14 | 14 | let mut open = state.edit.show_window; | |
| 15 | 15 | widgets::tool_window(ctx, "Sample Editor", &mut open, 400.0, 320.0, |ui| { | |
| 16 | - | // In-progress overlay | |
| 16 | + | // In-progress bar. Drawn at the top while an edit applies; the body | |
| 17 | + | // below stays rendered (every section greys itself out via its own | |
| 18 | + | // `in_progress` disabled flag) so the user keeps the waveform and | |
| 19 | + | // controls for spatial reference instead of the panel collapsing to a | |
| 20 | + | // lone spinner. | |
| 17 | 21 | if state.edit.in_progress { | |
| 18 | - | ui.vertical_centered(|ui| { | |
| 19 | - | ui.add_space(theme::space::MD); | |
| 22 | + | ui.horizontal(|ui| { | |
| 20 | 23 | ui.spinner(); | |
| 21 | 24 | ui.label("Applying edit..."); | |
| 22 | 25 | // M-11: best-effort cancel. Signals the worker and clears | |
| 23 | 26 | // in_progress so the UI is interactive even if the worker | |
| 24 | 27 | // is mid-write — the cancel is advisory, not synchronous. | |
| 25 | - | ui.add_space(theme::space::SM); | |
| 26 | 28 | if ui.button("Cancel").clicked() { | |
| 27 | 29 | state.cancel_edit_operation(); | |
| 28 | 30 | } | |
| 29 | 31 | }); | |
| 30 | - | return; | |
| 32 | + | ui.separator(); | |
| 31 | 33 | } | |
| 32 | 34 | ||
| 33 | 35 | // Result prompt overlay | |
| @@ -43,6 +45,7 @@ pub fn draw_edit_window(ctx: &egui::Context, state: &mut BrowserState) { | |||
| 43 | 45 | ||
| 44 | 46 | draw_waveform_section(ui, state, &hash); | |
| 45 | 47 | draw_info_line(ui, state); | |
| 48 | + | draw_transport_section(ui, state, &hash); | |
| 46 | 49 | ||
| 47 | 50 | ui.separator(); | |
| 48 | 51 | draw_trim_section(ui, state); | |
| @@ -141,28 +144,54 @@ fn draw_waveform_section(ui: &mut egui::Ui, state: &mut BrowserState, hash: &str | |||
| 141 | 144 | } | |
| 142 | 145 | } | |
| 143 | 146 | ||
| 144 | - | // Click-to-seek | |
| 147 | + | // Click-to-seek. If this sample isn't the active preview yet, start it | |
| 148 | + | // first so a click in the editor auditions from the clicked point — | |
| 149 | + | // previously the click was a silent no-op until preview was started | |
| 150 | + | // elsewhere. | |
| 145 | 151 | if resp.clicked() { | |
| 146 | 152 | if let Some(pos) = resp.interact_pointer_pos() { | |
| 147 | 153 | let rect = resp.rect; | |
| 148 | 154 | let normalized = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0); | |
| 149 | - | if state.previewing_hash.as_deref() == Some(hash) { | |
| 150 | - | let mut playback = state.shared.preview.lock(); | |
| 151 | - | if let Some(ref buf) = playback.buffer { | |
| 152 | - | let total_frames = if playback.streaming { | |
| 153 | - | playback.total_frames_estimate.unwrap_or(playback.decoded_frames) | |
| 154 | - | } else { | |
| 155 | - | buf.data.len() / 2 | |
| 156 | - | }; | |
| 157 | - | playback.position_frac = (normalized as f64 * total_frames as f64) | |
| 158 | - | .min((playback.decoded_frames.max(1) - 1) as f64); | |
| 159 | - | } | |
| 155 | + | if state.previewing_hash.as_deref() != Some(hash) { | |
| 156 | + | state.trigger_preview(hash); | |
| 157 | + | } | |
| 158 | + | let mut playback = state.shared.preview.lock(); | |
| 159 | + | if let Some(ref buf) = playback.buffer { | |
| 160 | + | let total_frames = if playback.streaming { | |
| 161 | + | playback.total_frames_estimate.unwrap_or(playback.decoded_frames) | |
| 162 | + | } else { | |
| 163 | + | buf.data.len() / 2 | |
| 164 | + | }; | |
| 165 | + | playback.position_frac = (normalized as f64 * total_frames as f64) | |
| 166 | + | .min((playback.decoded_frames.max(1) - 1) as f64); | |
| 160 | 167 | } | |
| 161 | 168 | } | |
| 162 | 169 | } | |
| 163 | 170 | } | |
| 164 | 171 | } | |
| 165 | 172 | ||
| 173 | + | /// Transport row: Play/Pause/Stop for the sample being edited, independent of | |
| 174 | + | /// the main file-list selection so the user can audition before committing a | |
| 175 | + | /// destructive edit. | |
| 176 | + | fn draw_transport_section(ui: &mut egui::Ui, state: &mut BrowserState, hash: &str) { | |
| 177 | + | let is_current = state.previewing_hash.as_deref() == Some(hash); | |
| 178 | + | let playing = is_current && state.shared.preview.lock().playing; | |
| 179 | + | ui.horizontal(|ui| { | |
| 180 | + | if widgets::secondary_button(ui, if playing { "Pause" } else { "Play" }).clicked() { | |
| 181 | + | if is_current { | |
| 182 | + | // Toggle play/pause on the already-loaded buffer. | |
| 183 | + | let mut pb = state.shared.preview.lock(); | |
| 184 | + | pb.playing = !pb.playing; | |
| 185 | + | } else { | |
| 186 | + | state.trigger_preview(hash); | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | if widgets::secondary_button(ui, "Stop").clicked() { | |
| 190 | + | state.stop_preview(); | |
| 191 | + | } | |
| 192 | + | }); | |
| 193 | + | } | |
| 194 | + | ||
| 166 | 195 | /// Draw a draggable trim-boundary handle over the waveform at `frac` (0..1). | |
| 167 | 196 | /// Returns `(new_frac, dragged)`. The hit area is wider than the painted line | |
| 168 | 197 | /// (Fitts) so it is easy to grab, the cursor switches to a horizontal resize on | |
| @@ -278,7 +307,7 @@ fn draw_levels_section(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 278 | 307 | ui.horizontal(|ui| { | |
| 279 | 308 | ui.label("Gain:"); | |
| 280 | 309 | ui.add_enabled(!disabled, egui::Slider::new(&mut state.edit.gain_db, -24.0..=24.0).suffix(" dB")); | |
| 281 | - | if ui.add_enabled(!disabled, egui::Button::new("Apply")).clicked() { | |
| 310 | + | if ui.add_enabled(!disabled, egui::Button::new("Apply gain")).clicked() { | |
| 282 | 311 | state.apply_edit_gain(); | |
| 283 | 312 | } | |
| 284 | 313 | }); | |
| @@ -308,7 +337,7 @@ fn draw_levels_section(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 308 | 337 | ||
| 309 | 338 | ui.horizontal(|ui| { | |
| 310 | 339 | ui.add_enabled(!disabled, egui::Slider::new(&mut state.edit.norm_target, norm_range).suffix(norm_suffix)); | |
| 311 | - | if ui.add_enabled(!disabled, egui::Button::new("Apply")).clicked() { | |
| 340 | + | if ui.add_enabled(!disabled, egui::Button::new("Normalize")).clicked() { | |
| 312 | 341 | state.apply_edit_normalize(); | |
| 313 | 342 | } | |
| 314 | 343 | }); | |
| @@ -356,7 +385,7 @@ fn draw_transform_section(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 356 | 385 | ui.selectable_value(&mut state.edit.fade_curve, FadeCurve::Logarithmic, "Log"); | |
| 357 | 386 | ui.selectable_value(&mut state.edit.fade_curve, FadeCurve::SCurve, "S-Curve"); | |
| 358 | 387 | }); | |
| 359 | - | if ui.add_enabled(!disabled, egui::Button::new("Apply")).clicked() { | |
| 388 | + | if ui.add_enabled(!disabled, egui::Button::new("Apply fade")).clicked() { | |
| 360 | 389 | state.apply_edit_fade(); | |
| 361 | 390 | } | |
| 362 | 391 | }); | |
| @@ -521,12 +550,12 @@ fn draw_result_section(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 521 | 550 | // points the user at Create sibling as the non-destructive default for | |
| 522 | 551 | // workflows that want to keep the original in the VFS. | |
| 523 | 552 | if matches!(state.edit.result_mode, Some(EditResultMode::Replace)) { | |
| 524 | - | ui.label( | |
| 525 | - | egui::RichText::new( | |
| 526 | - | "Replace mode: the original is removed from this vault. Use Create sibling to keep both, or click Undo on the next line within 10s to revert.", | |
| 527 | - | ) | |
| 528 | - | .small() | |
| 529 | - | .color(theme::accent_yellow()), | |
| 553 | + | // Promoted from small yellow footnote to a framed warning banner — the | |
| 554 | + | // consequence (original removed from the vault) is important enough that | |
| 555 | + | // the footnote style under-sold it. | |
| 556 | + | widgets::warning_banner( | |
| 557 | + | ui, | |
| 558 | + | "Replace mode: the original is removed from this vault. Use Create sibling to keep both, or click Undo on the next line within 10s to revert.", | |
| 530 | 559 | ); | |
| 531 | 560 | } | |
| 532 | 561 |