maolan 0.2.2

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use crate::message::{Message, ModulatorChange};
use crate::state::{Modulator, ModulatorRate, ModulatorShape, MusicalDivision};
use iced::{
    Alignment, Background, Border, Color, Element, Length,
    widget::{
        button, checkbox, column, container, mouse_area, pick_list, row, scrollable, slider, text,
        text_input,
    },
};

pub struct ModulatorsPane;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RateMode {
    Hz,
    Musical,
}

impl std::fmt::Display for RateMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Hz => write!(f, "Hz"),
            Self::Musical => write!(f, "Musical"),
        }
    }
}

impl ModulatorsPane {
    pub fn view<'a>(
        modulators: &'a [Modulator],
        selected_id: Option<usize>,
    ) -> Element<'a, Message> {
        let header = row![
            text("Modulators").size(16),
            button("+").on_press(Message::ModulatorAdd),
        ]
        .spacing(10)
        .align_y(Alignment::Center);

        let mut content = column![header].spacing(12);

        if modulators.is_empty() {
            content = content.push(text("No modulators. Press + to add one.").size(11));
        } else {
            for m in modulators {
                content = content.push(modulator_card(m, selected_id == Some(m.id)));
            }
        }

        container(
            column![scrollable(content).height(Length::Fill)]
                .spacing(10)
                .padding(12),
        )
        .style(|_theme| container::Style {
            border: Border {
                color: Color::from_rgba(0.34, 0.42, 0.56, 0.72),
                width: 1.0,
                ..Border::default()
            },
            ..crate::style::app_background()
        })
        .padding(12)
        .width(Length::Fixed(340.0))
        .height(Length::Fill)
        .into()
    }
}

fn modulator_card<'a>(m: &'a Modulator, selected: bool) -> Element<'a, Message> {
    let name_row = row![
        text_input("Name", &m.name)
            .on_input(|s| Message::ModulatorUpdate {
                id: m.id,
                change: ModulatorChange::Name(s),
            })
            .width(Length::Fill),
        checkbox(m.enabled).on_toggle(|v| Message::ModulatorUpdate {
            id: m.id,
            change: ModulatorChange::Enabled(v),
        }),
        button("×").on_press(Message::ModulatorRemove(m.id)),
    ]
    .spacing(8)
    .align_y(Alignment::Center);

    let shape_row = row![
        text("Shape").size(11).width(Length::Fixed(50.0)),
        pick_list(
            vec![
                ModulatorShape::Sine,
                ModulatorShape::Triangle,
                ModulatorShape::Saw,
                ModulatorShape::Square,
                ModulatorShape::SampleHold,
            ],
            Some(m.shape),
            move |s| Message::ModulatorUpdate {
                id: m.id,
                change: ModulatorChange::Shape(s),
            },
        )
        .width(Length::Fill),
    ]
    .spacing(6)
    .align_y(Alignment::Center);

    let rate_mode_row = row![
        text("Rate").size(11).width(Length::Fixed(50.0)),
        pick_list(
            vec![RateMode::Hz, RateMode::Musical],
            Some(match m.rate {
                ModulatorRate::Hz(_) => RateMode::Hz,
                ModulatorRate::Musical(_) => RateMode::Musical,
            }),
            move |mode| Message::ModulatorUpdate {
                id: m.id,
                change: match mode {
                    RateMode::Hz => ModulatorChange::Rate(ModulatorRate::Hz(match m.rate {
                        ModulatorRate::Hz(hz) => hz,
                        ModulatorRate::Musical(_) => 1.0,
                    })),
                    RateMode::Musical => {
                        ModulatorChange::Rate(ModulatorRate::Musical(MusicalDivision::Beat))
                    }
                },
            },
        )
        .width(Length::Fixed(90.0)),
    ]
    .spacing(6)
    .align_y(Alignment::Center);

    let rate_value_row: Element<'a, Message> = match m.rate {
        ModulatorRate::Hz(rate_hz) => labeled_slider(
            "",
            0.01..=20.0,
            rate_hz,
            0.01,
            |v| Message::ModulatorUpdate {
                id: m.id,
                change: ModulatorChange::Rate(ModulatorRate::Hz(v)),
            },
            "Hz",
        ),
        ModulatorRate::Musical(division) => row![
            text("").size(11).width(Length::Fixed(50.0)),
            pick_list(MusicalDivision::ALL.to_vec(), Some(division), move |div| {
                Message::ModulatorUpdate {
                    id: m.id,
                    change: ModulatorChange::Rate(ModulatorRate::Musical(div)),
                }
            },)
            .width(Length::Fill),
        ]
        .spacing(6)
        .align_y(Alignment::Center)
        .into(),
    };

    let phase_row = labeled_slider(
        "Phase",
        0.0..=1.0,
        m.phase,
        0.001,
        |v| Message::ModulatorUpdate {
            id: m.id,
            change: ModulatorChange::Phase(v),
        },
        "",
    );

    let card = container(
        column![
            name_row,
            shape_row,
            rate_mode_row,
            rate_value_row,
            phase_row,
        ]
        .spacing(8),
    )
    .style(move |_theme| container::Style {
        background: Some(Background::Color(Color::from_rgba(0.18, 0.2, 0.24, 0.6))),
        border: Border {
            color: if selected {
                Color::from_rgb(0.9, 0.75, 0.25)
            } else {
                Color::from_rgba(0.34, 0.42, 0.56, 0.72)
            },
            width: if selected { 2.0 } else { 1.0 },
            radius: 6.0.into(),
        },
        ..container::Style::default()
    })
    .padding(10);

    mouse_area(card)
        .on_press(Message::ModulatorSelect(Some(m.id)))
        .into()
}

fn labeled_slider<'a>(
    label: &'a str,
    range: std::ops::RangeInclusive<f32>,
    value: f32,
    step: f32,
    on_change: impl Fn(f32) -> Message + 'a,
    unit: &'a str,
) -> Element<'a, Message> {
    let value_text = if unit.is_empty() {
        format!("{:.3}", value)
    } else {
        format!("{:.2} {}", value, unit)
    };
    row![
        text(label).size(11).width(Length::Fixed(50.0)),
        slider(range, value, on_change)
            .step(step)
            .width(Length::Fill),
        text(value_text).size(11).width(Length::Fixed(70.0)),
    ]
    .spacing(6)
    .align_y(Alignment::Center)
    .into()
}