//! Floating Sample Forge window: chop, conform, and batch operations — the //! "maker" surface that turns a managed sample into hardware-ready material. use egui; use crate::state::{BrowserState, ChopMode}; use crate::waveform; use super::theme; use super::widgets; /// Draw the forge window. Call from the overlay layer. pub fn draw_forge_window(ctx: &egui::Context, state: &mut BrowserState) { let mut open = state.forge.show_window; widgets::tool_window(ctx, "Sample Forge", &mut open, 420.0, 340.0, |ui| { if state.forge.hash.is_none() { ui.label("Select a sample and open the forge to chop, conform, or batch-process it."); return; } if state.forge.busy { ui.horizontal(|ui| { ui.spinner(); ui.label("Working..."); }); ui.separator(); } draw_waveform_with_marks(ui, state); draw_info_line(ui, state); ui.separator(); draw_chop_section(ui, state); ui.separator(); draw_conform_section(ui, state); ui.separator(); draw_batch_section(ui, state); ui.separator(); draw_foreshadow_section(ui); }); state.forge.show_window = open; } /// Waveform with slice-boundary markers overlaid (from Preview). Uses the /// forge's own captured waveform so it stays bound to the sample being forged /// even if the file-list selection changes. fn draw_waveform_with_marks(ui: &mut egui::Ui, state: &BrowserState) { if let Some(ref waveform_data) = state.forge.waveform { let resp = waveform::draw_waveform(ui, waveform_data, None, 120.0); let rect = resp.rect; let painter = ui.painter_at(rect); for &frac in &state.forge.slice_marks { let x = rect.left() + rect.width() * frac.clamp(0.0, 1.0); painter.line_segment( [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())], egui::Stroke::new(1.0, theme::accent_yellow()), ); } } } fn draw_info_line(ui: &mut egui::Ui, state: &BrowserState) { let name = &state.forge.name; let rate = state.forge.source_rate; ui.horizontal_wrapped(|ui| { ui.label(egui::RichText::new(name).strong().size(12.0)); ui.label( egui::RichText::new(format!("{rate} Hz")) .color(theme::text_muted()) .size(11.0), ); }); } /// Chop controls: method + parameters, preview, and chop. fn draw_chop_section(ui: &mut egui::Ui, state: &mut BrowserState) { let disabled = state.forge.busy; ui.label(egui::RichText::new("Chop").strong()); ui.horizontal(|ui| { if ui .add_enabled(!disabled, egui::RadioButton::new(state.forge.chop_mode == ChopMode::Transient, "Transient")) .clicked() { state.forge.chop_mode = ChopMode::Transient; state.forge.slice_marks.clear(); } if ui .add_enabled(!disabled, egui::RadioButton::new(state.forge.chop_mode == ChopMode::Equal, "Divisions")) .clicked() { state.forge.chop_mode = ChopMode::Equal; state.forge.slice_marks.clear(); } if ui .add_enabled(!disabled, egui::RadioButton::new(state.forge.chop_mode == ChopMode::Bpm, "BPM grid")) .clicked() { state.forge.chop_mode = ChopMode::Bpm; state.forge.slice_marks.clear(); } }); match state.forge.chop_mode { ChopMode::Transient => { ui.horizontal(|ui| { ui.label("Sensitivity:"); if ui .add_enabled( !disabled, egui::Slider::new(&mut state.forge.sensitivity, 0.0..=1.0), ) .changed() { state.forge.slice_marks.clear(); } }); } ChopMode::Equal => { ui.horizontal(|ui| { ui.label("Slices:"); for n in [2usize, 4, 8, 16, 32] { if ui .add_enabled(!disabled, egui::Button::selectable(state.forge.divisions == n, n.to_string())) .clicked() { state.forge.divisions = n; state.forge.slice_marks.clear(); } } }); } ChopMode::Bpm => { ui.horizontal(|ui| { ui.label("BPM:"); if ui .add_enabled(!disabled, egui::DragValue::new(&mut state.forge.bpm).speed(0.5).range(20.0..=300.0)) .changed() { state.forge.slice_marks.clear(); } ui.label("Per beat:"); for n in [1u32, 2, 4] { let label = match n { 1 => "1/4", 2 => "1/8", _ => "1/16", }; if ui .add_enabled(!disabled, egui::Button::selectable(state.forge.subdivisions == n, label)) .clicked() { state.forge.subdivisions = n; state.forge.slice_marks.clear(); } } }); } } ui.horizontal(|ui| { if ui.add_enabled(!disabled, egui::Button::new("Preview slices")).clicked() { state.forge_preview_slices(); } let slice_count = state.forge.slice_marks.len().saturating_sub(1); let chop_label = if slice_count > 0 { format!("Chop into {slice_count} slices") } else { "Chop".to_string() }; if ui.add_enabled(!disabled, egui::Button::new(chop_label)).clicked() { state.forge_apply_chop(); } }); ui.label( egui::RichText::new("Slices are written into a new folder beside this sample.") .small() .color(theme::text_muted()), ); } /// Conform controls: pick a device, conform to its accepted format. fn draw_conform_section(ui: &mut egui::Ui, state: &mut BrowserState) { let disabled = state.forge.busy; ui.label(egui::RichText::new("Conform for device").strong()); if state.forge.devices.is_empty() { ui.label( egui::RichText::new("No device profiles available.") .small() .color(theme::text_muted()), ); return; } let selected_text = state .forge .conform_device .clone() .unwrap_or_else(|| "Select device...".to_string()); ui.horizontal(|ui| { egui::ComboBox::from_id_salt("forge_conform_device") .selected_text(selected_text) .width(200.0) .show_ui(ui, |ui| { for (name, summary) in &state.forge.devices { let label = if summary.is_empty() { name.clone() } else { format!("{name} ({summary})") }; let selected = state.forge.conform_device.as_deref() == Some(name); if ui.selectable_label(selected, label).clicked() { state.forge.conform_device = Some(name.clone()); } } }); let device = state.forge.conform_device.clone(); let can_conform = !disabled && device.is_some(); if ui.add_enabled(can_conform, egui::Button::new("Conform")).clicked() && let Some(d) = device { state.forge_conform_device(&d); } }); ui.label( egui::RichText::new("Resamples and converts bit depth to match the device, as a new sample.") .small() .color(theme::text_muted()), ); } /// Batch section: trim silence across the current multi-selection. Batch /// normalize/gain live in the Sample Editor's batch section. fn draw_batch_section(ui: &mut egui::Ui, state: &mut BrowserState) { let count = state.selected_sample_hashes().len(); ui.label(egui::RichText::new("Batch").strong()); if count < 2 { ui.label( egui::RichText::new("Select 2+ samples to batch trim silence.") .small() .color(theme::text_muted()), ); return; } ui.horizontal(|ui| { ui.label("Threshold:"); ui.add( egui::DragValue::new(&mut state.forge.trim_threshold_db) .speed(1.0) .range(-96.0..=-20.0) .suffix(" dBFS"), ); }); let threshold = state.forge.trim_threshold_db; if ui .button(format!("Trim silence on {count} samples")) .clicked() { state.batch_trim_silence(threshold); } } /// Foreshadow the CLAP/VST plugin host — the planned follow-on headline. Copy /// only; not built for this launch. fn draw_foreshadow_section(ui: &mut egui::Ui) { ui.label( egui::RichText::new("Plugin processing (CLAP/VST) — coming soon") .small() .italics() .color(theme::text_muted()), ); }