From f697f1aad9239eb62fbff91100563ec9afbe35ef Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Sat, 28 Feb 2026 22:30:15 +0000 Subject: [PATCH 1/2] feat: add collapsible stage cards with per-stage and global toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add collapse/expand functionality to stage cards so users can minimize stages they're not actively tweaking, keeping the UI compact. - Per-stage ▶/▼ toggle button in each stage header - Global "Collapse All / Expand All" button in the control bar - Collapse state synced with stage add/remove/move/preset-load - Transient UI-only state, not persisted in presets --- src/gui/app.rs | 27 ++- src/gui/components/control.rs | 11 +- src/gui/components/stage_list.rs | 5 +- src/gui/components/widgets/common.rs | 28 ++- src/gui/messages/mod.rs | 2 + src/gui/stages/compressor.rs | 127 ++++++++------ src/gui/stages/filter.rs | 78 +++++---- src/gui/stages/level.rs | 41 +++-- src/gui/stages/mod.rs | 4 +- src/gui/stages/multiband_saturator.rs | 242 +++++++++++++++----------- src/gui/stages/noise_gate.rs | 127 ++++++++------ src/gui/stages/poweramp.rs | 88 ++++++---- src/gui/stages/preamp.rs | 81 +++++---- src/gui/stages/tonestack.rs | 135 +++++++------- 14 files changed, 588 insertions(+), 408 deletions(-) diff --git a/src/gui/app.rs b/src/gui/app.rs index 6b15316..3acb9e0 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -34,6 +34,7 @@ pub struct AmplifierApp { control_bar: Control, settings: Settings, settings_handler: SettingsHandler, + collapsed_stages: Vec, dirty_chain: bool, ir_cabinet_control: IrCabinetControl, pitch_shift_control: PitchShiftControl, @@ -104,6 +105,8 @@ impl AmplifierApp { let hotkey_handler = HotkeyHandler::new(settings.hotkeys.clone()); + let collapsed_stages = vec![false; preset.stages.len()]; + ( Self { audio_manager, @@ -113,6 +116,7 @@ impl AmplifierApp { control_bar, settings, settings_handler, + collapsed_stages, // Set dirty chain to true to trigger initial rebuild dirty_chain: true, ir_cabinet_control, @@ -150,9 +154,10 @@ impl AmplifierApp { let main_content = column![ top_bar, self.preset_handler.view(), - self.stage_list.view(), + self.stage_list.view(&self.collapsed_stages), self.ir_cabinet_control.view(), - self.control_bar.view(self.is_recording), + self.control_bar + .view(self.is_recording, self.all_collapsed()), ] .spacing(10) .padding(20); @@ -226,6 +231,7 @@ impl AmplifierApp { pub fn update(&mut self, message: Message) -> Task { match message { Message::SetStages(stages) => { + self.collapsed_stages = vec![false; stages.len()]; self.stages = stages; self.mark_stages_dirty(); } @@ -233,26 +239,39 @@ impl AmplifierApp { Message::AddStage => { let new_stage = StageConfig::from(self.control_bar.selected()); self.stages.push(new_stage); + self.collapsed_stages.push(false); self.mark_stages_dirty(); } Message::RemoveStage(idx) => { if idx < self.stages.len() { self.stages.remove(idx); + self.collapsed_stages.remove(idx); self.mark_stages_dirty(); } } Message::MoveStageUp(idx) => { if idx > 0 && idx < self.stages.len() { self.stages.swap(idx - 1, idx); + self.collapsed_stages.swap(idx - 1, idx); self.mark_stages_dirty(); } } Message::MoveStageDown(idx) => { if idx + 1 < self.stages.len() { self.stages.swap(idx, idx + 1); + self.collapsed_stages.swap(idx, idx + 1); self.mark_stages_dirty(); } } + Message::ToggleStageCollapse(idx) => { + if let Some(collapsed) = self.collapsed_stages.get_mut(idx) { + *collapsed = !*collapsed; + } + } + Message::ToggleAllStagesCollapse => { + let any_expanded = self.collapsed_stages.iter().any(|&c| !c); + self.collapsed_stages.fill(any_expanded); + } Message::StageTypeSelected(stage_type) => { self.control_bar.set_selected_stage_type(stage_type); } @@ -416,6 +435,10 @@ impl AmplifierApp { Task::none() } + fn all_collapsed(&self) -> bool { + !self.collapsed_stages.is_empty() && self.collapsed_stages.iter().all(|&c| c) + } + fn any_dialog_visible(&self) -> bool { self.settings_handler.is_visible() || self.tuner_handler.is_visible() diff --git a/src/gui/components/control.rs b/src/gui/components/control.rs index 0fb2598..b4a3854 100644 --- a/src/gui/components/control.rs +++ b/src/gui/components/control.rs @@ -31,7 +31,13 @@ impl Control { self.selected_stage_type = ty; } - pub fn view(&self, is_recording: bool) -> Element<'_, Message> { + pub fn view(&self, is_recording: bool, all_collapsed: bool) -> Element<'_, Message> { + let collapse_label = if all_collapsed { + "▼ Expand All" + } else { + "▶ Collapse All" + }; + let stage_controls = row![ pick_list( STAGE_TYPES, @@ -39,6 +45,9 @@ impl Control { Message::StageTypeSelected ), button(tr!(add_stage)).on_press(Message::AddStage), + button(collapse_label) + .on_press(Message::ToggleAllStagesCollapse) + .style(iced::widget::button::secondary), ] .spacing(10) .align_y(Alignment::Center); diff --git a/src/gui/components/stage_list.rs b/src/gui/components/stage_list.rs index c6e1ba6..a4d9d1f 100644 --- a/src/gui/components/stage_list.rs +++ b/src/gui/components/stage_list.rs @@ -17,11 +17,12 @@ impl StageList { self.stages = stages.to_vec(); } - pub fn view(&self) -> Element<'_, Message> { + pub fn view(&self, collapsed: &[bool]) -> Element<'_, Message> { let mut col = column![].width(Length::Fill).padding(10); for (idx, stage) in self.stages.iter().enumerate() { - col = col.push(stage.view(idx, self.stages.len())); + let is_collapsed = collapsed.get(idx).copied().unwrap_or(false); + col = col.push(stage.view(idx, self.stages.len(), is_collapsed)); } scrollable(col).height(Length::FillPortion(9)).into() diff --git a/src/gui/components/widgets/common.rs b/src/gui/components/widgets/common.rs index bf5bce3..398355e 100644 --- a/src/gui/components/widgets/common.rs +++ b/src/gui/components/widgets/common.rs @@ -36,9 +36,21 @@ pub fn icon_button<'a>( } } -pub fn stage_header(stage_name: &str, idx: usize, total_stages: usize) -> Element<'_, Message> { +pub fn stage_header( + stage_name: &str, + idx: usize, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { let header_text = format!("{} {}", stage_name, idx + 1); + let collapse_icon = if is_collapsed { "▶" } else { "▼" }; + let collapse_btn = icon_button( + collapse_icon, + Some(Message::ToggleStageCollapse(idx)), + iced::widget::button::secondary, + ); + let move_up_btn = if idx > 0 { icon_button( "↑", @@ -65,8 +77,14 @@ pub fn stage_header(stage_name: &str, idx: usize, total_stages: usize) -> Elemen iced::widget::button::danger, ); - row![move_up_btn, move_down_btn, remove_btn, text(header_text)] - .spacing(5) - .align_y(Alignment::Center) - .into() + row![ + collapse_btn, + move_up_btn, + move_down_btn, + remove_btn, + text(header_text) + ] + .spacing(5) + .align_y(Alignment::Center) + .into() } diff --git a/src/gui/messages/mod.rs b/src/gui/messages/mod.rs index c4651e6..3c47bca 100644 --- a/src/gui/messages/mod.rs +++ b/src/gui/messages/mod.rs @@ -24,6 +24,8 @@ pub enum Message { RemoveStage(usize), MoveStageUp(usize), MoveStageDown(usize), + ToggleStageCollapse(usize), + ToggleAllStagesCollapse, StageTypeSelected(StageType), RebuildTick, SetStages(Vec), diff --git a/src/gui/stages/compressor.rs b/src/gui/stages/compressor.rs index bd10b19..df46bd1 100644 --- a/src/gui/stages/compressor.rs +++ b/src/gui/stages/compressor.rs @@ -68,69 +68,82 @@ pub enum CompressorMessage { // --- View --- -pub fn view(idx: usize, cfg: &CompressorConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_compressor), idx, total_stages); - - let body = column![ - labeled_slider( - tr!(threshold), - -60.0..=0.0, - cfg.threshold_db, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::ThresholdChanged(v)) +pub fn view( + idx: usize, + cfg: &CompressorConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_compressor), idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let body = column![ + labeled_slider( + tr!(threshold), + -60.0..=0.0, + cfg.threshold_db, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::ThresholdChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 1.0 ), - |v| format!("{v:.1} {}", tr!(db)), - 1.0 - ), - labeled_slider( - tr!(ratio), - 1.0..=20.0, - cfg.ratio, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::RatioChanged(v)) + labeled_slider( + tr!(ratio), + 1.0..=20.0, + cfg.ratio, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::RatioChanged(v)) + ), + |v| format!("{v:.1}:1"), + 0.1 ), - |v| format!("{v:.1}:1"), - 0.1 - ), - labeled_slider( - tr!(attack), - 0.1..=100.0, - cfg.attack_ms, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::AttackChanged(v)) + labeled_slider( + tr!(attack), + 0.1..=100.0, + cfg.attack_ms, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::AttackChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(ms)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(ms)), - 0.1 - ), - labeled_slider( - tr!(release), - 10.0..=1000.0, - cfg.release_ms, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::ReleaseChanged(v)) + labeled_slider( + tr!(release), + 10.0..=1000.0, + cfg.release_ms, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::ReleaseChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - labeled_slider( - tr!(makeup), - -12.0..=24.0, - cfg.makeup_db, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::MakeupChanged(v)) + labeled_slider( + tr!(makeup), + -12.0..=24.0, + cfg.makeup_db, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::MakeupChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(db)), - 0.1 - ), - ] - .spacing(5); + ] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; - container(column![header, body].spacing(5).padding(10)) + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/filter.rs b/src/gui/stages/filter.rs index c60977b..bff7fed 100644 --- a/src/gui/stages/filter.rs +++ b/src/gui/stages/filter.rs @@ -51,38 +51,54 @@ pub enum FilterMessage { const FILTER_TYPES: [FilterType; 2] = [FilterType::Highpass, FilterType::Lowpass]; -pub fn view(idx: usize, cfg: &FilterConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_filter), idx, total_stages); +pub fn view( + idx: usize, + cfg: &FilterConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_filter), idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let type_picker = row![ + text(tr!(type_label)).width(Length::FillPortion(3)), + pick_list(FILTER_TYPES, Some(cfg.filter_type), move |t| { + Message::Stage(idx, StageMessage::Filter(FilterMessage::TypeChanged(t))) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + let range = match cfg.filter_type { + FilterType::Highpass => 0.0..=1000.0, + FilterType::Lowpass => 5000.0..=15000.0, + }; + + let body = column![ + type_picker, + labeled_slider( + tr!(cutoff), + range, + cfg.cutoff_hz, + move |v| Message::Stage( + idx, + StageMessage::Filter(FilterMessage::CutoffChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(hz)), + 1.0 + ), + ] + .spacing(5); + + content = content.push(body); + } - let type_picker = row![ - text(tr!(type_label)).width(Length::FillPortion(3)), - pick_list(FILTER_TYPES, Some(cfg.filter_type), move |t| { - Message::Stage(idx, StageMessage::Filter(FilterMessage::TypeChanged(t))) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let range = match cfg.filter_type { - FilterType::Highpass => 0.0..=1000.0, - FilterType::Lowpass => 5000.0..=15000.0, - }; - - let body = column![ - type_picker, - labeled_slider( - tr!(cutoff), - range, - cfg.cutoff_hz, - move |v| Message::Stage(idx, StageMessage::Filter(FilterMessage::CutoffChanged(v))), - |v| format!("{v:.0} {}", tr!(hz)), - 1.0 - ), - ] - .spacing(5); - - container(column![header, body].spacing(5).padding(10)) + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/level.rs b/src/gui/stages/level.rs index b739bec..b3c244e 100644 --- a/src/gui/stages/level.rs +++ b/src/gui/stages/level.rs @@ -43,20 +43,33 @@ pub enum LevelMessage { // --- View --- -pub fn view(idx: usize, cfg: &LevelConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_level), idx, total_stages); - - let body = column![labeled_slider( - tr!(gain), - 0.0..=2.0, - cfg.gain, - move |v| Message::Stage(idx, StageMessage::Level(LevelMessage::GainChanged(v))), - |v| format!("{v:.2}"), - 0.05 - ),] - .spacing(5); - - container(column![header, body].spacing(5).padding(10)) +pub fn view( + idx: usize, + cfg: &LevelConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_level), idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let body = column![labeled_slider( + tr!(gain), + 0.0..=2.0, + cfg.gain, + move |v| Message::Stage(idx, StageMessage::Level(LevelMessage::GainChanged(v))), + |v| format!("{v:.2}"), + 0.05 + ),] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/mod.rs b/src/gui/stages/mod.rs index 6a7da6c..5bf1b89 100644 --- a/src/gui/stages/mod.rs +++ b/src/gui/stages/mod.rs @@ -74,9 +74,9 @@ macro_rules! stage_registry { } } - pub fn view(&self, idx: usize, total_stages: usize) -> Element<'_, Message> { + pub fn view(&self, idx: usize, total_stages: usize, is_collapsed: bool) -> Element<'_, Message> { match self { - $( StageConfig::$Variant(cfg) => $module::view(idx, cfg, total_stages), )+ + $( StageConfig::$Variant(cfg) => $module::view(idx, cfg, total_stages, is_collapsed), )+ } } } diff --git a/src/gui/stages/multiband_saturator.rs b/src/gui/stages/multiband_saturator.rs index 6a91bb7..83ffba4 100644 --- a/src/gui/stages/multiband_saturator.rs +++ b/src/gui/stages/multiband_saturator.rs @@ -87,124 +87,154 @@ pub fn view( idx: usize, cfg: &MultibandSaturatorConfig, total_stages: usize, + is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_multiband_saturator), idx, total_stages); - - let crossover_section = column![ - text(tr!(crossover)).size(14), - labeled_slider( - tr!(low_freq), - 50.0..=500.0, - cfg.low_freq, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::LowFreqChanged(v)) + let header = stage_header( + tr!(stage_multiband_saturator), + idx, + total_stages, + is_collapsed, + ); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let crossover_section = column![ + text(tr!(crossover)).size(14), + labeled_slider( + tr!(low_freq), + 50.0..=500.0, + cfg.low_freq, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowFreqChanged(v) + ) + ), + |v| format!("{v:.0} {}", tr!(hz)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(hz)), - 1.0 - ), - labeled_slider( - tr!(high_freq), - 1000.0..=6000.0, - cfg.high_freq, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::HighFreqChanged(v)) + labeled_slider( + tr!(high_freq), + 1000.0..=6000.0, + cfg.high_freq, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighFreqChanged(v) + ) + ), + |v| format!("{v:.0} {}", tr!(hz)), + 10.0 ), - |v| format!("{v:.0} {}", tr!(hz)), - 10.0 - ), - ] - .spacing(5); - - let low_band_section = column![ - text(tr!(low_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.low_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::LowDriveChanged(v)) + ] + .spacing(5); + + let low_band_section = column![ + text(tr!(low_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.low_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.low_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::LowLevelChanged(v)) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.low_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); - - let mid_band_section = column![ - text(tr!(mid_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.mid_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::MidDriveChanged(v)) + ] + .spacing(5); + + let mid_band_section = column![ + text(tr!(mid_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.mid_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::MidDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.mid_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::MidLevelChanged(v)) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.mid_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::MidLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); - - let high_band_section = column![ - text(tr!(high_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.high_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::HighDriveChanged(v)) + ] + .spacing(5); + + let high_band_section = column![ + text(tr!(high_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.high_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.high_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator(MultibandSaturatorMessage::HighLevelChanged(v)) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.high_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); + ] + .spacing(5); - let bands_row = row![low_band_section, mid_band_section, high_band_section] - .spacing(20) - .width(Length::Fill); + let bands_row = row![low_band_section, mid_band_section, high_band_section] + .spacing(20) + .width(Length::Fill); - let body = column![crossover_section, bands_row].spacing(10); + let body = column![crossover_section, bands_row].spacing(10); - container(column![header, body].spacing(5).padding(10)) + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/noise_gate.rs b/src/gui/stages/noise_gate.rs index 96274ed..8ed42bb 100644 --- a/src/gui/stages/noise_gate.rs +++ b/src/gui/stages/noise_gate.rs @@ -68,69 +68,82 @@ pub enum NoiseGateMessage { // --- View --- -pub fn view(idx: usize, cfg: &NoiseGateConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_noise_gate), idx, total_stages); - - let body = column![ - labeled_slider( - tr!(threshold), - -80.0..=0.0, - cfg.threshold_db, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::ThresholdChanged(v)) +pub fn view( + idx: usize, + cfg: &NoiseGateConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_noise_gate), idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let body = column![ + labeled_slider( + tr!(threshold), + -80.0..=0.0, + cfg.threshold_db, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::ThresholdChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 1.0 ), - |v| format!("{v:.1} {}", tr!(db)), - 1.0 - ), - labeled_slider( - tr!(ratio), - 1.0..=100.0, - cfg.ratio, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::RatioChanged(v)) + labeled_slider( + tr!(ratio), + 1.0..=100.0, + cfg.ratio, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::RatioChanged(v)) + ), + |v| format!("{v:.0}:1"), + 1.0 ), - |v| format!("{v:.0}:1"), - 1.0 - ), - labeled_slider( - tr!(attack), - 0.1..=100.0, - cfg.attack_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::AttackChanged(v)) + labeled_slider( + tr!(attack), + 0.1..=100.0, + cfg.attack_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::AttackChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(ms)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(ms)), - 0.1 - ), - labeled_slider( - tr!(hold), - 0.0..=500.0, - cfg.hold_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::HoldChanged(v)) + labeled_slider( + tr!(hold), + 0.0..=500.0, + cfg.hold_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::HoldChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - labeled_slider( - tr!(release), - 1.0..=1000.0, - cfg.release_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::ReleaseChanged(v)) + labeled_slider( + tr!(release), + 1.0..=1000.0, + cfg.release_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::ReleaseChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - ] - .spacing(5); + ] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; - container(column![header, body].spacing(5).padding(10)) + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/poweramp.rs b/src/gui/stages/poweramp.rs index ea11af9..5cda3b7 100644 --- a/src/gui/stages/poweramp.rs +++ b/src/gui/stages/poweramp.rs @@ -59,44 +59,60 @@ const POWER_AMP_TYPES: [PowerAmpType; 3] = [ PowerAmpType::ClassB, ]; -pub fn view(idx: usize, cfg: &PowerAmpConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_power_amp), idx, total_stages); +pub fn view( + idx: usize, + cfg: &PowerAmpConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_power_amp), idx, total_stages, is_collapsed); - let type_picker = row![ - text(tr!(type_label)).width(Length::FillPortion(3)), - pick_list(POWER_AMP_TYPES, Some(cfg.amp_type), move |t| { - Message::Stage(idx, StageMessage::PowerAmp(PowerAmpMessage::TypeChanged(t))) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let body = column![ - type_picker, - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.drive, - move |v| Message::Stage( - idx, - StageMessage::PowerAmp(PowerAmpMessage::DriveChanged(v)) + let mut content = column![header].spacing(5); + + if !is_collapsed { + let type_picker = row![ + text(tr!(type_label)).width(Length::FillPortion(3)), + pick_list(POWER_AMP_TYPES, Some(cfg.amp_type), move |t| { + Message::Stage(idx, StageMessage::PowerAmp(PowerAmpMessage::TypeChanged(t))) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + let body = column![ + type_picker, + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.drive, + move |v| Message::Stage( + idx, + StageMessage::PowerAmp(PowerAmpMessage::DriveChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 + ), + labeled_slider( + tr!(sag), + 0.0..=1.0, + cfg.sag, + move |v| Message::Stage( + idx, + StageMessage::PowerAmp(PowerAmpMessage::SagChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(sag), - 0.0..=1.0, - cfg.sag, - move |v| Message::Stage(idx, StageMessage::PowerAmp(PowerAmpMessage::SagChanged(v))), - |v| format!("{v:.2}"), - 0.05 - ), - ] - .spacing(5); - - container(column![header, body].spacing(5).padding(10)) + ] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/preamp.rs b/src/gui/stages/preamp.rs index 47849ce..7956620 100644 --- a/src/gui/stages/preamp.rs +++ b/src/gui/stages/preamp.rs @@ -62,41 +62,54 @@ const CLIPPER_TYPES: [ClipperType; 5] = [ ClipperType::ClassA, ]; -pub fn view(idx: usize, cfg: &PreampConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_preamp), idx, total_stages); +pub fn view( + idx: usize, + cfg: &PreampConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_preamp), idx, total_stages, is_collapsed); - let clipper_picker = row![ - text(tr!(clipper)).width(Length::FillPortion(3)), - pick_list(CLIPPER_TYPES, Some(cfg.clipper_type), move |t| { - Message::Stage(idx, StageMessage::Preamp(PreampMessage::ClipperChanged(t))) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let body = column![ - clipper_picker, - labeled_slider( - tr!(gain), - 0.0..=10.0, - cfg.gain, - move |v| Message::Stage(idx, StageMessage::Preamp(PreampMessage::GainChanged(v))), - |v| format!("{v:.1}"), - 0.1 - ), - labeled_slider( - tr!(bias), - -1.0..=1.0, - cfg.bias, - move |v| Message::Stage(idx, StageMessage::Preamp(PreampMessage::BiasChanged(v))), - |v| format!("{v:.2}"), - 0.1 - ), - ] - .spacing(5); - - container(column![header, body].spacing(5).padding(10)) + let mut content = column![header].spacing(5); + + if !is_collapsed { + let clipper_picker = row![ + text(tr!(clipper)).width(Length::FillPortion(3)), + pick_list(CLIPPER_TYPES, Some(cfg.clipper_type), move |t| { + Message::Stage(idx, StageMessage::Preamp(PreampMessage::ClipperChanged(t))) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + let body = column![ + clipper_picker, + labeled_slider( + tr!(gain), + 0.0..=10.0, + cfg.gain, + move |v| Message::Stage(idx, StageMessage::Preamp(PreampMessage::GainChanged(v))), + |v| format!("{v:.1}"), + 0.1 + ), + labeled_slider( + tr!(bias), + -1.0..=1.0, + cfg.bias, + move |v| Message::Stage(idx, StageMessage::Preamp(PreampMessage::BiasChanged(v))), + |v| format!("{v:.2}"), + 0.1 + ), + ] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() diff --git a/src/gui/stages/tonestack.rs b/src/gui/stages/tonestack.rs index 12e9de4..af82e97 100644 --- a/src/gui/stages/tonestack.rs +++ b/src/gui/stages/tonestack.rs @@ -75,72 +75,85 @@ const TONE_STACK_MODELS: [ToneStackModel; 4] = [ ToneStackModel::Flat, ]; -pub fn view(idx: usize, cfg: &ToneStackConfig, total_stages: usize) -> Element<'_, Message> { - let header = stage_header(tr!(stage_tone_stack), idx, total_stages); - - let model_picker = row![ - text(tr!(model)).width(Length::FillPortion(3)), - pick_list(TONE_STACK_MODELS, Some(cfg.model), move |m| { - Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::ModelChanged(m)), - ) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let body = column![ - model_picker, - labeled_slider( - tr!(bass), - 0.0..=2.0, - cfg.bass, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::BassChanged(v)) +pub fn view( + idx: usize, + cfg: &ToneStackConfig, + total_stages: usize, + is_collapsed: bool, +) -> Element<'_, Message> { + let header = stage_header(tr!(stage_tone_stack), idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + let model_picker = row![ + text(tr!(model)).width(Length::FillPortion(3)), + pick_list(TONE_STACK_MODELS, Some(cfg.model), move |m| { + Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::ModelChanged(m)), + ) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + let body = column![ + model_picker, + labeled_slider( + tr!(bass), + 0.0..=2.0, + cfg.bass, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::BassChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(mid), - 0.0..=2.0, - cfg.mid, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::MidChanged(v)) + labeled_slider( + tr!(mid), + 0.0..=2.0, + cfg.mid, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::MidChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(treble), - 0.0..=2.0, - cfg.treble, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::TrebleChanged(v)) + labeled_slider( + tr!(treble), + 0.0..=2.0, + cfg.treble, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::TrebleChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(presence), - 0.0..=2.0, - cfg.presence, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::PresenceChanged(v)) + labeled_slider( + tr!(presence), + 0.0..=2.0, + cfg.presence, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::PresenceChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - ] - .spacing(5); + ] + .spacing(5); + + content = content.push(body); + } + + let padding = if is_collapsed { 5 } else { 10 }; - container(column![header, body].spacing(5).padding(10)) + container(content.padding(padding)) .width(Length::Fill) .style(|theme: &iced::Theme| { container::Style::default() From 21e36feb369417c41cad9879d3515ccc2260b9c9 Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Sat, 28 Feb 2026 22:40:35 +0000 Subject: [PATCH 2/2] refactor: extract stage_card helper and add i18n for collapse labels - Extract stage_card() in common.rs to deduplicate the header/container/padding/style boilerplate across all 8 stage views - Each stage view now just provides a body closure to stage_card() - Add collapse_all/expand_all translation keys for EN and ZH_CN - Use tr!() for the collapse/expand button label in control bar --- src/gui/components/control.rs | 6 +- src/gui/components/widgets/common.rs | 29 ++- src/gui/stages/compressor.rs | 140 +++++++------- src/gui/stages/filter.rs | 28 +-- src/gui/stages/level.rs | 30 +-- src/gui/stages/multiband_saturator.rs | 266 ++++++++++++-------------- src/gui/stages/noise_gate.rs | 140 +++++++------- src/gui/stages/poweramp.rs | 100 +++++----- src/gui/stages/preamp.rs | 28 +-- src/gui/stages/tonestack.rs | 146 +++++++------- src/i18n/mod.rs | 6 + 11 files changed, 424 insertions(+), 495 deletions(-) diff --git a/src/gui/components/control.rs b/src/gui/components/control.rs index b4a3854..36cb756 100644 --- a/src/gui/components/control.rs +++ b/src/gui/components/control.rs @@ -33,9 +33,9 @@ impl Control { pub fn view(&self, is_recording: bool, all_collapsed: bool) -> Element<'_, Message> { let collapse_label = if all_collapsed { - "▼ Expand All" + format!("▼ {}", tr!(expand_all)) } else { - "▶ Collapse All" + format!("▶ {}", tr!(collapse_all)) }; let stage_controls = row![ @@ -45,7 +45,7 @@ impl Control { Message::StageTypeSelected ), button(tr!(add_stage)).on_press(Message::AddStage), - button(collapse_label) + button(text(collapse_label)) .on_press(Message::ToggleAllStagesCollapse) .style(iced::widget::button::secondary), ] diff --git a/src/gui/components/widgets/common.rs b/src/gui/components/widgets/common.rs index 398355e..4b9719b 100644 --- a/src/gui/components/widgets/common.rs +++ b/src/gui/components/widgets/common.rs @@ -1,5 +1,5 @@ use crate::gui::messages::Message; -use iced::widget::{button, row, slider, text}; +use iced::widget::{button, column, container, row, slider, text}; use iced::{Alignment, Element, Length}; pub fn labeled_slider<'a, F: 'a + Fn(f32) -> Message>( @@ -88,3 +88,30 @@ pub fn stage_header( .align_y(Alignment::Center) .into() } + +pub fn stage_card<'a>( + stage_name: &'a str, + idx: usize, + total_stages: usize, + is_collapsed: bool, + body: impl FnOnce() -> Element<'a, Message>, +) -> Element<'a, Message> { + let header = stage_header(stage_name, idx, total_stages, is_collapsed); + + let mut content = column![header].spacing(5); + + if !is_collapsed { + content = content.push(body()); + } + + let padding = if is_collapsed { 5 } else { 10 }; + + container(content.padding(padding)) + .width(Length::Fill) + .style(|theme: &iced::Theme| { + container::Style::default() + .background(theme.palette().background) + .border(iced::Border::default().rounded(5)) + }) + .into() +} diff --git a/src/gui/stages/compressor.rs b/src/gui/stages/compressor.rs index df46bd1..998bb4e 100644 --- a/src/gui/stages/compressor.rs +++ b/src/gui/stages/compressor.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container}; -use iced::{Element, Length}; +use iced::widget::column; +use iced::Element; use serde::{Deserialize, Serialize}; use crate::amp::stages::compressor::CompressorStage; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -74,81 +74,71 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_compressor), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let body = column![ - labeled_slider( - tr!(threshold), - -60.0..=0.0, - cfg.threshold_db, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::ThresholdChanged(v)) + stage_card( + tr!(stage_compressor), + idx, + total_stages, + is_collapsed, + || { + column![ + labeled_slider( + tr!(threshold), + -60.0..=0.0, + cfg.threshold_db, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::ThresholdChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 1.0 ), - |v| format!("{v:.1} {}", tr!(db)), - 1.0 - ), - labeled_slider( - tr!(ratio), - 1.0..=20.0, - cfg.ratio, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::RatioChanged(v)) + labeled_slider( + tr!(ratio), + 1.0..=20.0, + cfg.ratio, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::RatioChanged(v)) + ), + |v| format!("{v:.1}:1"), + 0.1 ), - |v| format!("{v:.1}:1"), - 0.1 - ), - labeled_slider( - tr!(attack), - 0.1..=100.0, - cfg.attack_ms, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::AttackChanged(v)) + labeled_slider( + tr!(attack), + 0.1..=100.0, + cfg.attack_ms, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::AttackChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(ms)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(ms)), - 0.1 - ), - labeled_slider( - tr!(release), - 10.0..=1000.0, - cfg.release_ms, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::ReleaseChanged(v)) + labeled_slider( + tr!(release), + 10.0..=1000.0, + cfg.release_ms, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::ReleaseChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - labeled_slider( - tr!(makeup), - -12.0..=24.0, - cfg.makeup_db, - move |v| Message::Stage( - idx, - StageMessage::Compressor(CompressorMessage::MakeupChanged(v)) + labeled_slider( + tr!(makeup), + -12.0..=24.0, + cfg.makeup_db, + move |v| Message::Stage( + idx, + StageMessage::Compressor(CompressorMessage::MakeupChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(db)), - 0.1 - ), - ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) - .into() + ] + .spacing(5) + .into() + }, + ) } diff --git a/src/gui/stages/filter.rs b/src/gui/stages/filter.rs index bff7fed..2c5331f 100644 --- a/src/gui/stages/filter.rs +++ b/src/gui/stages/filter.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container, pick_list, row, text}; +use iced::widget::{column, pick_list, row, text}; use iced::{Element, Length}; use serde::{Deserialize, Serialize}; use crate::amp::stages::filter::{FilterStage, FilterType}; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -57,11 +57,7 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_filter), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { + stage_card(tr!(stage_filter), idx, total_stages, is_collapsed, || { let type_picker = row![ text(tr!(type_label)).width(Length::FillPortion(3)), pick_list(FILTER_TYPES, Some(cfg.filter_type), move |t| { @@ -77,7 +73,7 @@ pub fn view( FilterType::Lowpass => 5000.0..=15000.0, }; - let body = column![ + column![ type_picker, labeled_slider( tr!(cutoff), @@ -91,19 +87,7 @@ pub fn view( 1.0 ), ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) + .spacing(5) .into() + }) } diff --git a/src/gui/stages/level.rs b/src/gui/stages/level.rs index b3c244e..fa88b7c 100644 --- a/src/gui/stages/level.rs +++ b/src/gui/stages/level.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container}; -use iced::{Element, Length}; +use iced::widget::column; +use iced::Element; use serde::{Deserialize, Serialize}; use crate::amp::stages::level::LevelStage; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -49,12 +49,8 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_level), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let body = column![labeled_slider( + stage_card(tr!(stage_level), idx, total_stages, is_collapsed, || { + column![labeled_slider( tr!(gain), 0.0..=2.0, cfg.gain, @@ -62,19 +58,7 @@ pub fn view( |v| format!("{v:.2}"), 0.05 ),] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) + .spacing(5) .into() + }) } diff --git a/src/gui/stages/multiband_saturator.rs b/src/gui/stages/multiband_saturator.rs index 83ffba4..d362fd7 100644 --- a/src/gui/stages/multiband_saturator.rs +++ b/src/gui/stages/multiband_saturator.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container, row, text}; +use iced::widget::{column, row, text}; use iced::{Element, Length}; use serde::{Deserialize, Serialize}; use crate::amp::stages::multiband_saturator::MultibandSaturatorStage; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -89,157 +89,141 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header( + stage_card( tr!(stage_multiband_saturator), idx, total_stages, is_collapsed, - ); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let crossover_section = column![ - text(tr!(crossover)).size(14), - labeled_slider( - tr!(low_freq), - 50.0..=500.0, - cfg.low_freq, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::LowFreqChanged(v) - ) + || { + let crossover_section = column![ + text(tr!(crossover)).size(14), + labeled_slider( + tr!(low_freq), + 50.0..=500.0, + cfg.low_freq, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowFreqChanged(v) + ) + ), + |v| format!("{v:.0} {}", tr!(hz)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(hz)), - 1.0 - ), - labeled_slider( - tr!(high_freq), - 1000.0..=6000.0, - cfg.high_freq, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::HighFreqChanged(v) - ) + labeled_slider( + tr!(high_freq), + 1000.0..=6000.0, + cfg.high_freq, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighFreqChanged(v) + ) + ), + |v| format!("{v:.0} {}", tr!(hz)), + 10.0 ), - |v| format!("{v:.0} {}", tr!(hz)), - 10.0 - ), - ] - .spacing(5); - - let low_band_section = column![ - text(tr!(low_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.low_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::LowDriveChanged(v) - ) + ] + .spacing(5); + + let low_band_section = column![ + text(tr!(low_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.low_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.low_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::LowLevelChanged(v) - ) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.low_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::LowLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); - - let mid_band_section = column![ - text(tr!(mid_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.mid_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::MidDriveChanged(v) - ) + ] + .spacing(5); + + let mid_band_section = column![ + text(tr!(mid_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.mid_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::MidDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.mid_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::MidLevelChanged(v) - ) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.mid_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::MidLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); - - let high_band_section = column![ - text(tr!(high_band)).size(14), - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.high_drive, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::HighDriveChanged(v) - ) + ] + .spacing(5); + + let high_band_section = column![ + text(tr!(high_band)).size(14), + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.high_drive, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighDriveChanged(v) + ) + ), + |v| format!("{:.0}%", v * 100.0), + 0.01 ), - |v| format!("{:.0}%", v * 100.0), - 0.01 - ), - labeled_slider( - tr!(level), - 0.0..=2.0, - cfg.high_level, - move |v| Message::Stage( - idx, - StageMessage::MultibandSaturator( - MultibandSaturatorMessage::HighLevelChanged(v) - ) + labeled_slider( + tr!(level), + 0.0..=2.0, + cfg.high_level, + move |v| Message::Stage( + idx, + StageMessage::MultibandSaturator( + MultibandSaturatorMessage::HighLevelChanged(v) + ) + ), + |v| format!("{v:.2}"), + 0.01 ), - |v| format!("{v:.2}"), - 0.01 - ), - ] - .spacing(5); + ] + .spacing(5); - let bands_row = row![low_band_section, mid_band_section, high_band_section] - .spacing(20) - .width(Length::Fill); + let bands_row = row![low_band_section, mid_band_section, high_band_section] + .spacing(20) + .width(Length::Fill); - let body = column![crossover_section, bands_row].spacing(10); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) - .into() + column![crossover_section, bands_row].spacing(10).into() + }, + ) } diff --git a/src/gui/stages/noise_gate.rs b/src/gui/stages/noise_gate.rs index 8ed42bb..49a0d64 100644 --- a/src/gui/stages/noise_gate.rs +++ b/src/gui/stages/noise_gate.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container}; -use iced::{Element, Length}; +use iced::widget::column; +use iced::Element; use serde::{Deserialize, Serialize}; use crate::amp::stages::noise_gate::NoiseGateStage; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -74,81 +74,71 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_noise_gate), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let body = column![ - labeled_slider( - tr!(threshold), - -80.0..=0.0, - cfg.threshold_db, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::ThresholdChanged(v)) + stage_card( + tr!(stage_noise_gate), + idx, + total_stages, + is_collapsed, + || { + column![ + labeled_slider( + tr!(threshold), + -80.0..=0.0, + cfg.threshold_db, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::ThresholdChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(db)), + 1.0 ), - |v| format!("{v:.1} {}", tr!(db)), - 1.0 - ), - labeled_slider( - tr!(ratio), - 1.0..=100.0, - cfg.ratio, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::RatioChanged(v)) + labeled_slider( + tr!(ratio), + 1.0..=100.0, + cfg.ratio, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::RatioChanged(v)) + ), + |v| format!("{v:.0}:1"), + 1.0 ), - |v| format!("{v:.0}:1"), - 1.0 - ), - labeled_slider( - tr!(attack), - 0.1..=100.0, - cfg.attack_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::AttackChanged(v)) + labeled_slider( + tr!(attack), + 0.1..=100.0, + cfg.attack_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::AttackChanged(v)) + ), + |v| format!("{v:.1} {}", tr!(ms)), + 0.1 ), - |v| format!("{v:.1} {}", tr!(ms)), - 0.1 - ), - labeled_slider( - tr!(hold), - 0.0..=500.0, - cfg.hold_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::HoldChanged(v)) + labeled_slider( + tr!(hold), + 0.0..=500.0, + cfg.hold_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::HoldChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - labeled_slider( - tr!(release), - 1.0..=1000.0, - cfg.release_ms, - move |v| Message::Stage( - idx, - StageMessage::NoiseGate(NoiseGateMessage::ReleaseChanged(v)) + labeled_slider( + tr!(release), + 1.0..=1000.0, + cfg.release_ms, + move |v| Message::Stage( + idx, + StageMessage::NoiseGate(NoiseGateMessage::ReleaseChanged(v)) + ), + |v| format!("{v:.0} {}", tr!(ms)), + 1.0 ), - |v| format!("{v:.0} {}", tr!(ms)), - 1.0 - ), - ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) - .into() + ] + .spacing(5) + .into() + }, + ) } diff --git a/src/gui/stages/poweramp.rs b/src/gui/stages/poweramp.rs index 5cda3b7..7da3d00 100644 --- a/src/gui/stages/poweramp.rs +++ b/src/gui/stages/poweramp.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container, pick_list, row, text}; +use iced::widget::{column, pick_list, row, text}; use iced::{Element, Length}; use serde::{Deserialize, Serialize}; use crate::amp::stages::poweramp::{PowerAmpStage, PowerAmpType}; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -65,59 +65,49 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_power_amp), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let type_picker = row![ - text(tr!(type_label)).width(Length::FillPortion(3)), - pick_list(POWER_AMP_TYPES, Some(cfg.amp_type), move |t| { - Message::Stage(idx, StageMessage::PowerAmp(PowerAmpMessage::TypeChanged(t))) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let body = column![ - type_picker, - labeled_slider( - tr!(drive), - 0.0..=1.0, - cfg.drive, - move |v| Message::Stage( - idx, - StageMessage::PowerAmp(PowerAmpMessage::DriveChanged(v)) + stage_card( + tr!(stage_power_amp), + idx, + total_stages, + is_collapsed, + || { + let type_picker = row![ + text(tr!(type_label)).width(Length::FillPortion(3)), + pick_list(POWER_AMP_TYPES, Some(cfg.amp_type), move |t| { + Message::Stage(idx, StageMessage::PowerAmp(PowerAmpMessage::TypeChanged(t))) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + column![ + type_picker, + labeled_slider( + tr!(drive), + 0.0..=1.0, + cfg.drive, + move |v| Message::Stage( + idx, + StageMessage::PowerAmp(PowerAmpMessage::DriveChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(sag), - 0.0..=1.0, - cfg.sag, - move |v| Message::Stage( - idx, - StageMessage::PowerAmp(PowerAmpMessage::SagChanged(v)) + labeled_slider( + tr!(sag), + 0.0..=1.0, + cfg.sag, + move |v| Message::Stage( + idx, + StageMessage::PowerAmp(PowerAmpMessage::SagChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) - .into() + ] + .spacing(5) + .into() + }, + ) } diff --git a/src/gui/stages/preamp.rs b/src/gui/stages/preamp.rs index 7956620..60f1d13 100644 --- a/src/gui/stages/preamp.rs +++ b/src/gui/stages/preamp.rs @@ -1,10 +1,10 @@ -use iced::widget::{column, container, pick_list, row, text}; +use iced::widget::{column, pick_list, row, text}; use iced::{Element, Length}; use serde::{Deserialize, Serialize}; use crate::amp::stages::clipper::ClipperType; use crate::amp::stages::preamp::PreampStage; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -68,11 +68,7 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_preamp), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { + stage_card(tr!(stage_preamp), idx, total_stages, is_collapsed, || { let clipper_picker = row![ text(tr!(clipper)).width(Length::FillPortion(3)), pick_list(CLIPPER_TYPES, Some(cfg.clipper_type), move |t| { @@ -83,7 +79,7 @@ pub fn view( .spacing(10) .align_y(iced::Alignment::Center); - let body = column![ + column![ clipper_picker, labeled_slider( tr!(gain), @@ -102,19 +98,7 @@ pub fn view( 0.1 ), ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) + .spacing(5) .into() + }) } diff --git a/src/gui/stages/tonestack.rs b/src/gui/stages/tonestack.rs index af82e97..00ec680 100644 --- a/src/gui/stages/tonestack.rs +++ b/src/gui/stages/tonestack.rs @@ -1,9 +1,9 @@ -use iced::widget::{column, container, pick_list, row, text}; +use iced::widget::{column, pick_list, row, text}; use iced::{Element, Length}; use serde::{Deserialize, Serialize}; use crate::amp::stages::tonestack::{ToneStackModel, ToneStackStage}; -use crate::gui::components::widgets::common::{labeled_slider, stage_header}; +use crate::gui::components::widgets::common::{labeled_slider, stage_card}; use crate::gui::messages::Message; use crate::tr; @@ -81,84 +81,74 @@ pub fn view( total_stages: usize, is_collapsed: bool, ) -> Element<'_, Message> { - let header = stage_header(tr!(stage_tone_stack), idx, total_stages, is_collapsed); - - let mut content = column![header].spacing(5); - - if !is_collapsed { - let model_picker = row![ - text(tr!(model)).width(Length::FillPortion(3)), - pick_list(TONE_STACK_MODELS, Some(cfg.model), move |m| { - Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::ModelChanged(m)), - ) - }) - .width(Length::FillPortion(7)), - ] - .spacing(10) - .align_y(iced::Alignment::Center); - - let body = column![ - model_picker, - labeled_slider( - tr!(bass), - 0.0..=2.0, - cfg.bass, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::BassChanged(v)) + stage_card( + tr!(stage_tone_stack), + idx, + total_stages, + is_collapsed, + || { + let model_picker = row![ + text(tr!(model)).width(Length::FillPortion(3)), + pick_list(TONE_STACK_MODELS, Some(cfg.model), move |m| { + Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::ModelChanged(m)), + ) + }) + .width(Length::FillPortion(7)), + ] + .spacing(10) + .align_y(iced::Alignment::Center); + + column![ + model_picker, + labeled_slider( + tr!(bass), + 0.0..=2.0, + cfg.bass, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::BassChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(mid), - 0.0..=2.0, - cfg.mid, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::MidChanged(v)) + labeled_slider( + tr!(mid), + 0.0..=2.0, + cfg.mid, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::MidChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(treble), - 0.0..=2.0, - cfg.treble, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::TrebleChanged(v)) + labeled_slider( + tr!(treble), + 0.0..=2.0, + cfg.treble, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::TrebleChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - labeled_slider( - tr!(presence), - 0.0..=2.0, - cfg.presence, - move |v| Message::Stage( - idx, - StageMessage::ToneStack(ToneStackMessage::PresenceChanged(v)) + labeled_slider( + tr!(presence), + 0.0..=2.0, + cfg.presence, + move |v| Message::Stage( + idx, + StageMessage::ToneStack(ToneStackMessage::PresenceChanged(v)) + ), + |v| format!("{v:.2}"), + 0.05 ), - |v| format!("{v:.2}"), - 0.05 - ), - ] - .spacing(5); - - content = content.push(body); - } - - let padding = if is_collapsed { 5 } else { 10 }; - - container(content.padding(padding)) - .width(Length::Fill) - .style(|theme: &iced::Theme| { - container::Style::default() - .background(theme.palette().background) - .border(iced::Border::default().rounded(5)) - }) - .into() + ] + .spacing(5) + .into() + }, + ) } diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs index 2c4d4f1..e101e69 100644 --- a/src/i18n/mod.rs +++ b/src/i18n/mod.rs @@ -126,6 +126,8 @@ pub struct Translations { // Control bar pub add_stage: &'static str, + pub collapse_all: &'static str, + pub expand_all: &'static str, pub stop_recording: &'static str, pub start_recording: &'static str, pub recording: &'static str, @@ -295,6 +297,8 @@ pub static EN: Translations = Translations { // Control bar add_stage: "Add Stage", + collapse_all: "Collapse All", + expand_all: "Expand All", stop_recording: "Stop Recording", start_recording: "Start Recording", recording: "Recording...", @@ -455,6 +459,8 @@ pub static ZH_CN: Translations = Translations { // Control bar add_stage: "添加级", + collapse_all: "全部折叠", + expand_all: "全部展开", stop_recording: "停止录音", start_recording: "开始录音", recording: "录音中...",