maolan 0.2.3

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use crate::message::{ApplyTemplate, Message};
use crate::state::State;
use iced::{
    Alignment, Border, Color, Element, Length,
    widget::{button, column, container, pick_list, row, text},
};

#[derive(Debug)]
pub struct ApplyTemplateView {
    state: State,
}

impl ApplyTemplateView {
    pub fn new(state: State) -> Self {
        Self { state }
    }

    pub fn update(&mut self, message: &Message) {
        if let Message::ApplyTemplate(ApplyTemplate::TemplateSelected(template)) = message
            && let Some(dialog) = &mut self.state.blocking_write().apply_template_dialog
        {
            dialog.selected_template = Some(template.clone());
        }
    }

    fn load_track_template_config(template_name: &str) -> Option<(usize, usize, usize, usize)> {
        use std::fs::File;
        use std::io::BufReader;

        let home = std::env::var("HOME").ok()?;
        let template_path = format!(
            "{}/.config/maolan/track_templates/{}/track.json",
            home, template_name
        );

        let file = File::open(template_path).ok()?;
        let reader = BufReader::new(file);
        let json: serde_json::Value = serde_json::from_reader(reader).ok()?;

        let track = json.get("track")?;
        let audio = track.get("audio")?;
        let midi = track.get("midi")?;

        let audio_ins = audio.get("ins")?.as_u64()? as usize;
        let audio_outs = audio.get("outs")?.as_u64()? as usize;
        let midi_ins = midi.get("ins")?.as_u64()? as usize;
        let midi_outs = midi.get("outs")?.as_u64()? as usize;

        Some((audio_ins, audio_outs, midi_ins, midi_outs))
    }

    pub fn view(&self) -> Element<'_, Message> {
        let state = self.state.blocking_read();
        let Some(dialog) = &state.apply_template_dialog else {
            return container("").into();
        };

        let target_track = state.tracks.iter().find(|t| t.name == dialog.track_name);
        let target_is_folder = target_track.is_some_and(|t| t.is_folder);
        let target_label = if target_is_folder {
            format!("Folder: {}", dialog.track_name)
        } else {
            format!("Track: {}", dialog.track_name)
        };

        let selected_display = dialog.selected_template.as_deref().unwrap_or("");

        let template_options: Vec<String> = if target_is_folder {
            dialog.available_folder_templates.clone()
        } else {
            dialog.available_templates.clone()
        };

        let effective_selected = dialog
            .selected_template
            .as_deref()
            .filter(|t| {
                if target_is_folder {
                    crate::gui::is_track_template_folder(t)
                } else {
                    !crate::gui::is_track_template_folder(t)
                }
            })
            .unwrap_or("")
            .to_string();

        let mut info_text = String::new();
        let mut can_apply = !effective_selected.is_empty();

        if let Some(track) = target_track {
            if let Some((t_audio_ins, t_audio_outs, t_midi_ins, t_midi_outs)) =
                Self::load_track_template_config(&effective_selected)
            {
                let existing = (
                    track.primary_audio_ins(),
                    track.primary_audio_outs(),
                    track.midi.ins,
                    track.midi.outs,
                );
                let template = (t_audio_ins, t_audio_outs, t_midi_ins, t_midi_outs);
                let compatible = existing == template;
                can_apply = compatible;
                info_text = format!(
                    "Template: audio {} in / {} out, midi {} in / {} out\n{}: audio {} in / {} out, midi {} in / {} out\n{}",
                    template.0,
                    template.1,
                    template.2,
                    template.3,
                    if target_is_folder { "Folder" } else { "Track" },
                    existing.0,
                    existing.1,
                    existing.2,
                    existing.3,
                    if compatible {
                        "Compatible"
                    } else {
                        "Incompatible input/output counts"
                    }
                );
            } else if !effective_selected.is_empty() {
                info_text = "Unable to read track template".to_string();
                can_apply = false;
            }
        } else {
            info_text = "Target track not found".to_string();
            can_apply = false;
        }

        if !selected_display.is_empty() && selected_display != effective_selected {
            info_text = "Selected template is not compatible with this track type".to_string();
            can_apply = false;
        }

        let apply_button = if can_apply {
            button("Apply").on_press(Message::ApplyTemplate(ApplyTemplate::Submit))
        } else {
            button("Apply")
        };

        let pick = pick_list(
            template_options,
            if effective_selected.is_empty() {
                None
            } else {
                Some(effective_selected.clone())
            },
            |template| Message::ApplyTemplate(ApplyTemplate::TemplateSelected(template)),
        )
        .placeholder("Select a template...")
        .width(Length::Fixed(240.0));

        let col = column![
            text(target_label).size(16),
            row![text("Template:"), pick].spacing(10),
            text(info_text).size(12),
            row![
                apply_button,
                button("Cancel")
                    .on_press(Message::Cancel)
                    .style(button::secondary)
            ]
            .spacing(10),
        ]
        .spacing(10)
        .align_x(Alignment::End);

        container(
            column![text("Apply Template"), col]
                .spacing(10)
                .align_x(Alignment::End),
        )
        .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(360.0))
        .height(Length::Fill)
        .into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use tokio::sync::RwLock;

    #[test]
    fn new_creates_view() {
        let state = Arc::new(RwLock::new(crate::state::StateData::default()));
        let _view = ApplyTemplateView::new(state);
    }

    #[test]
    fn update_sets_selected_template() {
        let state = Arc::new(RwLock::new(crate::state::StateData::default()));
        state.blocking_write().apply_template_dialog = Some(crate::state::ApplyTemplateDialog {
            track_name: "Kick".to_string(),
            selected_template: None,
            available_templates: vec![],
            available_folder_templates: vec![],
        });
        let mut view = ApplyTemplateView::new(state.clone());

        view.update(&Message::ApplyTemplate(ApplyTemplate::TemplateSelected(
            "Drum".to_string(),
        )));

        assert_eq!(
            state
                .blocking_read()
                .apply_template_dialog
                .as_ref()
                .and_then(|d| d.selected_template.as_deref()),
            Some("Drum")
        );
    }

    #[test]
    fn view_returns_empty_when_dialog_closed() {
        let state = Arc::new(RwLock::new(crate::state::StateData::default()));
        let view = ApplyTemplateView::new(state);
        let _element = view.view();
    }
}