rustydialogs 0.4.2

Provides a simple and cross-platform way to display native dialog boxes.
Documentation
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::UI::Controls::Dialogs::{
	GetOpenFileNameW, GetSaveFileNameW, OPENFILENAMEW, OFN_ALLOWMULTISELECT, OFN_EXPLORER,
	OFN_FILEMUSTEXIST, OFN_NOCHANGEDIR, OFN_OVERWRITEPROMPT, OFN_PATHMUSTEXIST,
};

use super::*;

pub fn pick_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	pick_files_impl(p, false).and_then(|paths| paths.into_iter().next())
}

pub fn pick_files(p: &FileDialog<'_>) -> Option<Vec<PathBuf>> {
	pick_files_impl(p, true)
}

pub fn save_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	let title = utf16cs(p.title);
	let filter = build_windows_filter(p.filter);
	let path = utils::abspath(p.path);
	let mut file_buffer = initial_file_buffer(path.as_deref());

	let mut open_file_name = OPENFILENAMEW::default();
	open_file_name.lStructSize = std::mem::size_of::<OPENFILENAMEW>() as u32;
	open_file_name.lpstrTitle = PCWSTR(title.as_ptr());
	if let Some(filter) = &filter {
		open_file_name.lpstrFilter = PCWSTR(filter.as_ptr());
	}
	open_file_name.hwndOwner = hwnd(p.owner).unwrap_or_default();
	open_file_name.lpstrFile = PWSTR(file_buffer.as_mut_ptr());
	open_file_name.nMaxFile = file_buffer.len() as u32;
	open_file_name.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT;

	let selected = unsafe { GetSaveFileNameW(&mut open_file_name).as_bool() };
	if !selected {
		return None;
	}

	wide_to_string_until_nul(&file_buffer)
}

fn pick_files_impl(p: &FileDialog<'_>, allow_multiple_selects: bool) -> Option<Vec<PathBuf>> {
	let title = utf16cs(p.title);
	let filter = build_windows_filter(p.filter);
	let path = utils::abspath(p.path);
	let mut file_buffer = initial_file_buffer(path.as_deref());

	let mut open_file_name = OPENFILENAMEW::default();
	open_file_name.lStructSize = std::mem::size_of::<OPENFILENAMEW>() as u32;
	open_file_name.lpstrTitle = PCWSTR(title.as_ptr());
	if let Some(filter) = &filter {
		open_file_name.lpstrFilter = PCWSTR(filter.as_ptr());
	}
	open_file_name.hwndOwner = hwnd(p.owner).unwrap_or_default();
	open_file_name.lpstrFile = PWSTR(file_buffer.as_mut_ptr());
	open_file_name.nMaxFile = file_buffer.len() as u32;
	open_file_name.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
	if allow_multiple_selects {
		open_file_name.Flags |= OFN_ALLOWMULTISELECT;
	}

	let selected = unsafe { GetOpenFileNameW(&mut open_file_name).as_bool() };
	if !selected {
		return None;
	}

	parse_open_file_buffer(&file_buffer)
}

fn initial_file_buffer(file: Option<&Path>) -> Vec<u16> {
	let mut buffer = vec![0u16; 16 * 1024];
	let Some(file) = file else {
		return buffer;
	};

	if file.as_os_str().is_empty() {
		return buffer;
	}

	let encoded = utf16cs(&file.to_string_lossy());
	let copy_len = encoded.len().min(buffer.len());
	buffer[..copy_len].copy_from_slice(&encoded[..copy_len]);
	buffer
}

fn build_windows_filter(filter: Option<&[FileFilter<'_>]>) -> Option<Vec<u16>> {
	let filter = filter?;
	if filter.is_empty() {
		return None;
	}

	let mut spec = String::new();
	for entry in filter {
		_ = write!(spec, "{}\0{}\0", entry.desc, utils::PrintJoin { parts: entry.patterns, separator: ";" });
	}
	spec.push_str("All Files\0*.*\0\0");
	Some(spec.encode_utf16().collect())
}

fn wide_to_string_until_nul(input: &[u16]) -> Option<PathBuf> {
	let length = input.iter().position(|value| *value == 0)?;
	if length == 0 {
		return None;
	}
	Some(PathBuf::from(String::from_utf16_lossy(&input[..length])))
}

fn parse_open_file_buffer(input: &[u16]) -> Option<Vec<PathBuf>> {
	let mut segments = Vec::new();
	let mut start = 0usize;

	for index in 0..input.len() {
		if input[index] != 0 {
			continue;
		}

		if index == start {
			break;
		}

		segments.push(PathBuf::from(String::from_utf16_lossy(&input[start..index])));
		start = index + 1;
	}

	// No files were selected somehow
	if segments.is_empty() {
		return None;
	}

	// Only a single file was selected
	// The buffer contains the full path and file name
	if segments.len() == 1 {
		return Some(segments);
	}

	// Multiple files were selected
	// The first segment is the directory and the following segments are the file names
	let mut segments = segments.into_iter();
	let directory = segments.next().unwrap();
	let full_paths = segments.map(|file_name| directory.join(file_name)).collect();
	Some(full_paths)
}