tauri-rfd 0.1.0

Rusty File Dialog
Documentation
use crate::FileDialog;

use std::{
    ffi::{c_void, OsStr, OsString},
    iter::once,
    ops::Deref,
    os::windows::{ffi::OsStrExt, prelude::OsStringExt},
    path::PathBuf,
    ptr,
};

use winapi::{
    shared::{
        guiddef::GUID, minwindef::LPVOID, ntdef::LPWSTR, winerror::HRESULT,
        wtypesbase::CLSCTX_INPROC_SERVER,
    },
    um::{
        combaseapi::{CoCreateInstance, CoTaskMemFree},
        shobjidl::{
            IFileDialog, IFileOpenDialog, IFileSaveDialog, FOS_ALLOWMULTISELECT, FOS_PICKFOLDERS,
        },
        shobjidl_core::{
            CLSID_FileOpenDialog, CLSID_FileSaveDialog, IShellItem, IShellItemArray,
            SHCreateItemFromParsingName, SIGDN_FILESYSPATH,
        },
        shtypes::COMDLG_FILTERSPEC,
    },
    Interface,
};

#[cfg(feature = "parent")]
use raw_window_handle::RawWindowHandle;

use super::super::utils::ToResult;

fn to_os_string(s: &LPWSTR) -> OsString {
    let slice = unsafe {
        let mut len = 0;
        while *s.offset(len) != 0 {
            len += 1;
        }
        std::slice::from_raw_parts(*s, len as usize)
    };
    OsStringExt::from_wide(slice)
}

pub struct IDialog(pub *mut IFileDialog, Option<*mut c_void>);

impl IDialog {
    fn new_file_dialog(class: &GUID, id: &GUID) -> Result<*mut IFileDialog, HRESULT> {
        let mut dialog: *mut IFileDialog = ptr::null_mut();

        unsafe {
            CoCreateInstance(
                class,
                ptr::null_mut(),
                CLSCTX_INPROC_SERVER,
                id,
                &mut dialog as *mut *mut IFileDialog as *mut LPVOID,
            )
            .check()?;
        }

        Ok(dialog)
    }

    fn new_open_dialog(opt: &FileDialog) -> Result<Self, HRESULT> {
        let ptr = Self::new_file_dialog(&CLSID_FileOpenDialog, &IFileOpenDialog::uuidof())?;
        #[cfg(feature = "parent")]
        let parent = match opt.parent {
            Some(RawWindowHandle::Windows(handle)) => Some(handle.hwnd),
            None => None,
            _ => unreachable!("Unsuported window handle, expected: Windows"),
        };
        #[cfg(not(feature = "parent"))]
        let parent = None;
        Ok(Self(ptr, parent))
    }

    fn new_save_dialog(opt: &FileDialog) -> Result<Self, HRESULT> {
        let ptr = Self::new_file_dialog(&CLSID_FileSaveDialog, &IFileSaveDialog::uuidof())?;
        #[cfg(feature = "parent")]
        let parent = match opt.parent {
            Some(RawWindowHandle::Windows(handle)) => Some(handle.hwnd),
            None => None,
            _ => unreachable!("Unsuported window handle, expected: Windows"),
        };
        #[cfg(not(feature = "parent"))]
        let parent = None;
        Ok(Self(ptr, parent))
    }

    fn add_filters(&self, filters: &[crate::dialog::Filter]) -> Result<(), HRESULT> {
        if let Some(first_filter) = filters.first() {
            if let Some(first_extension) = first_filter.extensions.first() {
                let extension: Vec<u16> = first_extension.encode_utf16().chain(Some(0)).collect();
                unsafe {
                    (*self.0).SetDefaultExtension(extension.as_ptr()).check()?;
                }
            }
        }

        let f_list = {
            let mut f_list = Vec::new();

            for f in filters.iter() {
                let name: Vec<u16> = OsStr::new(&f.name).encode_wide().chain(once(0)).collect();
                let ext_string = f
                    .extensions
                    .iter()
                    .map(|item| format!("*.{}", item))
                    .collect::<Vec<_>>()
                    .join(";");

                let ext: Vec<u16> = OsStr::new(&ext_string)
                    .encode_wide()
                    .chain(once(0))
                    .collect();

                f_list.push((name, ext));
            }
            f_list
        };

        let spec: Vec<_> = f_list
            .iter()
            .map(|(name, ext)| COMDLG_FILTERSPEC {
                pszName: name.as_ptr(),
                pszSpec: ext.as_ptr(),
            })
            .collect();

        unsafe {
            if !spec.is_empty() {
                (*self.0)
                    .SetFileTypes(spec.len() as _, spec.as_ptr())
                    .check()?;
            }
        }
        Ok(())
    }

