appcui/dialogs.rs
1//! Dialog system for AppCUI applications.
2//!
3//! This module provides a set of predefined modal windows that are common when using a UI system:
4//! * **Notification dialogs** - Show errors, warnings, messages, or ask for validation
5//! * **File dialogs** - Allow users to select files to open or save
6//! * **Folder selection dialogs** - Allow users to select folders
7//!
8//! # Notification Dialogs
9//!
10//! The module provides several functions for displaying notifications with different severity levels:
11//! * [`error`] - Shows an error message with an "Ok" button
12//! * [`retry`] - Shows an error message with "Retry" and "Cancel" buttons
13//! * [`alert`] - Shows a warning message with an "Ok" button
14//! * [`proceed`] - Shows a warning message with "Yes" and "No" buttons
15//! * [`message`] - Shows an information message with an "Ok" button
16//! * [`validate`] - Shows a question with "Yes" and "No" buttons
17//! * [`validate_or_cancel`] - Shows a question with "Yes", "No", and "Cancel" buttons
18//!
19//! # File Dialogs
20//!
21//! For file operations, the module offers:
22//! * [`open`] - A dialog for selecting a file to open
23//! * [`save`] - A dialog for selecting a location to save a file
24//!
25//! # Folder Selection Dialogs
26//!
27//! For folder selection:
28//! * [`select_folder`] - A dialog for selecting a folder
29//!
30//! # Examples
31//!
32//! ```rust,no_run
33//! use appcui::dialogs;
34//!
35//! // Show a simple error message
36//! dialogs::error("Error", "An error has occurred");
37//!
38//! // Ask the user a yes/no question
39//! if dialogs::validate("Confirm", "Do you want to proceed?") {
40//! // User clicked "Yes"
41//! } else {
42//! // User clicked "No" or closed the dialog
43//! }
44//!
45//! // Open a file dialog
46//! if let Some(file_path) = dialogs::open("Open File",
47//! "document.txt",
48//! dialogs::Location::Current,
49//! Some("Text files = [txt]"),
50//! dialogs::OpenFileDialogFlags::Icons)
51//! {
52//! // User selected a file
53//! println!("Selected file: {:?}", file_path);
54//! }
55//! ```
56mod dialog_buttons;
57mod dialog_result;
58mod extension_selection_dialog;
59mod file_mask;
60mod folder_select_dialog;
61mod generic_alert_dialog;
62mod input_dialog;
63mod open_save_dialog;
64mod root_select_dialog;
65#[cfg(test)]
66mod tests;
67
68use std::{path::{Path, PathBuf}, str::FromStr};
69
70use crate::{
71 prelude::{window, ModalWindowMethods},
72 utils::{self, Navigator},
73};
74use dialog_buttons::DialogButtons;
75use dialog_result::DialogResult;
76use file_mask::FileMask;
77use folder_select_dialog::{FolderExplorer, FolderSelectionDialogResult};
78use generic_alert_dialog::GenericAlertDialog;
79use input_dialog::StringImputDialog;
80use open_save_dialog::{FileExplorer, OpenSaveDialogResult};
81use EnumBitFlags::EnumBitFlags;
82
83/// Result of a validation dialog with a cancel option.
84///
85/// This enum represents the possible outcomes when a dialog with "Yes", "No",
86/// and "Cancel" buttons is displayed.
87///
88/// # Values
89/// * `Yes` - The user clicked the "Yes" button.
90/// * `No` - The user clicked the "No" button.
91/// * `Cancel` - The user clicked the "Cancel" button or closed the dialog.
92#[derive(Copy, Clone, PartialEq, Eq)]
93pub enum ValidateOrCancelResult {
94 Yes,
95 No,
96 Cancel,
97}
98
99/// Displays an error dialog with an "Ok" button.
100///
101/// This function shows a modal error dialog with the specified title and message.
102/// The dialog will have a single "Ok" button and will block until the user dismisses it.
103///
104/// # Arguments
105/// * `title` - The title of the dialog.
106/// * `caption` - The message to display in the dialog.
107///
108/// # Example
109/// ```rust,no_run
110/// use appcui::dialogs;
111///
112/// dialogs::error("Error", "An error has occurred during the last operation");
113/// ```
114pub fn error(title: &str, caption: &str) {
115 let w = GenericAlertDialog::new(title, caption, DialogButtons::Ok, window::Background::Error);
116 w.show();
117}
118
119/// Displays an error dialog with "Retry" and "Cancel" buttons.
120///
121/// This function shows a modal error dialog with the specified title and message.
122/// The dialog will have "Retry" and "Cancel" buttons and will block until the user
123/// makes a selection.
124///
125/// # Arguments
126/// * `title` - The title of the dialog.
127/// * `caption` - The message to display in the dialog.
128///
129/// # Returns
130/// * `true` - If the user clicked the "Retry" button.
131/// * `false` - If the user clicked the "Cancel" button or closed the dialog.
132///
133/// # Example
134/// ```rust,no_run
135/// use appcui::dialogs;
136///
137/// if dialogs::retry("Error", "An error occurred while performing a copy operation.\nRetry again?") {
138/// // Retry the operation
139/// }
140/// ```
141pub fn retry(title: &str, caption: &str) -> bool {
142 let w = GenericAlertDialog::new(title, caption, DialogButtons::RetryCancel, window::Background::Error);
143 if let Some(result) = w.show() {
144 return result == DialogResult::Retry;
145 }
146 false
147}
148
149/// Displays an alert dialog with an "Ok" button.
150///
151/// This function shows a modal warning dialog with the specified title and message.
152/// The dialog will have a single "Ok" button and will block until the user dismisses it.
153///
154/// # Arguments
155/// * `title` - The title of the dialog.
156/// * `caption` - The message to display in the dialog.
157///
158/// # Example
159/// ```rust,no_run
160/// use appcui::dialogs;
161///
162/// dialogs::alert("Warning", "Low disk space detected");
163/// ```
164pub fn alert(title: &str, caption: &str) {
165 let w = GenericAlertDialog::new(title, caption, DialogButtons::Ok, window::Background::Warning);
166 w.show();
167}
168
169/// Displays an alert dialog with "Yes" and "No" buttons.
170///
171/// This function shows a modal warning dialog with the specified title and message.
172/// The dialog will have "Yes" and "No" buttons and will block until the user
173/// makes a selection.
174///
175/// # Arguments
176/// * `title` - The title of the dialog.
177/// * `caption` - The message to display in the dialog.
178///
179/// # Returns
180/// * `true` - If the user clicked the "Yes" button.
181/// * `false` - If the user clicked the "No" button or closed the dialog.
182///
183/// # Example
184/// ```rust,no_run
185/// use appcui::dialogs;
186///
187/// if dialogs::proceed("Warning", "An error occurred while performing a copy operation.\nContinue anyway?") {
188/// // Continue with the operation
189/// }
190/// ```
191pub fn proceed(title: &str, caption: &str) -> bool {
192 let w = GenericAlertDialog::new(title, caption, DialogButtons::YesNo, window::Background::Warning);
193 if let Some(result) = w.show() {
194 return result == DialogResult::Yes;
195 }
196 false
197}
198
199/// Displays a notification dialog with an "Ok" button.
200///
201/// This function shows a modal notification dialog with the specified title and message.
202/// The dialog will have a single "Ok" button and will block until the user dismisses it.
203///
204/// # Arguments
205/// * `title` - The title of the dialog.
206/// * `caption` - The message to display in the dialog.
207///
208/// # Example
209/// ```rust,no_run
210/// use appcui::dialogs;
211///
212/// dialogs::message("Success", "All files have been copied");
213/// ```
214pub fn message(title: &str, caption: &str) {
215 let w = GenericAlertDialog::new(title, caption, DialogButtons::Ok, window::Background::Notification);
216 w.show();
217}
218
219/// Displays a validation dialog with "Yes" and "No" buttons.
220///
221/// This function shows a modal notification dialog with the specified title and message.
222/// The dialog will have "Yes" and "No" buttons and will block until the user
223/// makes a selection.
224///
225/// # Arguments
226/// * `title` - The title of the dialog.
227/// * `caption` - The message to display in the dialog.
228///
229/// # Returns
230/// * `true` - If the user clicked the "Yes" button.
231/// * `false` - If the user clicked the "No" button or closed the dialog.
232///
233/// # Example
234/// ```rust,no_run
235/// use appcui::dialogs;
236///
237/// if dialogs::validate("Question", "Are you sure you want to proceed?") {
238/// // Start the action
239/// }
240/// ```
241pub fn validate(title: &str, caption: &str) -> bool {
242 let w = GenericAlertDialog::new(title, caption, DialogButtons::YesNo, window::Background::Notification);
243 if let Some(result) = w.show() {
244 return result == DialogResult::Yes;
245 }
246 false
247}
248
249/// Displays a validation dialog with "Yes", "No", and "Cancel" buttons.
250///
251/// This function shows a modal notification dialog with the specified title and message.
252/// The dialog will have "Yes", "No", and "Cancel" buttons and will block until the user
253/// makes a selection.
254///
255/// # Arguments
256/// * `title` - The title of the dialog.
257/// * `caption` - The message to display in the dialog.
258///
259/// # Returns
260/// A `ValidateOrCancelResult` indicating which button was clicked:
261/// * `ValidateOrCancelResult::Yes` - If the user clicked the "Yes" button.
262/// * `ValidateOrCancelResult::No` - If the user clicked the "No" button.
263/// * `ValidateOrCancelResult::Cancel` - If the user clicked the "Cancel" button or closed the dialog.
264///
265/// # Example
266/// ```rust,no_run
267/// use appcui::dialogs;
268/// use appcui::dialogs::ValidateOrCancelResult;
269///
270/// let result = dialogs::validate_or_cancel("Exit", "Do you want to save your files?");
271/// match result {
272/// ValidateOrCancelResult::Yes => { /* save files and then exit application */ },
273/// ValidateOrCancelResult::No => { /* exit the application directly */ },
274/// ValidateOrCancelResult::Cancel => { /* don't exit the application */ }
275/// }
276/// ```
277pub fn validate_or_cancel(title: &str, caption: &str) -> ValidateOrCancelResult {
278 let w = GenericAlertDialog::new(title, caption, DialogButtons::YesNoCancel, window::Background::Notification);
279 match w.show() {
280 Some(DialogResult::Yes) => ValidateOrCancelResult::Yes,
281 Some(DialogResult::No) => ValidateOrCancelResult::No,
282 _ => ValidateOrCancelResult::Cancel,
283 }
284}
285
286/// Specifies the initial location for file and folder selection dialogs.
287///
288/// This enum represents different ways to specify where file and folder
289/// selection dialogs should start browsing.
290///
291/// # Variants
292/// * `Current` - Start in the current working directory.
293/// * `Last` - Start in the last location used in a previous dialog. If no previous dialog
294/// has been opened, falls back to the current directory.
295/// * `Path` - Start in the specified path.
296///
297/// # Example
298/// ```rust,no_run
299/// use appcui::dialogs;
300/// use std::path::Path;
301///
302/// // Start in a specific directory
303/// let specific_path = Path::new("C:/Users/Documents");
304/// let location = dialogs::Location::Path(specific_path);
305///
306/// // Start in the current directory
307/// let current_location = dialogs::Location::Current;
308///
309/// // Start in the last used location
310/// let last_location = dialogs::Location::Last;
311/// ```
312#[derive(Clone)]
313pub enum Location<'a> {
314 Current,
315 Last,
316 Path(&'a Path),
317}
318
319#[EnumBitFlags(bits = 8)]
320pub enum SaveFileDialogFlags {
321 Icons = 1,
322 ValidateOverwrite = 2,
323}
324
325#[EnumBitFlags(bits = 8)]
326pub enum OpenFileDialogFlags {
327 Icons = 1,
328 CheckIfFileExists = 2,
329}
330
331#[EnumBitFlags(bits = 8)]
332pub enum SelectFolderDialogFlags {
333 Icons = 1,
334}
335
336pub(super) fn inner_save<T>(
337 title: &str,
338 file_name: &str,
339 location: Location,
340 extension_mask: Option<&str>,
341 flags: SaveFileDialogFlags,
342 nav: T,
343) -> Option<PathBuf>
344where
345 T: crate::utils::Navigator<crate::utils::fs::Entry, crate::utils::fs::Root, PathBuf> + 'static,
346{
347 let ext_mask = extension_mask.unwrap_or_default();
348 match FileMask::parse(ext_mask) {
349 Ok(mask_list) => {
350 let mut inner_flags = open_save_dialog::InnerFlags::Save;
351 if flags.contains(SaveFileDialogFlags::Icons) {
352 inner_flags |= open_save_dialog::InnerFlags::Icons;
353 }
354 if flags.contains(SaveFileDialogFlags::ValidateOverwrite) {
355 inner_flags |= open_save_dialog::InnerFlags::ValidateOverwrite;
356 }
357
358 let w = FileExplorer::new(file_name, title, location, mask_list, nav, inner_flags);
359 let result = w.show();
360 match result {
361 Some(OpenSaveDialogResult::Path(path)) => Some(path),
362 _ => None,
363 }
364 }
365 Err(err_msg) => {
366 panic!(
367 "Error parsing file mask: '{ext_mask}'. It should be in the format 'name1 = [ext1, ext2, ... extn], name2 = [...], ...'.\n{err_msg}"
368 );
369 }
370 }
371}
372
373pub(super) fn inner_open<T>(
374 title: &str,
375 file_name: &str,
376 location: Location,
377 extension_mask: Option<&str>,
378 flags: OpenFileDialogFlags,
379 nav: T,
380) -> Option<PathBuf>
381where
382 T: crate::utils::Navigator<crate::utils::fs::Entry, crate::utils::fs::Root, PathBuf> + 'static,
383{
384 let ext_mask = extension_mask.unwrap_or_default();
385 match FileMask::parse(ext_mask) {
386 Ok(mask_list) => {
387 let mut inner_flags = open_save_dialog::InnerFlags::None;
388 if flags.contains(OpenFileDialogFlags::Icons) {
389 inner_flags |= open_save_dialog::InnerFlags::Icons;
390 }
391 if flags.contains(OpenFileDialogFlags::CheckIfFileExists) {
392 inner_flags |= open_save_dialog::InnerFlags::CheckIfFileExists;
393 }
394
395 let w = FileExplorer::new(file_name, title, location, mask_list, nav, inner_flags);
396 let result = w.show();
397 match result {
398 Some(OpenSaveDialogResult::Path(path)) => Some(path),
399 _ => None,
400 }
401 }
402 Err(err_msg) => {
403 panic!(
404 "Error parsing file mask: '{ext_mask}'. It should be in the format 'name1 = [ext1, ext2, ... extn], name2 = [...], ...'.\n{err_msg}"
405 );
406 }
407 }
408}
409
410pub(super) fn inner_select_folder<T>(title: &str, location: Location, flags: SelectFolderDialogFlags, nav: T) -> Option<PathBuf>
411where
412 T: crate::utils::Navigator<crate::utils::fs::Entry, crate::utils::fs::Root, PathBuf> + 'static,
413{
414 let w = FolderExplorer::new(title, location, nav, flags);
415 let result = w.show();
416 match result {
417 Some(FolderSelectionDialogResult::Path(path)) => Some(path),
418 _ => None,
419 }
420}
421
422pub(crate) fn clear_last_path() {
423 if let Some(m) = open_save_dialog::LAST_PATH.get() {
424 if let Ok(mut guard) = m.lock() {
425 *guard = None;
426 }
427 }
428 if let Some(m) = folder_select_dialog::FOLDER_LAST_PATH.get() {
429 if let Ok(mut guard) = m.lock() {
430 *guard = None;
431 }
432 }
433}
434
435/// Opens a file dialog for saving a file and returns the path of the file selected by the user or None if the user canceled the operation.
436/// # Arguments
437/// * `title` - The title of the dialog.
438/// * `file_name` - The default file name.
439/// * `location` - The initial location of the dialog (one of Current, Last or Path). If Last is used, the dialog will open in the last location used by the user.
440/// * `extension_mask` - A string that specifies the file extensions that can be selected by the user. The format is `name1 = [ext1, ext2, ... extn], name2 = [...], ...`. If None is provided, all files will be displayed.
441/// * `flags` - Flags that specify the behavior of the dialog.
442///
443/// # Example
444/// ```rust,no_run
445/// use appcui::dialogs;
446///
447/// if let Some(path) = dialogs::save("Save file",
448/// "file.txt",
449/// dialogs::Location::Current,
450/// Some("Text files = [txt]"),
451/// dialogs::SaveFileDialogFlags::Icons)
452/// {
453/// println!("File saved at: {:?}", path);
454/// }
455/// ```
456pub fn save(title: &str, file_name: &str, location: Location, extension_mask: Option<&str>, flags: SaveFileDialogFlags) -> Option<PathBuf> {
457 inner_save(title, file_name, location, extension_mask, flags, utils::fs::Navigator::new())
458}
459
460/// Opens a file dialog for opening a file and returns the path of the file selected by the user or None if the user canceled the operation.
461/// # Arguments
462/// * `title` - The title of the dialog.
463/// * `file_name` - The default file name.
464/// * `location` - The initial location of the dialog (one of Current, Last or Path). If Last is used, the dialog will open in the last location used by the user.
465/// * `extension_mask` - A string that specifies the file extensions that can be selected by the user. The format is `name1 = [ext1, ext2, ... extn], name2 = [...], ...`. If None is provided, all files will be displayed.
466/// * `flags` - Flags that specify the behavior of the dialog.
467///
468/// # Example
469/// ```rust,no_run
470/// use appcui::dialogs;
471///
472/// if let Some(path) = dialogs::open("Open file",
473/// "file.txt",
474/// dialogs::Location::Current,
475/// Some("Text files = [txt]"),
476/// dialogs::OpenFileDialogFlags::Icons)
477/// {
478/// println!("File opened: {:?}", path);
479/// }
480/// ```
481pub fn open(title: &str, file_name: &str, location: Location, extension_mask: Option<&str>, flags: OpenFileDialogFlags) -> Option<PathBuf> {
482 inner_open(title, file_name, location, extension_mask, flags, utils::fs::Navigator::new())
483}
484
485/// Opens a dialog for selecting a folder and returns the path of the folder selected by the user or None if the user canceled the operation.
486/// # Arguments
487/// * `title` - The title of the dialog.
488/// * `location` - The initial location of the dialog (one of Current, Last or Path). If Last is used, the dialog will open in the last location used by the user.
489/// * `flags` - Flags that specify the behavior of the dialog (ex: display icons).
490///
491/// # Example
492/// ```rust,no_run
493/// use appcui::dialogs;
494///
495/// if let Some(path) = dialogs::select_folder("Select folder",
496/// dialogs::Location::Current,
497/// dialogs::SelectFolderDialogFlags::Icons)
498/// {
499/// println!("Folder selected: {:?}", path);
500/// }
501/// ```
502pub fn select_folder(title: &str, location: Location, flags: SelectFolderDialogFlags) -> Option<PathBuf> {
503 inner_select_folder(title, location, flags, utils::fs::Navigator::new())
504}
505
506
507type InputCallback<T> = fn(&T) -> Result<(), String>;
508
509/// Opens an input dialog for entering a value of type T and returns the value entered by the user or None if the user canceled the operation.
510/// # Arguments
511/// * `title` - The title of the dialog.
512/// * `text` - The text to display in the dialog.
513/// * `value` - An optional value to pre-fill the input field with.
514/// * `validation` - An optional validation function that can be used to validate the input value.
515///
516/// # Example
517/// ```rust,no_run
518/// use appcui::dialogs;
519///
520/// if let Some(res) = dialogs::input::<i32>("Title", "Enter a value", None, None) {
521/// // res value contains the selected value
522/// } else {
523/// // the user canceled the dialog
524/// }
525/// ```
526pub fn input<T>(title: &str, text: &str, value: Option<T>, validation: Option<InputCallback<T>>) -> Option<T>
527where
528 T: FromStr + Sized + std::fmt::Display + 'static,
529{
530 StringImputDialog::new(title, text, value, validation).show()
531}