rustydialogs 0.4.2

Provides a simple and cross-platform way to display native dialog boxes.
Documentation
use std::ffi::{CStr, CString, OsStr};
use std::os::raw::{c_char, c_void};
use std::os::unix::ffi::OsStrExt;
use std::{ptr, sync};

use gtk4_gio_sys::{g_file_get_path, g_list_model_get_item, g_list_model_get_n_items, GFile, GListModel};
use gtk4_glib_sys::{g_free, g_main_loop_new, g_main_loop_quit, g_main_loop_run, g_main_loop_unref, GMainLoop};
use gtk4_gobject_sys::{
	g_object_ref_sink, g_object_unref, g_signal_connect_data, g_signal_handler_disconnect,
};

use super::*;

mod color;
mod file;
mod folder;
mod input;
mod message;

pub fn message_box(p: &MessageBox<'_>) -> Option<MessageResult> {
	message::show(p)
}

pub fn pick_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	file::pick_file(p)
}

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

pub fn save_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	file::save_file(p)
}

pub fn folder_dialog(p: &FolderDialog<'_>) -> Option<PathBuf> {
	folder::folder_dialog(p)
}

pub fn text_input(p: &TextInput<'_>) -> Option<String> {
	input::text_input(p)
}

pub fn color_picker(p: &ColorPicker<'_>) -> Option<ColorValue> {
	color::color_picker(p)
}

static GTK_INITIALIZED: sync::OnceLock<bool> = sync::OnceLock::new();

fn ensure_gtk_initialized() {
	let ok = *GTK_INITIALIZED.get_or_init(|| unsafe { gtk4_sys::gtk_init_check() != 0 });
	if !ok {
		panic!("Failed to initialize GTK4 backend. Ensure a graphical session is available.");
	}
}

fn cstring(value: &str) -> CString {
	CString::new(value).unwrap_or_else(|_| CString::new(value.replace('\0', " ")).unwrap())
}

fn c_to_path_buf(ptr: *mut c_char) -> Option<PathBuf> {
	if ptr.is_null() {
		return None;
	}
	let bytes = unsafe { CStr::from_ptr(ptr).to_bytes().to_vec() };
	unsafe { g_free(ptr as *mut _) };
	Some(PathBuf::from(OsStr::from_bytes(&bytes)))
}

fn gfile_to_path_buf(file: *mut GFile) -> Option<PathBuf> {
	if file.is_null() {
		return None;
	}
	let path_ptr = unsafe { g_file_get_path(file) };
	unsafe { g_object_unref(file as *mut _) };
	c_to_path_buf(path_ptr)
}

fn collect_file_model(model: *mut GListModel) -> Vec<PathBuf> {
	if model.is_null() {
		return Vec::new();
	}

	let mut result = Vec::new();
	let count = unsafe { g_list_model_get_n_items(model) };
	for index in 0..count {
		let item = unsafe { g_list_model_get_item(model, index) };
		let file = item as *mut GFile;
		if let Some(path) = gfile_to_path_buf(file) {
			result.push(path);
		}
	}

	unsafe { g_object_unref(model as *mut _) };
	result
}

#[repr(C)]
struct ResponseState {
	loop_: *mut GMainLoop,
	response: i32,
}

unsafe extern "C" fn on_dialog_response(
	_dialog: *mut gtk4_sys::GtkDialog,
	response: i32,
	user_data: *mut c_void,
) {
	let state = &mut *(user_data as *mut ResponseState);
	state.response = response;
	g_main_loop_quit(state.loop_);
}

unsafe extern "C" fn on_native_dialog_response(
	_dialog: *mut gtk4_sys::GtkNativeDialog,
	response: i32,
	user_data: *mut c_void,
) {
	let state = &mut *(user_data as *mut ResponseState);
	state.response = response;
	g_main_loop_quit(state.loop_);
}

fn run_dialog(dialog: *mut gtk4_sys::GtkDialog) -> i32 {
	let loop_ = unsafe { g_main_loop_new(ptr::null_mut(), 0) };
	let mut state = ResponseState {
		loop_,
		response: gtk4_sys::GTK_RESPONSE_NONE,
	};

	unsafe {
		let callback: gtk4_gobject_sys::GCallback = std::mem::transmute(Some(
			on_dialog_response
				as unsafe extern "C" fn(*mut gtk4_sys::GtkDialog, i32, *mut c_void),
		));
		let handler_id = g_signal_connect_data(
			dialog as *mut _,
			c"response".as_ptr(),
			callback,
			&mut state as *mut _ as *mut _,
			None,
			0,
		);

		gtk4_sys::gtk_window_present(dialog as *mut gtk4_sys::GtkWindow);
		g_main_loop_run(loop_);
		g_signal_handler_disconnect(dialog as *mut _, handler_id);
		g_main_loop_unref(loop_);
	}

	state.response
}

#[inline]
fn run_dialog_f<T, F: FnOnce(i32) -> T>(dialog: *mut gtk4_sys::GtkDialog, f: F) -> T {
	unsafe {
		g_object_ref_sink(dialog as *mut _);
	}
	let response = run_dialog(dialog);
	let result = f(response);
	unsafe {
		gtk4_sys::gtk_window_close(dialog as *mut gtk4_sys::GtkWindow);
		g_object_unref(dialog as *mut _);
	}
	result
}

fn run_native_dialog(dialog: *mut gtk4_sys::GtkNativeDialog) -> i32 {
	let loop_ = unsafe { g_main_loop_new(ptr::null_mut(), 0) };
	let mut state = ResponseState {
		loop_,
		response: gtk4_sys::GTK_RESPONSE_NONE,
	};

	unsafe {
		let callback: gtk4_gobject_sys::GCallback = std::mem::transmute(Some(
			on_native_dialog_response
				as unsafe extern "C" fn(*mut gtk4_sys::GtkNativeDialog, i32, *mut c_void),
		));
		let handler_id = g_signal_connect_data(
			dialog as *mut _,
			c"response".as_ptr(),
			callback,
			&mut state as *mut _ as *mut _,
			None,
			0,
		);

		gtk4_sys::gtk_native_dialog_show(dialog);
		g_main_loop_run(loop_);
		g_signal_handler_disconnect(dialog as *mut _, handler_id);
		gtk4_sys::gtk_native_dialog_hide(dialog);
		g_main_loop_unref(loop_);
	}

	state.response
}

#[inline]
fn run_native_dialog_f<T, F: FnOnce(i32) -> T>(dialog: *mut gtk4_sys::GtkNativeDialog, f: F) -> T {
	let response = run_native_dialog(dialog);
	let result = f(response);
	unsafe {
		g_object_unref(dialog as *mut _);
	}
	result
}