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}