    fn set_path(&self, path: &Option<PathBuf>) -> Result<(), HRESULT> {
        if let Some(path) = path {
            if let Some(path) = path.to_str() {
                let wide_path: Vec<u16> = OsStr::new(path).encode_wide().chain(once(0)).collect();

                unsafe {
                    let mut item: *mut IShellItem = ptr::null_mut();
                    SHCreateItemFromParsingName(
                        wide_path.as_ptr(),
                        ptr::null_mut(),
                        &IShellItem::uuidof(),
                        &mut item as *mut *mut IShellItem as *mut *mut _,
                    )
                    .check()?;

                    (*self.0).SetDefaultFolder(item).check()?;
                }
            }
        }
        Ok(())
    }

    fn set_file_name(&self, file_name: &Option<String>) -> Result<(), HRESULT> {
        if let Some(path) = file_name {
            let wide_path: Vec<u16> = OsStr::new(path).encode_wide().chain(once(0)).collect();

            unsafe {
                (*self.0).SetFileName(wide_path.as_ptr()).check()?;
            }
        }
        Ok(())
    }

    pub fn get_results(&self) -> Result<Vec<PathBuf>, HRESULT> {
        unsafe {
            let mut res_items: *mut IShellItemArray = ptr::null_mut();
            (*(self.0 as *mut IFileOpenDialog))
                .GetResults(&mut res_items)
                .check()?;

            let items = &*res_items;
            let mut count = 0;
            items.GetCount(&mut count);
            let mut paths = Vec::new();
            for id in 0..count {
                let mut res_item: *mut IShellItem = ptr::null_mut();
                items.GetItemAt(id, &mut res_item).check()?;
                let mut display_name: LPWSTR = ptr::null_mut();
                (*res_item)
                    .GetDisplayName(SIGDN_FILESYSPATH, &mut display_name)
                    .check()?;
                let filename = to_os_string(&display_name);
                CoTaskMemFree(display_name as LPVOID);
                let path = PathBuf::from(filename);
                paths.push(path);
            }
            items.Release();

            Ok(paths)
        }
    }

    pub fn get_result(&self) -> Result<PathBuf, HRESULT> {
        let mut res_item: *mut IShellItem = ptr::null_mut();
        unsafe {
            (*self.0).GetResult(&mut res_item).check()?;

            let mut display_name: LPWSTR = ptr::null_mut();

            (*res_item)
                .GetDisplayName(SIGDN_FILESYSPATH, &mut display_name)
                .check()?;

            let filename = to_os_string(&display_name);
            CoTaskMemFree(display_name as LPVOID);

            Ok(PathBuf::from(filename))
        }
    }

    pub fn show(&self) -> Result<(), HRESULT> {
        unsafe {
            self.Show(self.1.unwrap_or_else(|| ptr::null_mut()) as _)
                .check()?
        };
        Ok(())
    }
}

impl IDialog {
    pub fn build_pick_file(opt: &FileDialog) -> Result<Self, HRESULT> {
        let dialog = IDialog::new_open_dialog(opt)?;

        dialog.add_filters(&opt.filters)?;
        dialog.set_path(&opt.starting_directory)?;
        dialog.set_file_name(&opt.file_name)?;

        Ok(dialog)
    }

    pub fn build_save_file(opt: &FileDialog) -> Result<Self, HRESULT> {
        let dialog = IDialog::new_save_dialog(opt)?;

        dialog.add_filters(&opt.filters)?;
        dialog.set_path(&opt.starting_directory)?;
        dialog.set_file_name(&opt.file_name)?;

        Ok(dialog)
    }

    pub fn build_pick_folder(opt: &FileDialog) -> Result<Self, HRESULT> {
        let dialog = IDialog::new_open_dialog(opt)?;

        dialog.set_path(&opt.starting_directory)?;

        unsafe {
            dialog.SetOptions(FOS_PICKFOLDERS).check()?;
        }

        Ok(dialog)
    }

    pub fn build_pick_files(opt: &FileDialog) -> Result<Self, HRESULT> {
        let dialog = IDialog::new_open_dialog(opt)?;

        dialog.add_filters(&opt.filters)?;
        dialog.set_path(&opt.starting_directory)?;
        dialog.set_file_name(&opt.file_name)?;

        unsafe {
            dialog.SetOptions(FOS_ALLOWMULTISELECT).check()?;
        }

        Ok(dialog)
    }
}

impl Deref for IDialog {
    type Target = IFileDialog;
    fn deref(&self) -> &Self::Target {
        unsafe { &*self.0 }
    }
}

impl Drop for IDialog {
    fn drop(&mut self) {
        unsafe { (*(self.0 as *mut IFileDialog)).Release() };
    }
}