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::*;