native-dialog 0.9.6

A library to display dialogs. Supports GNU/Linux, BSD Unix, macOS and Windows.
Documentation
use std::path::Path;

use wfd::{
    DialogError, DialogParams, FOS_ALLOWMULTISELECT, FOS_FILEMUSTEXIST, FOS_NOREADONLYRETURN,
    FOS_OVERWRITEPROMPT, FOS_PATHMUSTEXIST, FOS_PICKFOLDERS, FOS_STRICTFILETYPES, OpenDialogResult,
    SaveDialogResult,
};

use crate::dialog::{
    DialogImpl, FileFilter, OpenMultipleFile, OpenSingleDir, OpenSingleFile, SaveSingleFile,
};
use crate::ffi::UnsafeWindowHandle;
use crate::utils::resolve_tilde;
use crate::{Error, Result};

impl OpenSingleFile {
    fn create(&self) -> OpenDialogParams<'_> {
        OpenDialogParams {
            title: &self.title,
            filename: self.filename.as_deref(),
            location: self.location.as_deref(),
            filters: &self.filters.items,
            owner: self.owner.clone(),
            multiple: false,
            dir: false,
        }
    }
}

impl DialogImpl for OpenSingleFile {
    fn show(self) -> Result<Self::Output> {
        super::process_init();
        let result = open_dialog(self.create())?;
        Ok(result.map(|x| x.selected_file_path))
    }

    #[cfg(feature = "async")]
    async fn spawn(self) -> Result<Self::Output> {
        self.show()
    }
}

impl OpenMultipleFile {
    fn create(&self) -> OpenDialogParams<'_> {
        OpenDialogParams {
            title: &self.title,
            filename: self.filename.as_deref(),
            location: self.location.as_deref(),
            filters: &self.filters.items,
            owner: self.owner.clone(),
            multiple: true,
            dir: false,
        }
    }
}

impl DialogImpl for OpenMultipleFile {
    fn show(self) -> Result<Self::Output> {
        super::process_init();

        let result = open_dialog(self.create())?;
        match result {
            Some(t) => Ok(t.selected_file_paths),
            None => Ok(vec![]),
        }
    }

    #[cfg(feature = "async")]
    async fn spawn(self) -> Result<Self::Output> {
        self.show()
    }
}

impl OpenSingleDir {
    fn create(&self) -> OpenDialogParams<'_> {
        OpenDialogParams {
            title: &self.title,
            filename: self.filename.as_deref(),
            location: self.location.as_deref(),
            filters: &[],
            owner: self.owner.clone(),
            multiple: false,
            dir: true,
        }
    }
}

impl DialogImpl for OpenSingleDir {
    fn show(self) -> Result<Self::Output> {
        super::process_init();

        let result = open_dialog(self.create())?;
        Ok(result.map(|x| x.selected_file_path))
    }

    #[cfg(feature = "async")]
    async fn spawn(self) -> Result<Self::Output> {
        self.show()
    }
}

impl SaveSingleFile {
    fn create(&self) -> SaveDialogParams<'_> {
        SaveDialogParams {
            title: &self.title,
            filename: self.filename.as_deref(),
            location: self.location.as_deref(),
            filters: &self.filters.items,
            owner: self.owner.clone(),
        }
    }
}

impl DialogImpl for SaveSingleFile {
    fn show(self) -> Result<Self::Output> {
        super::process_init();

        let result = save_dialog(self.create())?;
        Ok(result.map(|x| x.selected_file_path))
    }

    #[cfg(feature = "async")]
    async fn spawn(self) -> Result<Self::Output> {
        self.show()
    }
}

pub struct OpenDialogParams<'a> {
    title: &'a str,
    filename: Option<&'a str>,
    location: Option<&'a Path>,
    filters: &'a [FileFilter],
    owner: UnsafeWindowHandle,
    multiple: bool,
    dir: bool,
}

fn open_dialog(params: OpenDialogParams) -> Result<Option<OpenDialogResult>> {
    let folder = params.location.and_then(resolve_tilde);
    let folder = folder.as_deref().and_then(Path::to_str).unwrap_or("");

    let file_types: Vec<_> = get_dialog_file_types(params.filters);
    let file_types = file_types.iter().map(|t| (t.0, &*t.1)).collect();

    let file_name = params.filename.unwrap_or("");

    let mut options = FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_STRICTFILETYPES;
    if params.multiple {
        options |= FOS_ALLOWMULTISELECT;
    }
    if params.dir {
        options |= FOS_PICKFOLDERS;
    }

    let owner = unsafe { params.owner.as_win32() };

    let params = DialogParams {
        folder,
        file_types,
        file_name,
        options,
        owner,
        title: params.title,
        ..Default::default()
    };

    let result = wfd::open_dialog(params);

    convert_result(result)
}

pub struct SaveDialogParams<'a> {
    title: &'a str,
    filename: Option<&'a str>,
    location: Option<&'a Path>,
    filters: &'a [FileFilter],
    owner: UnsafeWindowHandle,
}

fn save_dialog(params: SaveDialogParams) -> Result<Option<SaveDialogResult>> {
    let folder = params.location.and_then(resolve_tilde);
    let folder = folder.as_deref().and_then(Path::to_str).unwrap_or("");

    let file_types: Vec<_> = get_dialog_file_types(params.filters);
    let file_types = file_types.iter().map(|t| (t.0, &*t.1)).collect();

    let file_name = params.filename.unwrap_or("");

    let default_extension = match params.filters.first() {
        Some(first) => &first.extensions[0],
        _ => "",
    };

    let options =
        FOS_OVERWRITEPROMPT | FOS_PATHMUSTEXIST | FOS_NOREADONLYRETURN | FOS_STRICTFILETYPES;

    let owner = unsafe { params.owner.as_win32() };

    let params = DialogParams {
        folder,
        file_types,
        file_name,
        default_extension,
        options,
        owner,
        title: params.title,
        ..Default::default()
    };

    let result = wfd::save_dialog(params);

    convert_result(result)
}

fn get_dialog_file_types(filters: &[FileFilter]) -> Vec<(&str, String)> {
    filters
        .iter()
        .map(|filter| {
            let name = &*filter.name;
            let types = filter.format("{types}", "*{ext}", ";");
            (name, types)
        })
        .collect()
}

fn convert_result<T>(result: std::result::Result<T, DialogError>) -> Result<Option<T>> {
    match result {
        Ok(t) => Ok(Some(t)),
        Err(e) => match e {
            DialogError::UserCancelled => Ok(None),
            DialogError::HResultFailed { error_method, .. } => Err(Error::Other(error_method)),
            DialogError::UnsupportedFilepath => {
                Err(Error::Other("Unsupported filepath".to_string()))
            }
        },
    }
}