sequoia-platform 0.1.0

Sequoia GUI framework — windowing platform (winit + wgpu)
use std::path::PathBuf;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileFilter {
    pub name: String,
    pub extensions: Vec<String>,
}

impl FileFilter {
    pub fn new(name: impl Into<String>, extensions: &[&str]) -> Self {
        Self {
            name: name.into(),
            extensions: extensions.iter().map(|s| s.to_string()).collect(),
        }
    }

    pub fn matches(&self, file_name: &str) -> bool {
        if self.extensions.iter().any(|e| e == "*") {
            return true;
        }
        let lower = file_name.to_lowercase();
        self.extensions.iter().any(|ext| {
            let ext = ext.to_lowercase();
            lower.ends_with(&format!(".{ext}"))
        })
    }
}

#[derive(Clone, Debug, Default)]
pub struct NativeFileDialog {
    title: String,
    directory: Option<PathBuf>,
    file_name: Option<String>,
    filters: Vec<FileFilter>,
}

impl NativeFileDialog {
    pub fn open_file(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            ..Self::default()
        }
    }

    pub fn save_file(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            ..Self::default()
        }
    }

    pub fn pick_folder(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            ..Self::default()
        }
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }

    pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
        self.directory = Some(dir.into());
        self
    }

    pub fn file_name(mut self, name: impl Into<String>) -> Self {
        self.file_name = Some(name.into());
        self
    }

    pub fn filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
        self.filters.push(FileFilter::new(name, extensions));
        self
    }

    pub fn dialog_title(&self) -> &str {
        &self.title
    }

    pub fn filters(&self) -> &[FileFilter] {
        &self.filters
    }

    fn apply_to_rfd(&self, mut dlg: rfd::FileDialog) -> rfd::FileDialog {
        if !self.title.is_empty() {
            dlg = dlg.set_title(&self.title);
        }
        if let Some(dir) = &self.directory {
            dlg = dlg.set_directory(dir);
        }
        if let Some(name) = &self.file_name {
            dlg = dlg.set_file_name(name);
        }
        for f in &self.filters {
            let exts: Vec<&str> = f.extensions.iter().map(|s| s.as_str()).collect();
            dlg = dlg.add_filter(&f.name, &exts);
        }
        dlg
    }

    pub fn pick(&self) -> Option<PathBuf> {
        self.apply_to_rfd(rfd::FileDialog::new()).pick_file()
    }

    pub fn pick_many(&self) -> Vec<PathBuf> {
        self.apply_to_rfd(rfd::FileDialog::new())
            .pick_files()
            .unwrap_or_default()
    }

    pub fn save(&self) -> Option<PathBuf> {
        self.apply_to_rfd(rfd::FileDialog::new()).save_file()
    }

    pub fn pick_dir(&self) -> Option<PathBuf> {
        self.apply_to_rfd(rfd::FileDialog::new()).pick_folder()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MessageKind {
    Info,
    Warning,
    Error,
}

pub fn native_message(title: &str, description: &str, kind: MessageKind) {
    let dlg = rfd::MessageDialog::new()
        .set_title(title)
        .set_description(description);
    let dlg = match kind {
        MessageKind::Info => dlg.set_level(rfd::MessageLevel::Info),
        MessageKind::Warning => dlg.set_level(rfd::MessageLevel::Warning),
        MessageKind::Error => dlg.set_level(rfd::MessageLevel::Error),
    };
    dlg.show();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filter_matches_by_extension_case_insensitive() {
        let f = FileFilter::new("Obrazy", &["png", "jpg"]);
        assert!(f.matches("foto.PNG"));
        assert!(f.matches("a.b.jpg"));
        assert!(!f.matches("doc.txt"));
        assert!(!f.matches("png"));
        let all = FileFilter::new("Wszystkie", &["*"]);
        assert!(all.matches("cokolwiek.bin"));
    }

    #[test]
    fn builder_keeps_title_filters_and_paths() {
        let d = NativeFileDialog::save_file("Zapisz")
            .directory("/tmp")
            .file_name("doc.txt")
            .filter("Tekst", &["txt"])
            .filter("Wszystkie", &["*"]);
        assert_eq!(d.dialog_title(), "Zapisz");
        assert_eq!(d.filters().len(), 2);
        assert_eq!(d.filters()[0].extensions, vec!["txt".to_string()]);
    }
}