dear_file_browser/
core.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Dialog mode
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum DialogMode {
7    /// Pick a single file
8    OpenFile,
9    /// Pick multiple files
10    OpenFiles,
11    /// Pick a directory
12    PickFolder,
13    /// Save file
14    SaveFile,
15}
16
17/// Backend preference
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Backend {
20    /// Prefer native when available, fallback to ImGui
21    Auto,
22    /// Force native (rfd) backend
23    Native,
24    /// Force ImGui UI backend
25    ImGui,
26}
27
28impl Default for Backend {
29    fn default() -> Self {
30        Backend::Auto
31    }
32}
33
34/// File filter (e.g., "Images" -> ["png", "jpg"]).
35///
36/// Extensions are matched case-insensitively and should be provided without a
37/// leading dot. The variants created from tuples will be normalized to
38/// lowercase automatically.
39#[derive(Clone, Debug, Default)]
40pub struct FileFilter {
41    /// Filter display name
42    pub name: String,
43    /// Lower-case extensions without dot (e.g., "png")
44    pub extensions: Vec<String>,
45}
46
47impl FileFilter {
48    /// Create a filter from a name and extensions.
49    ///
50    /// Extensions should be provided without dots (e.g. "png"). Matching is
51    /// case-insensitive.
52    pub fn new(name: impl Into<String>, exts: impl Into<Vec<String>>) -> Self {
53        Self {
54            name: name.into(),
55            extensions: exts.into(),
56        }
57    }
58}
59
60impl From<(&str, &[&str])> for FileFilter {
61    fn from(value: (&str, &[&str])) -> Self {
62        Self {
63            name: value.0.to_owned(),
64            extensions: value.1.iter().map(|s| s.to_lowercase()).collect(),
65        }
66    }
67}
68
69/// Selection result containing one or more paths
70#[derive(Clone, Debug, Default)]
71pub struct Selection {
72    /// Selected filesystem paths
73    pub paths: Vec<PathBuf>,
74}
75
76/// Errors returned by file dialogs and in-UI browser
77#[derive(Error, Debug)]
78pub enum FileDialogError {
79    /// User cancelled the dialog / browser
80    #[error("cancelled")]
81    Cancelled,
82    /// I/O error
83    #[error("io error: {0}")]
84    Io(#[from] std::io::Error),
85    /// Requested operation unsupported by the chosen backend
86    #[error("unsupported operation for backend")]
87    Unsupported,
88    /// Invalid or non-existing path requested
89    #[error("invalid path: {0}")]
90    InvalidPath(String),
91    /// Platform-specific error or general failure
92    #[error("internal error: {0}")]
93    Internal(String),
94}
95
96/// Click behavior for directory rows
97#[derive(Copy, Clone, Debug, PartialEq, Eq)]
98pub enum ClickAction {
99    /// Clicking a directory only selects it
100    Select,
101    /// Clicking a directory navigates into it
102    Navigate,
103}
104
105/// Layout style for the in-UI file browser
106#[derive(Copy, Clone, Debug, PartialEq, Eq)]
107pub enum LayoutStyle {
108    /// Standard layout with quick locations + file list
109    Standard,
110    /// Minimal layout with a single file list pane
111    Minimal,
112}
113
114/// Sort keys for file list
115#[derive(Copy, Clone, Debug, PartialEq, Eq)]
116pub enum SortBy {
117    /// Sort by file or directory name
118    Name,
119    /// Sort by file size (directories first)
120    Size,
121    /// Sort by last modified time
122    Modified,
123}
124
125/// Builder for launching file dialogs
126#[derive(Clone, Debug)]
127pub struct FileDialog {
128    pub(crate) backend: Backend,
129    pub(crate) mode: DialogMode,
130    pub(crate) start_dir: Option<PathBuf>,
131    pub(crate) default_name: Option<String>,
132    pub(crate) allow_multi: bool,
133    pub(crate) filters: Vec<FileFilter>,
134    pub(crate) show_hidden: bool,
135}
136
137impl FileDialog {
138    /// Create a new builder with the given mode
139    pub fn new(mode: DialogMode) -> Self {
140        Self {
141            backend: Backend::Auto,
142            mode,
143            start_dir: None,
144            default_name: None,
145            allow_multi: matches!(mode, DialogMode::OpenFiles),
146            filters: Vec::new(),
147            show_hidden: false,
148        }
149    }
150
151    /// Choose a backend (Auto by default)
152    pub fn backend(mut self, backend: Backend) -> Self {
153        self.backend = backend;
154        self
155    }
156    /// Set initial directory
157    pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
158        self.start_dir = Some(dir.into());
159        self
160    }
161    /// Set default file name (for SaveFile)
162    pub fn default_file_name(mut self, name: impl Into<String>) -> Self {
163        self.default_name = Some(name.into());
164        self
165    }
166    /// Allow multi selection (only for OpenFiles)
167    pub fn multi_select(mut self, yes: bool) -> Self {
168        self.allow_multi = yes;
169        self
170    }
171    /// Show hidden files in ImGui browser (native follows OS behavior)
172    pub fn show_hidden(mut self, yes: bool) -> Self {
173        self.show_hidden = yes;
174        self
175    }
176    /// Add a filter.
177    ///
178    /// Examples
179    /// ```
180    /// use dear_file_browser::{FileDialog, DialogMode};
181    /// let d = FileDialog::new(DialogMode::OpenFile)
182    ///     .filter(("Images", &["png", "jpg"]))
183    ///     .filter(("Rust", &["rs"]))
184    ///     .show_hidden(true);
185    /// ```
186    pub fn filter<F: Into<FileFilter>>(mut self, filter: F) -> Self {
187        self.filters.push(filter.into());
188        self
189    }
190    /// Add multiple filters.
191    ///
192    /// The list will be appended to any previously-added filters. Extensions
193    /// are compared case-insensitively and should be provided without dots.
194    ///
195    /// Examples
196    /// ```
197    /// use dear_file_browser::{FileDialog, DialogMode, FileFilter};
198    /// let filters = vec![
199    ///     FileFilter::from(("Images", &["png", "jpg", "jpeg"]))
200    /// ];
201    /// let d = FileDialog::new(DialogMode::OpenFiles)
202    ///     .filters(filters)
203    ///     .multi_select(true);
204    /// ```
205    pub fn filters<I, F>(mut self, filters: I) -> Self
206    where
207        I: IntoIterator<Item = F>,
208        F: Into<FileFilter>,
209    {
210        self.filters.extend(filters.into_iter().map(Into::into));
211        self
212    }
213
214    /// Resolve the effective backend
215    pub(crate) fn effective_backend(&self) -> Backend {
216        match self.backend {
217            Backend::Native => Backend::Native,
218            Backend::ImGui => Backend::ImGui,
219            Backend::Auto => {
220                #[cfg(feature = "native-rfd")]
221                {
222                    return Backend::Native;
223                }
224                #[cfg(not(feature = "native-rfd"))]
225                {
226                    return Backend::ImGui;
227                }
228            }
229        }
230    }
231}
232
233// Default stubs when native feature is disabled
234#[cfg(not(feature = "native-rfd"))]
235impl FileDialog {
236    /// Open a dialog synchronously (blocking). Unsupported without `native-rfd`.
237    pub fn open_blocking(self) -> Result<Selection, FileDialogError> {
238        Err(FileDialogError::Unsupported)
239    }
240    /// Open a dialog asynchronously. Unsupported without `native-rfd`.
241    pub async fn open_async(self) -> Result<Selection, FileDialogError> {
242        Err(FileDialogError::Unsupported)
243    }
244}