Skip to main content

rustydialogs/
lib.rs

1/*!
2Rusty Dialogs is a cross-platform library for showing native dialog boxes and notifications.
3
4Supported platforms and backends:
5
6### Windows
7
8Win32-based legacy dialogs compatible with any COM apartment model.
9
10By default, notifications use a tray icon with balloon tips.
11
12Optional WinRT-Toast notifications are available on Windows 10 and later. (feature: `winrt-toast`)
13
14### Linux & BSDs
15
16By default, executable-based backends (`kdialog` and `zenity`) are used.
17
18Optional GTK3 and GTK4 backends are available with libnotify-based notifications. (feature: `gtk3`, `gtk4`)
19
20XDG desktop portal support is also available, but limited to file and folder dialogs. (feature: `xdg-portal`)
21
22### macOS
23
24By default, AppleScript-based dialogs are used.
25
26Optional AppKit-based dialogs and notifications are also available. (feature: `appkit`)
27*/
28
29use std::path::{Path, PathBuf};
30use raw_window_handle::HasWindowHandle;
31
32mod utils;
33
34/// Icon types for message dialogs.
35#[derive(Copy, Clone, Debug, Eq, PartialEq)]
36pub enum MessageIcon {
37	/// Information icon.
38	Info,
39	/// Warning icon.
40	Warning,
41	/// Error icon.
42	Error,
43	/// Question icon.
44	Question,
45}
46
47/// Button configurations for message dialogs.
48#[derive(Copy, Clone, Debug, Eq, PartialEq)]
49pub enum MessageButtons {
50	/// OK button only.
51	Ok,
52	/// OK and Cancel buttons.
53	OkCancel,
54	/// Yes and No buttons.
55	YesNo,
56	/// Yes, No, and Cancel buttons.
57	YesNoCancel,
58}
59
60/// Result of a message dialog.
61#[derive(Copy, Clone, Debug, Eq, PartialEq)]
62pub enum MessageResult {
63	/// OK result.
64	Ok,
65	/// Cancel result.
66	Cancel,
67	/// Yes result.
68	Yes,
69	/// No result.
70	No,
71}
72
73/// Message box dialog.
74///
75/// ```no_run
76/// let result = rustydialogs::MessageBox {
77/// 	title: "Confirm Action",
78/// 	message: "Are you sure you want to proceed?",
79/// 	icon: rustydialogs::MessageIcon::Question,
80/// 	buttons: rustydialogs::MessageButtons::YesNo,
81/// 	owner: None,
82/// }.show();
83/// if result == Some(rustydialogs::MessageResult::Yes) {
84/// 	println!("User chose Yes");
85/// }
86/// ```
87#[derive(Copy, Clone)]
88pub struct MessageBox<'a> {
89	/// The title of the dialog.
90	pub title: &'a str,
91	/// The message to display to the user.
92	pub message: &'a str,
93	/// The icon to show in the dialog.
94	pub icon: MessageIcon,
95	/// The buttons to show in the dialog.
96	pub buttons: MessageButtons,
97	/// The owner window of the dialog.
98	pub owner: Option<&'a dyn HasWindowHandle>,
99}
100
101impl<'a> MessageBox<'a> {
102	/// Show the dialog.
103	///
104	/// Prefer to check if the dialog result matches `Some(Ok)` or `Some(Yes)` rather than checking for `Some(No)` or `Some(Cancel)`.
105	///
106	/// When the dialog is dismissed (closing the dialog or pressing ESC), the result maybe `None` or may be any of the message results even if that button is not present (e.g. `Some(Cancel)`).
107	#[inline]
108	pub fn show(&self) -> Option<MessageResult> {
109		message_box(self)
110	}
111}
112
113
114/// File filter for file dialogs.
115#[derive(Copy, Clone, Debug)]
116pub struct FileFilter<'a> {
117	/// The description of the file filter, e.g. `"Text Files"`.
118	pub desc: &'a str,
119	/// The file patterns of the file filter, e.g. `&["*.txt"]` or `&["*.jpg", "*.jpeg"]`.
120	pub patterns: &'a [&'a str],
121}
122
123/// File dialog.
124///
125/// The file dialog allows the user to select a file or multiple files, or to specify a file name for saving.
126///
127/// ```no_run
128/// use std::env;
129///
130/// let file = rustydialogs::FileDialog {
131/// 	title: "Open File",
132/// 	path: env::current_dir().ok().as_deref(),
133/// 	filter: Some(&[
134/// 		rustydialogs::FileFilter {
135/// 			desc: "Text Files",
136/// 			patterns: &["*.txt", "*.md"],
137/// 		},
138/// 	]),
139/// 	owner: None,
140/// }.pick_file();
141///
142/// if let Some(path) = file {
143/// 	println!("Picked file: {}", path.display());
144/// }
145/// ```
146#[derive(Copy, Clone)]
147pub struct FileDialog<'a> {
148	/// The title of the dialog.
149	pub title: &'a str,
150	/// The initial path to show in the file dialog.
151	///
152	/// If the path is relative, it is joined with the current working directory.
153	///
154	/// If the resulting path exists and is a directory, no default file name is provided.
155	///
156	/// Otherwise, [`Path::file_name`] is used as the default file name.
157	pub path: Option<&'a Path>,
158	/// An optional list of file filters to show in the file dialog.
159	pub filter: Option<&'a [FileFilter<'a>]>,
160	/// The owner window of the dialog.
161	pub owner: Option<&'a dyn HasWindowHandle>,
162}
163
164impl<'a> FileDialog<'a> {
165	/// Show open file dialog, allowing the user to select a single file.
166	#[inline]
167	pub fn pick_file(&self) -> Option<PathBuf> {
168		pick_file(self)
169	}
170
171	/// Show open file dialog, allowing the user to select multiple files.
172	#[inline]
173	pub fn pick_files(&self) -> Option<Vec<PathBuf>> {
174		pick_files(self)
175	}
176
177	/// Show save file dialog.
178	#[inline]
179	pub fn save_file(&self) -> Option<PathBuf> {
180		save_file(self)
181	}
182}
183
184/// Folder dialog.
185///
186/// The folder dialog allows the user to select a folder or directory.
187///
188/// ```no_run
189/// use std::env;
190///
191/// let folder = rustydialogs::FolderDialog {
192/// 	title: "Select Folder",
193/// 	directory: env::current_dir().ok().as_deref(),
194/// 	owner: None,
195/// }.show();
196///
197/// if let Some(path) = folder {
198/// 	println!("Picked folder: {}", path.display());
199/// }
200/// ```
201#[derive(Copy, Clone)]
202pub struct FolderDialog<'a> {
203	/// The title of the dialog.
204	pub title: &'a str,
205	/// The initial directory to show in the folder dialog.
206	pub directory: Option<&'a Path>,
207	/// The owner window of the dialog.
208	pub owner: Option<&'a dyn HasWindowHandle>,
209}
210
211impl<'a> FolderDialog<'a> {
212	/// Show the dialog.
213	#[inline]
214	pub fn show(&self) -> Option<std::path::PathBuf> {
215		folder_dialog(self)
216	}
217}
218
219/// Modes for text input dialogs.
220#[derive(Copy, Clone, Debug, Eq, PartialEq)]
221pub enum TextInputMode {
222	/// Single line text input dialog.
223	SingleLine,
224	/// Multi-line text input dialog.
225	MultiLine,
226	/// Password input dialog, which hides the input text.
227	Password,
228}
229
230/// Text input dialog.
231///
232/// The text input dialog allows the user to enter text, which is returned as a string.
233///
234/// ```no_run
235/// let name = rustydialogs::TextInput {
236/// 	title: "User Name",
237/// 	message: "Enter your name:",
238/// 	value: "",
239/// 	mode: rustydialogs::TextInputMode::SingleLine,
240/// 	owner: None,
241/// }.show();
242///
243/// if let Some(name) = name {
244/// 	println!("Hello, {name}!");
245/// }
246/// ```
247#[derive(Copy, Clone)]
248pub struct TextInput<'a> {
249	/// The title of the dialog.
250	pub title: &'a str,
251	/// The message to display to the user.
252	pub message: &'a str,
253	/// The initial value to display in the text input.
254	pub value: &'a str,
255	/// The mode of the text input, which determines the type of dialog shown and how the input is handled.
256	pub mode: TextInputMode,
257	/// The owner window of the dialog.
258	pub owner: Option<&'a dyn HasWindowHandle>,
259}
260
261impl<'a> TextInput<'a> {
262	/// Show the dialog.
263	///
264	/// Returns `Some(String)` if the user provided input and confirmed the dialog, or `None` if the user cancelled the dialog.
265	#[inline]
266	pub fn show(&self) -> Option<String> {
267		text_input(self)
268	}
269}
270
271/// Color value.
272#[derive(Copy, Clone, Debug, Eq, PartialEq)]
273pub struct ColorValue {
274	/// The red component of the color, in the range [0, 255].
275	pub red: u8,
276	/// The green component of the color, in the range [0, 255].
277	pub green: u8,
278	/// The blue component of the color, in the range [0, 255].
279	pub blue: u8,
280}
281
282/// Color picker dialog.
283///
284/// The color picker dialog allows the user to select a color, which is returned as an RGB value.
285/// The dialog may also show a palette of predefined colors for the user to choose from.
286///
287/// ```no_run
288/// let color = rustydialogs::ColorPicker {
289/// 	title: "Pick a Color",
290/// 	value: rustydialogs::ColorValue {
291/// 		red: 64,
292/// 		green: 128,
293/// 		blue: 255,
294/// 	},
295/// 	owner: None,
296/// }.show();
297///
298/// if let Some(color) = color {
299/// 	println!("RGB({}, {}, {})", color.red, color.green, color.blue);
300/// }
301/// ```
302#[derive(Copy, Clone)]
303pub struct ColorPicker<'a> {
304	/// The title of the dialog.
305	pub title: &'a str,
306	/// The initial color value to show in the color picker dialog.
307	pub value: ColorValue,
308	/// The owner window of the dialog.
309	pub owner: Option<&'a dyn HasWindowHandle>,
310}
311
312impl<'a> ColorPicker<'a> {
313	/// Show the dialog.
314	///
315	/// Returns `Some(ColorValue)` if the user selected a color and confirmed the dialog, or `None` if the user cancelled the dialog.
316	#[inline]
317	pub fn show(&self) -> Option<ColorValue> {
318		color_picker(self)
319	}
320}
321
322/// Notification.
323///
324/// Shows a brief message to the user without blocking their interaction with the application.
325///
326/// ```no_run
327/// // Define a unique application identifier for the notification system.
328/// const APP_ID: &str = "com.example.myapp";
329///
330/// // Invoke setup at application initialization to ensure the application
331/// // is registered and ready to show notifications later.
332/// rustydialogs::Notification::setup(APP_ID);
333///
334/// rustydialogs::Notification {
335/// 	app_id: APP_ID,
336/// 	title: "Task Complete",
337/// 	message: "All files were processed successfully.",
338/// 	icon: rustydialogs::MessageIcon::Info,
339/// 	timeout: rustydialogs::Notification::SHORT_TIMEOUT,
340/// }.show();
341/// ```
342#[derive(Copy, Clone, Debug)]
343pub struct Notification<'a> {
344	/// Application identifier used by notification backends.
345	///
346	/// This is a best-effort hint: some backends may ignore it, and some only honor the first value seen by the process/session.
347	pub app_id: &'a str,
348	/// The title of the notification.
349	pub title: &'a str,
350	/// The message to display in the notification.
351	pub message: &'a str,
352	/// The icon to show in the notification.
353	// Future: Change to optional Option<MessageIcon>
354	pub icon: MessageIcon,
355	/// Timeout in milliseconds after which the notification should automatically close.
356	///
357	/// A value less than or equal to `0` means that the notification will not automatically close.
358	///
359	/// This is a best-effort hint: some backends may ignore it and use their own default timeout, or may not support timeouts at all.
360	// Future: Change to dedicated duration type
361	pub timeout: i32,
362}
363
364impl<'a> Notification<'a> {
365	/// Short timeout duration in milliseconds for notification popups.
366	pub const SHORT_TIMEOUT: i32 = 5000;
367	/// Long timeout duration in milliseconds for notification popups.
368	pub const LONG_TIMEOUT: i32 = 10000;
369
370	/// Perform any necessary setup for notifications, such as registering the application with the notification system.
371	///
372	/// This step is optional, when skipped the library will attempt to perform any necessary setup automatically when showing the first notification,
373	/// but this method can be used to ensure that the setup is done at a specific time in the application lifecycle.
374	///
375	/// ### Windows
376	///
377	/// By default, this initializes a process-wide tray icon used for balloon notifications.
378	///
379	/// When using the `winrt-toast` backend, this creates a Start Menu shortcut for the application with the provided application identifier, which is required for showing toast notifications on Windows.
380	/// It is recommended to call this method during application initialization before showing any notifications or the first notification may be skipped due to delays in the shortcut creation process.
381	///
382	/// ### Linux
383	///
384	/// When using the `libnotify` backend, this registers the application with the notification system using the provided application identifier.
385	#[inline]
386	pub fn setup(app_id: &str) {
387		// Future: Return whether setup was successful (API breaking change)
388		notify_setup(app_id);
389	}
390
391	/// Show the notification.
392	#[inline]
393	pub fn show(&self) {
394		notify(self)
395	}
396}
397
398#[cfg(windows)]
399mod win32;
400#[cfg(windows)]
401use win32::*;
402
403#[cfg(any(
404	target_os = "linux",
405	target_os = "freebsd",
406	target_os = "dragonfly",
407	target_os = "netbsd",
408	target_os = "openbsd",
409))]
410mod linux;
411#[cfg(any(
412	target_os = "linux",
413	target_os = "freebsd",
414	target_os = "dragonfly",
415	target_os = "netbsd",
416	target_os = "openbsd",
417))]
418use linux::*;
419
420#[cfg(target_os = "macos")]
421mod macos;
422#[cfg(target_os = "macos")]
423use macos::*;
424
425#[cfg(not(any(
426	windows,
427	target_os = "linux",
428	target_os = "freebsd",
429	target_os = "dragonfly",
430	target_os = "netbsd",
431	target_os = "openbsd",
432	target_os = "macos",
433)))]
434mod unsupported;
435#[cfg(not(any(
436	windows,
437	target_os = "linux",
438	target_os = "freebsd",
439	target_os = "dragonfly",
440	target_os = "netbsd",
441	target_os = "openbsd",
442	target_os = "macos",
443)))]
444use unsupported::*;