native-dialog 0.6.3

A library to display dialogs. Supports GNU/Linux, BSD Unix, macOS and Windows.
Documentation
use crate::dialog::{DialogImpl, OpenMultipleFile, OpenSingleDir, OpenSingleFile, SaveSingleFile};
use crate::util::resolve_tilde;
use crate::{Error, Filter, Result};
use raw_window_handle::RawWindowHandle;
use std::path::Path;
use wfd::{
    DialogError, DialogParams, OpenDialogResult, SaveDialogResult, FOS_ALLOWMULTISELECT,
    FOS_FILEMUSTEXIST, FOS_NOREADONLYRETURN, FOS_OVERWRITEPROMPT, FOS_PATHMUSTEXIST,
    FOS_PICKFOLDERS, FOS_STRICTFILETYPES, HWND,
};

impl DialogImpl for OpenSingleFile<'_> {
    fn show(&mut self) -> Result<Self::Output> {
        super::process_init();

        let result = open_dialog(OpenDialogParams {
            filename: self.filename,
            location: self.location,
            filters: &self.filters,
            owner: self.owner,
            multiple: false,
            dir: false,
        })?;

        Ok(result.map(|x| x.selected_file_path))
    }
}

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

        let result = open_dialog(OpenDialogParams {
            filename: self.filename,
            location: self.location,
            filters: &self.filters,
            owner: self.owner,
            multiple: true,
            dir: false,
        })?;

        match result {
            Some(t) => Ok(t.selected_file_paths),
            None => Ok(vec![]),
        }
    }
}

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

        let result = open_dialog(OpenDialogParams {
            filename: self.filename,
            location: self.location,
            filters: &[],
            owner: self.owner,
            multiple: false,
            dir: true,
        })?;

        Ok(result.map(|x| x.selected_file_path))
    }
}

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

        let result = save_dialog(SaveDialogParams {
            filename: self.filename,
            location: self.location,
            filters: &self.filters,
            owner: self.owner,
        })?;

        Ok(result.map(|x| x.selected_file_path))
    }
}

struct OpenDialogParams<'a> {
    filename: Option<&'a str>,
    location: Option<&'a Path>,
    filters: &'a [Filter<'a>],
    owner: Option<RawWindowHandle>,
    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 = match params.owner {
        Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd as HWND),
        _ => None,
    };

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

    let result = wfd::open_dialog(params);

    convert_result(result)
}

struct SaveDialogParams<'a> {
    filename: Option<&'a str>,
    location: Option<&'a Path>,
    filters: &'a [Filter<'a>],
    owner: Option<RawWindowHandle>,
}

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_filter, ..] => first_filter.extensions[0],
        _ => "",
    };

    let options =
        FOS_OVERWRITEPROMPT | FOS_PATHMUSTEXIST | FOS_NOREADONLYRETURN | FOS_STRICTFILETYPES;

    let owner = match params.owner {
        Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd as HWND),
        _ => None,
    };

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

    let result = wfd::save_dialog(params);

    convert_result(result)
}

fn get_dialog_file_types<'a>(filters: &'a [Filter<'a>]) -> Vec<(&'a str, String)> {
    filters
        .iter()
        .map(|filter| {
            let extensions = filter.extensions.iter().map(|x| format!("*.{}", x));
            (filter.description, extensions.collect::<Vec<_>>().join(";"))
        })
        .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::ImplementationError(error_method))
            }
            DialogError::UnsupportedFilepath => Err(Error::ImplementationError(
                "Unsupported filepath".to_string(),
            )),
        },
    }
}