1use std::path::PathBuf;
2use std::time::Duration;
3
4use dear_imgui_rs::FontId;
5
6use crate::core::{ClickAction, DialogMode, LayoutStyle};
7use crate::dialog_core::{EntryId, FileDialogCore, ScanPolicy, ScanStatus};
8use crate::file_style::FileStyleRegistry;
9use crate::thumbnails::{ThumbnailCache, ThumbnailCacheConfig};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum FileListViewMode {
14 List,
16 ThumbnailsList,
20 Grid,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum FileListDataColumn {
27 Name,
29 Extension,
31 Size,
33 Modified,
35}
36
37impl FileListDataColumn {
38 fn compact_token(self) -> &'static str {
39 match self {
40 Self::Name => "name",
41 Self::Extension => "ext",
42 Self::Size => "size",
43 Self::Modified => "modified",
44 }
45 }
46
47 fn from_compact_token(token: &str) -> Option<Self> {
48 match token {
49 "name" => Some(Self::Name),
50 "ext" => Some(Self::Extension),
51 "size" => Some(Self::Size),
52 "modified" => Some(Self::Modified),
53 _ => None,
54 }
55 }
56}
57
58#[derive(Clone, Debug, PartialEq)]
63pub struct FileListColumnWeightOverrides {
64 pub preview: Option<f32>,
66 pub name: Option<f32>,
68 pub extension: Option<f32>,
70 pub size: Option<f32>,
72 pub modified: Option<f32>,
74}
75
76impl Default for FileListColumnWeightOverrides {
77 fn default() -> Self {
78 Self {
79 preview: None,
80 name: None,
81 extension: None,
82 size: None,
83 modified: None,
84 }
85 }
86}
87
88#[derive(Clone, Debug, PartialEq)]
90pub struct FileListColumnsConfig {
91 pub show_preview: bool,
93 pub show_extension: bool,
95 pub show_size: bool,
97 pub show_modified: bool,
99 pub order: [FileListDataColumn; 4],
103 pub weight_overrides: FileListColumnWeightOverrides,
105}
106
107#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct FileListColumnsDeserializeError {
110 msg: String,
111}
112
113impl FileListColumnsDeserializeError {
114 fn new(msg: impl Into<String>) -> Self {
115 Self { msg: msg.into() }
116 }
117}
118
119impl std::fmt::Display for FileListColumnsDeserializeError {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "file list columns deserialize error: {}", self.msg)
122 }
123}
124
125impl std::error::Error for FileListColumnsDeserializeError {}
126
127impl FileListColumnsConfig {
128 pub fn serialize_compact(&self) -> String {
132 let order = self
133 .normalized_order()
134 .iter()
135 .map(|c| c.compact_token())
136 .collect::<Vec<_>>()
137 .join(",");
138 let weights = [
139 self.weight_overrides.preview,
140 self.weight_overrides.name,
141 self.weight_overrides.extension,
142 self.weight_overrides.size,
143 self.weight_overrides.modified,
144 ]
145 .into_iter()
146 .map(|v| {
147 v.map(|w| format!("{w:.4}"))
148 .unwrap_or_else(|| "auto".to_string())
149 })
150 .collect::<Vec<_>>()
151 .join(",");
152
153 format!(
154 "v1;preview={};ext={};size={};modified={};order={};weights={}",
155 u8::from(self.show_preview),
156 u8::from(self.show_extension),
157 u8::from(self.show_size),
158 u8::from(self.show_modified),
159 order,
160 weights,
161 )
162 }
163
164 pub fn deserialize_compact(input: &str) -> Result<Self, FileListColumnsDeserializeError> {
166 let mut version_ok = false;
167 let mut preview = None;
168 let mut ext = None;
169 let mut size = None;
170 let mut modified = None;
171 let mut order = None;
172 let mut weights = None;
173
174 for token in input.split(';').filter(|s| !s.trim().is_empty()) {
175 if token == "v1" {
176 version_ok = true;
177 continue;
178 }
179 if token.starts_with('v') {
180 return Err(FileListColumnsDeserializeError::new(format!(
181 "unsupported version token `{token}`"
182 )));
183 }
184 let (key, value) = token.split_once('=').ok_or_else(|| {
185 FileListColumnsDeserializeError::new(format!("invalid token `{token}`"))
186 })?;
187 match key {
188 "preview" => preview = Some(parse_compact_bool(value)?),
189 "ext" => ext = Some(parse_compact_bool(value)?),
190 "size" => size = Some(parse_compact_bool(value)?),
191 "modified" => modified = Some(parse_compact_bool(value)?),
192 "order" => order = Some(parse_compact_order(value)?),
193 "weights" => weights = Some(parse_compact_weights(value)?),
194 _ => {
195 return Err(FileListColumnsDeserializeError::new(format!(
196 "unknown key `{key}`"
197 )));
198 }
199 }
200 }
201
202 if !version_ok {
203 return Err(FileListColumnsDeserializeError::new(
204 "missing or unsupported version token",
205 ));
206 }
207
208 Ok(Self {
209 show_preview: preview
210 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `preview`"))?,
211 show_extension: ext
212 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `ext`"))?,
213 show_size: size
214 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `size`"))?,
215 show_modified: modified
216 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `modified`"))?,
217 order: order
218 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `order`"))?,
219 weight_overrides: weights
220 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `weights`"))?,
221 })
222 }
223
224 pub fn normalized_order(&self) -> [FileListDataColumn; 4] {
226 normalized_order(self.order)
227 }
228}
229
230impl Default for FileListColumnsConfig {
231 fn default() -> Self {
232 Self {
233 show_preview: true,
234 show_extension: true,
235 show_size: true,
236 show_modified: true,
237 order: [
238 FileListDataColumn::Name,
239 FileListDataColumn::Extension,
240 FileListDataColumn::Size,
241 FileListDataColumn::Modified,
242 ],
243 weight_overrides: FileListColumnWeightOverrides::default(),
244 }
245 }
246}
247
248fn normalized_order(order: [FileListDataColumn; 4]) -> [FileListDataColumn; 4] {
249 let mut out = Vec::with_capacity(4);
250 for c in order {
251 if !out.contains(&c) {
252 out.push(c);
253 }
254 }
255 for c in [
256 FileListDataColumn::Name,
257 FileListDataColumn::Extension,
258 FileListDataColumn::Size,
259 FileListDataColumn::Modified,
260 ] {
261 if !out.contains(&c) {
262 out.push(c);
263 }
264 }
265 [out[0], out[1], out[2], out[3]]
266}
267
268fn parse_compact_bool(value: &str) -> Result<bool, FileListColumnsDeserializeError> {
269 match value {
270 "0" => Ok(false),
271 "1" => Ok(true),
272 _ => Err(FileListColumnsDeserializeError::new(format!(
273 "invalid bool value `{value}`"
274 ))),
275 }
276}
277
278fn parse_compact_order(
279 value: &str,
280) -> Result<[FileListDataColumn; 4], FileListColumnsDeserializeError> {
281 let cols = value
282 .split(',')
283 .map(FileListDataColumn::from_compact_token)
284 .collect::<Option<Vec<_>>>()
285 .ok_or_else(|| FileListColumnsDeserializeError::new("invalid column token in `order`"))?;
286 if cols.len() != 4 {
287 return Err(FileListColumnsDeserializeError::new(
288 "`order` must contain exactly 4 columns",
289 ));
290 }
291 let order = [cols[0], cols[1], cols[2], cols[3]];
292 let normalized = normalized_order(order);
293 if normalized != order {
294 return Err(FileListColumnsDeserializeError::new(
295 "`order` must contain each column exactly once",
296 ));
297 }
298 Ok(order)
299}
300
301fn parse_compact_optional_weight(
302 value: &str,
303) -> Result<Option<f32>, FileListColumnsDeserializeError> {
304 if value.eq_ignore_ascii_case("auto") {
305 return Ok(None);
306 }
307 let parsed = value.parse::<f32>().map_err(|_| {
308 FileListColumnsDeserializeError::new(format!("invalid weight value `{value}`"))
309 })?;
310 if !parsed.is_finite() || parsed <= 0.0 {
311 return Err(FileListColumnsDeserializeError::new(format!(
312 "weight must be finite and > 0, got `{value}`"
313 )));
314 }
315 Ok(Some(parsed))
316}
317
318fn parse_compact_weights(
319 value: &str,
320) -> Result<FileListColumnWeightOverrides, FileListColumnsDeserializeError> {
321 let parts: Vec<&str> = value.split(',').collect();
322 if parts.len() != 5 {
323 return Err(FileListColumnsDeserializeError::new(
324 "`weights` must contain exactly 5 values",
325 ));
326 }
327 Ok(FileListColumnWeightOverrides {
328 preview: parse_compact_optional_weight(parts[0])?,
329 name: parse_compact_optional_weight(parts[1])?,
330 extension: parse_compact_optional_weight(parts[2])?,
331 size: parse_compact_optional_weight(parts[3])?,
332 modified: parse_compact_optional_weight(parts[4])?,
333 })
334}
335
336impl Default for FileListViewMode {
337 fn default() -> Self {
338 Self::List
339 }
340}
341
342#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
344pub enum ValidationButtonsAlign {
345 #[default]
347 Left,
348 Right,
350}
351
352#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
354pub enum ValidationButtonsOrder {
355 #[default]
357 ConfirmCancel,
358 CancelConfirm,
360}
361
362#[derive(Clone, Debug)]
364pub struct ValidationButtonsConfig {
365 pub align: ValidationButtonsAlign,
367 pub order: ValidationButtonsOrder,
369 pub confirm_label: Option<String>,
371 pub cancel_label: Option<String>,
373 pub button_width: Option<f32>,
375 pub confirm_width: Option<f32>,
377 pub cancel_width: Option<f32>,
379}
380
381impl Default for ValidationButtonsConfig {
382 fn default() -> Self {
383 Self {
384 align: ValidationButtonsAlign::Left,
385 order: ValidationButtonsOrder::ConfirmCancel,
386 confirm_label: None,
387 cancel_label: None,
388 button_width: None,
389 confirm_width: None,
390 cancel_width: None,
391 }
392 }
393}
394
395#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
397pub enum ToolbarDensity {
398 #[default]
400 Normal,
401 Compact,
403 Spacious,
405}
406
407#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
409pub enum ToolbarIconMode {
410 #[default]
412 Text,
413 IconOnly,
415 IconAndText,
417}
418
419#[derive(Clone, Debug, Default)]
421pub struct ToolbarIcons {
422 pub mode: ToolbarIconMode,
424 pub places: Option<String>,
426 pub refresh: Option<String>,
428 pub new_folder: Option<String>,
430 pub columns: Option<String>,
432 pub options: Option<String>,
434}
435
436#[derive(Clone, Debug)]
438pub struct ToolbarConfig {
439 pub density: ToolbarDensity,
441 pub icons: ToolbarIcons,
443 pub show_tooltips: bool,
445}
446
447impl Default for ToolbarConfig {
448 fn default() -> Self {
449 Self {
450 density: ToolbarDensity::Normal,
451 icons: ToolbarIcons::default(),
452 show_tooltips: true,
453 }
454 }
455}
456
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub enum ClipboardOp {
460 Copy,
462 Cut,
464}
465
466#[derive(Clone, Debug)]
468pub struct FileClipboard {
469 pub op: ClipboardOp,
471 pub sources: Vec<PathBuf>,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
477pub(crate) enum PasteConflictAction {
478 Overwrite,
480 Skip,
482 KeepBoth,
484}
485
486#[derive(Clone, Debug)]
488pub(crate) struct PasteConflictPrompt {
489 pub source: PathBuf,
491 pub dest: PathBuf,
493 pub apply_to_all: bool,
495}
496
497#[derive(Clone, Debug)]
499pub(crate) struct PendingPasteJob {
500 pub clipboard: FileClipboard,
502 pub dest_dir: PathBuf,
504 pub next_index: usize,
506 pub created: Vec<String>,
508 pub apply_all_conflicts: Option<PasteConflictAction>,
510 pub pending_conflict_action: Option<PasteConflictAction>,
512 pub conflict: Option<PasteConflictPrompt>,
514}
515
516#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
518pub(crate) enum PlacesIoMode {
519 #[default]
521 Export,
522 Import,
524}
525
526#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
528pub(crate) enum PlacesEditMode {
529 #[default]
531 AddGroup,
532 RenameGroup,
534 AddPlace,
536 EditPlace,
538 RemoveGroupConfirm,
540}
541
542#[derive(Debug)]
548pub struct FileDialogUiConfig {
549 pub header_style: HeaderStyle,
551 pub layout: LayoutStyle,
553 pub validation_buttons: ValidationButtonsConfig,
555 pub toolbar: ToolbarConfig,
557 pub places_pane_shown: bool,
559 pub places_pane_width: f32,
561 pub file_list_view: FileListViewMode,
563 pub file_list_columns: FileListColumnsConfig,
565 pub path_bar_style: PathBarStyle,
567 pub breadcrumbs_quick_select: bool,
571 pub breadcrumbs_max_segments: usize,
573 pub empty_hint_enabled: bool,
575 pub empty_hint_color: [f32; 4],
577 pub empty_hint_static_message: Option<String>,
579 pub new_folder_enabled: bool,
581 pub file_style_fonts: std::collections::HashMap<String, FontId>,
583 pub file_styles: FileStyleRegistry,
585 pub thumbnails_enabled: bool,
587 pub thumbnail_size: [f32; 2],
589 pub type_select_enabled: bool,
591 pub type_select_timeout: Duration,
593 pub custom_pane_enabled: bool,
595 pub custom_pane_dock: CustomPaneDock,
597 pub custom_pane_height: f32,
599 pub custom_pane_width: f32,
601}
602
603impl Default for FileDialogUiConfig {
604 fn default() -> Self {
605 Self {
606 header_style: HeaderStyle::ToolbarAndAddress,
607 layout: LayoutStyle::Standard,
608 validation_buttons: ValidationButtonsConfig::default(),
609 toolbar: ToolbarConfig::default(),
610 places_pane_shown: true,
611 places_pane_width: 150.0,
612 file_list_view: FileListViewMode::default(),
613 file_list_columns: FileListColumnsConfig::default(),
614 path_bar_style: PathBarStyle::TextInput,
615 breadcrumbs_quick_select: true,
616 breadcrumbs_max_segments: 6,
617 empty_hint_enabled: true,
618 empty_hint_color: [0.7, 0.7, 0.7, 1.0],
619 empty_hint_static_message: None,
620 new_folder_enabled: true,
621 file_style_fonts: std::collections::HashMap::new(),
622 file_styles: FileStyleRegistry::default(),
623 thumbnails_enabled: false,
624 thumbnail_size: [32.0, 32.0],
625 type_select_enabled: true,
626 type_select_timeout: Duration::from_millis(750),
627 custom_pane_enabled: true,
628 custom_pane_dock: CustomPaneDock::default(),
629 custom_pane_height: 120.0,
630 custom_pane_width: 250.0,
631 }
632 }
633}
634
635impl FileDialogUiConfig {
636 pub fn apply_igfd_classic_preset(&mut self) {
641 self.header_style = HeaderStyle::IgfdClassic;
642 self.layout = LayoutStyle::Standard;
643 self.places_pane_shown = true;
644 self.places_pane_width = 150.0;
645 self.file_list_view = FileListViewMode::List;
646 self.thumbnails_enabled = false;
647 self.toolbar.density = ToolbarDensity::Compact;
648 self.path_bar_style = PathBarStyle::Breadcrumbs;
649 self.breadcrumbs_quick_select = true;
650
651 if self.file_styles.rules.is_empty() && self.file_styles.callback.is_none() {
652 self.file_styles = crate::file_style::FileStyleRegistry::igfd_ascii_preset();
653 }
654
655 self.file_list_columns.show_preview = false;
656 self.file_list_columns.show_extension = false;
657 self.file_list_columns.show_size = true;
658 self.file_list_columns.show_modified = true;
659 self.file_list_columns.order = [
660 FileListDataColumn::Name,
661 FileListDataColumn::Extension,
662 FileListDataColumn::Size,
663 FileListDataColumn::Modified,
664 ];
665
666 self.custom_pane_enabled = true;
667 self.custom_pane_dock = CustomPaneDock::Right;
668 self.custom_pane_width = 250.0;
669 self.custom_pane_height = 120.0;
670
671 self.validation_buttons.align = ValidationButtonsAlign::Right;
672 self.validation_buttons.order = ValidationButtonsOrder::CancelConfirm;
673 self.validation_buttons.confirm_label = Some("OK".to_string());
674 self.validation_buttons.cancel_label = Some("Cancel".to_string());
675 self.validation_buttons.button_width = None;
676 self.validation_buttons.confirm_width = None;
677 self.validation_buttons.cancel_width = None;
678 }
679}
680
681#[derive(Debug, Default)]
686pub(crate) struct FileDialogUiRuntime {
687 pub(crate) path: PathUiRuntime,
689 pub(crate) opened_cwd: Option<PathBuf>,
691 pub(crate) focus_search_next: bool,
693 pub(crate) error: Option<String>,
695 pub(crate) type_select_buffer: String,
697 pub(crate) type_select_last_input: Option<std::time::Instant>,
699 pub(crate) breadcrumb: BreadcrumbUiRuntime,
701 pub(crate) footer: FooterUiRuntime,
703}
704
705#[derive(Debug, Default)]
707pub(crate) struct PathUiRuntime {
708 pub(crate) input_mode: bool,
713 pub(crate) edit: bool,
715 pub(crate) buffer: String,
717 pub(crate) last_cwd: String,
718 pub(crate) history_index: Option<usize>,
719 pub(crate) history_saved_buffer: Option<String>,
720 pub(crate) programmatic_edit: bool,
721 pub(crate) focus_next: bool,
723}
724
725#[derive(Debug, Default)]
727pub(crate) struct BreadcrumbUiRuntime {
728 pub(crate) scroll_to_end_next: bool,
729 pub(crate) quick_parent: Option<PathBuf>,
731}
732
733#[derive(Debug, Default)]
735pub(crate) struct FooterUiRuntime {
736 pub(crate) height_last: f32,
739 pub(crate) file_name_buffer: String,
745 pub(crate) file_name_last_display: String,
748}
749
750#[derive(Debug, Default)]
755pub(crate) struct FileDialogOperationState {
756 pub(crate) new_folder: NewFolderOperationState,
758 pub(crate) rename: RenameOperationState,
760 pub(crate) delete: DeleteOperationState,
762 pub(crate) paste: PasteOperationState,
764 pub(crate) places: PlacesOperationState,
766 pub(crate) reveal_id_next: Option<EntryId>,
768}
769
770#[derive(Debug, Default)]
772pub(crate) struct NewFolderOperationState {
773 pub(crate) inline_active: bool,
775 pub(crate) open_next: bool,
777 pub(crate) name: String,
779 pub(crate) focus_next: bool,
781 pub(crate) error: Option<String>,
783}
784
785#[derive(Debug, Default)]
787pub(crate) struct RenameOperationState {
788 pub(crate) open_next: bool,
790 pub(crate) focus_next: bool,
792 pub(crate) target_id: Option<EntryId>,
794 pub(crate) to: String,
796 pub(crate) error: Option<String>,
798}
799
800#[derive(Debug, Default)]
802pub(crate) struct DeleteOperationState {
803 pub(crate) open_next: bool,
805 pub(crate) target_ids: Vec<EntryId>,
807 pub(crate) recursive: bool,
809 pub(crate) error: Option<String>,
811}
812
813#[derive(Debug, Default)]
815pub(crate) struct PasteOperationState {
816 pub(crate) clipboard: Option<FileClipboard>,
818 pub(crate) job: Option<PendingPasteJob>,
820 pub(crate) conflict_open_next: bool,
822}
823
824#[derive(Debug, Default)]
826pub(crate) struct PlacesOperationState {
827 pub(crate) io: PlacesIoOperationState,
828 pub(crate) edit: PlacesEditOperationState,
829 pub(crate) selected: Option<(String, PathBuf)>,
831 pub(crate) inline_edit: PlacesInlineEditState,
832}
833
834#[derive(Debug, Default)]
836pub(crate) struct PlacesIoOperationState {
837 pub(crate) mode: PlacesIoMode,
838 pub(crate) buffer: String,
839 pub(crate) open_next: bool,
840 pub(crate) include_code: bool,
841 pub(crate) error: Option<String>,
842}
843
844#[derive(Debug, Default)]
846pub(crate) struct PlacesEditOperationState {
847 pub(crate) mode: PlacesEditMode,
848 pub(crate) open_next: bool,
849 pub(crate) focus_next: bool,
850 pub(crate) error: Option<String>,
851 pub(crate) group: String,
853 pub(crate) group_from: Option<String>,
855 pub(crate) place_from_path: Option<PathBuf>,
857 pub(crate) place_label: String,
859 pub(crate) place_path: String,
861}
862
863#[derive(Debug, Default)]
865pub(crate) struct PlacesInlineEditState {
866 pub(crate) target: Option<(String, PathBuf)>,
868 pub(crate) buffer: String,
870 pub(crate) focus_next: bool,
872}
873
874#[derive(Debug)]
880pub struct FileDialogUiState {
881 pub visible: bool,
883 pub config: FileDialogUiConfig,
885 pub(crate) runtime: FileDialogUiRuntime,
887 pub(crate) operations: FileDialogOperationState,
889 pub thumbnails: ThumbnailCache,
891}
892
893impl Default for FileDialogUiState {
894 fn default() -> Self {
895 Self {
896 visible: true,
897 config: FileDialogUiConfig::default(),
898 runtime: FileDialogUiRuntime::default(),
899 operations: FileDialogOperationState::default(),
900 thumbnails: ThumbnailCache::new(ThumbnailCacheConfig::default()),
901 }
902 }
903}
904
905impl FileDialogUiState {
906 pub fn apply_igfd_classic_preset(&mut self) {
915 self.config.apply_igfd_classic_preset();
916 self.runtime.path.input_mode = false;
917 self.runtime.breadcrumb.scroll_to_end_next = true;
918 }
919}
920
921#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
923pub enum HeaderStyle {
924 #[default]
926 ToolbarAndAddress,
927 IgfdClassic,
929}
930
931#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
933pub enum PathBarStyle {
934 #[default]
936 TextInput,
937 Breadcrumbs,
939}
940
941#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
943pub enum CustomPaneDock {
944 #[default]
946 Bottom,
947 Right,
949}
950
951#[derive(Debug)]
953pub struct FileDialogState {
954 pub core: FileDialogCore,
956 pub ui: FileDialogUiState,
958}
959
960impl FileDialogState {
961 pub fn new(mode: DialogMode) -> Self {
963 let mut core = FileDialogCore::new(mode);
964 core.set_scan_policy(ScanPolicy::tuned_incremental());
965 Self {
966 core,
967 ui: FileDialogUiState::default(),
968 }
969 }
970
971 pub fn open(&mut self) {
975 self.ui.visible = true;
976 self.ui.runtime.opened_cwd = Some(self.core.cwd.clone());
977 }
978
979 pub fn reopen(&mut self) {
983 self.open();
984 }
985
986 pub fn close(&mut self) {
990 self.ui.visible = false;
991 }
992
993 pub fn is_open(&self) -> bool {
995 self.ui.visible
996 }
997
998 pub fn scan_policy(&self) -> ScanPolicy {
1000 self.core.scan_policy()
1001 }
1002
1003 pub fn set_scan_policy(&mut self, policy: ScanPolicy) {
1005 self.core.set_scan_policy(policy);
1006 }
1007
1008 pub fn scan_status(&self) -> &ScanStatus {
1010 self.core.scan_status()
1011 }
1012
1013 pub fn request_rescan(&mut self) {
1015 self.core.request_rescan();
1016 }
1017
1018 pub fn set_scan_hook<F>(&mut self, hook: F)
1022 where
1023 F: FnMut(&mut crate::FsEntry) -> crate::ScanHookAction + 'static,
1024 {
1025 self.core.set_scan_hook(hook);
1026 }
1027
1028 pub fn clear_scan_hook(&mut self) {
1030 self.core.clear_scan_hook();
1031 }
1032
1033 pub fn apply_igfd_classic_preset(&mut self) {
1038 self.ui.apply_igfd_classic_preset();
1039 self.core.click_action = ClickAction::Navigate;
1040 self.core.sort_mode = crate::core::SortMode::Natural;
1041 self.core.sort_by = crate::core::SortBy::Name;
1042 self.core.sort_ascending = true;
1043 self.core.dirs_first = true;
1044 }
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050
1051 #[test]
1052 fn igfd_classic_preset_updates_ui_and_core() {
1053 let mut state = FileDialogState::new(DialogMode::OpenFile);
1054 state.apply_igfd_classic_preset();
1055
1056 assert_eq!(state.ui.config.layout, LayoutStyle::Standard);
1057 assert_eq!(state.ui.config.file_list_view, FileListViewMode::List);
1058 assert_eq!(state.ui.config.custom_pane_dock, CustomPaneDock::Right);
1059 assert!(!state.ui.config.file_list_columns.show_extension);
1060 assert_eq!(
1061 state.ui.config.validation_buttons.align,
1062 ValidationButtonsAlign::Right
1063 );
1064 assert_eq!(
1065 state.ui.config.validation_buttons.order,
1066 ValidationButtonsOrder::CancelConfirm
1067 );
1068 assert_eq!(state.core.click_action, ClickAction::Navigate);
1069 assert_eq!(state.core.sort_mode, crate::core::SortMode::Natural);
1070 }
1071
1072 #[test]
1073 fn open_close_roundtrip() {
1074 let mut state = FileDialogState::new(DialogMode::OpenFile);
1075
1076 assert!(state.is_open());
1077 state.close();
1078 assert!(!state.is_open());
1079
1080 state.open();
1081 assert!(state.is_open());
1082
1083 state.close();
1084 assert!(!state.is_open());
1085
1086 state.reopen();
1087 assert!(state.is_open());
1088 }
1089
1090 #[test]
1091 fn default_scan_policy_is_tuned_incremental() {
1092 let state = FileDialogState::new(DialogMode::OpenFile);
1093 assert_eq!(state.scan_policy(), ScanPolicy::tuned_incremental());
1094 }
1095
1096 #[test]
1097 fn ui_config_defaults_own_caller_facing_ui_knobs() {
1098 let state = FileDialogUiState::default();
1099
1100 assert_eq!(state.config.header_style, HeaderStyle::ToolbarAndAddress);
1101 assert_eq!(state.config.layout, LayoutStyle::Standard);
1102 assert_eq!(state.config.file_list_view, FileListViewMode::default());
1103 assert_eq!(state.config.path_bar_style, PathBarStyle::TextInput);
1104 assert!(state.config.breadcrumbs_quick_select);
1105 assert_eq!(state.config.type_select_timeout, Duration::from_millis(750));
1106 assert!(!state.config.thumbnails_enabled);
1107 assert_eq!(state.config.thumbnail_size, [32.0, 32.0]);
1108 }
1109
1110 #[test]
1111 fn ui_config_igfd_classic_preset_updates_config_without_runtime_buffers() {
1112 let mut state = FileDialogUiState::default();
1113 state.runtime.path.buffer = "keep-runtime-buffer".to_string();
1114 state.runtime.path.input_mode = true;
1115
1116 state.apply_igfd_classic_preset();
1117
1118 assert_eq!(state.config.header_style, HeaderStyle::IgfdClassic);
1119 assert_eq!(state.config.layout, LayoutStyle::Standard);
1120 assert_eq!(state.config.file_list_view, FileListViewMode::List);
1121 assert_eq!(state.config.toolbar.density, ToolbarDensity::Compact);
1122 assert_eq!(state.config.path_bar_style, PathBarStyle::Breadcrumbs);
1123 assert_eq!(state.config.custom_pane_dock, CustomPaneDock::Right);
1124 assert!(!state.config.file_list_columns.show_extension);
1125 assert_eq!(
1126 state.config.validation_buttons.align,
1127 ValidationButtonsAlign::Right
1128 );
1129 assert_eq!(state.runtime.path.buffer, "keep-runtime-buffer");
1130 assert!(!state.runtime.path.input_mode);
1131 assert!(state.runtime.breadcrumb.scroll_to_end_next);
1132 }
1133
1134 #[test]
1135 fn ui_runtime_and_operation_state_are_internal_to_ui_state() {
1136 let state = FileDialogUiState::default();
1137
1138 assert!(state.config.new_folder_enabled);
1139 assert!(state.config.type_select_enabled);
1140 assert!(state.runtime.type_select_buffer.is_empty());
1141 assert!(state.runtime.type_select_last_input.is_none());
1142 assert!(!state.operations.new_folder.inline_active);
1143 assert!(!state.operations.new_folder.open_next);
1144 assert!(state.operations.new_folder.name.is_empty());
1145 assert!(!state.operations.new_folder.focus_next);
1146 assert!(state.operations.new_folder.error.is_none());
1147 assert!(!state.runtime.path.input_mode);
1148 assert!(!state.runtime.path.edit);
1149 assert!(state.runtime.path.buffer.is_empty());
1150 assert!(state.runtime.path.history_index.is_none());
1151 assert!(state.runtime.path.history_saved_buffer.is_none());
1152 assert!(!state.runtime.focus_search_next);
1153 assert!(state.runtime.error.is_none());
1154 assert!(state.runtime.breadcrumb.quick_parent.is_none());
1155 assert_eq!(state.runtime.footer.height_last, 0.0);
1156 assert!(state.runtime.footer.file_name_buffer.is_empty());
1157 assert!(state.operations.rename.target_id.is_none());
1158 assert!(!state.operations.rename.open_next);
1159 assert!(state.operations.rename.to.is_empty());
1160 assert!(state.operations.delete.target_ids.is_empty());
1161 assert!(!state.operations.delete.open_next);
1162 assert!(state.operations.paste.clipboard.is_none());
1163 assert!(state.operations.paste.job.is_none());
1164 assert!(!state.operations.paste.conflict_open_next);
1165 assert!(state.operations.reveal_id_next.is_none());
1166 assert!(state.operations.places.io.buffer.is_empty());
1167 assert!(state.operations.places.selected.is_none());
1168 assert!(state.operations.places.inline_edit.target.is_none());
1169 }
1170
1171 #[test]
1172 fn file_list_columns_compact_roundtrip() {
1173 let cfg = FileListColumnsConfig {
1174 show_preview: false,
1175 show_extension: true,
1176 show_size: true,
1177 show_modified: false,
1178 order: [
1179 FileListDataColumn::Name,
1180 FileListDataColumn::Size,
1181 FileListDataColumn::Modified,
1182 FileListDataColumn::Extension,
1183 ],
1184 weight_overrides: FileListColumnWeightOverrides {
1185 preview: Some(0.15),
1186 name: Some(0.61),
1187 extension: Some(0.1),
1188 size: Some(0.17),
1189 modified: None,
1190 },
1191 };
1192
1193 let encoded = cfg.serialize_compact();
1194 let decoded = FileListColumnsConfig::deserialize_compact(&encoded).unwrap();
1195 assert_eq!(decoded, cfg);
1196 }
1197
1198 #[test]
1199 fn file_list_columns_deserialize_rejects_duplicate_order_entries() {
1200 let err = FileListColumnsConfig::deserialize_compact(
1201 "v1;preview=1;ext=1;size=1;modified=1;order=name,name,size,modified;weights=auto,auto,auto,auto,auto",
1202 )
1203 .unwrap_err();
1204 assert!(
1205 err.to_string()
1206 .contains("order` must contain each column exactly once")
1207 );
1208 }
1209
1210 #[test]
1211 fn file_list_columns_deserialize_rejects_non_positive_weight() {
1212 let err = FileListColumnsConfig::deserialize_compact(
1213 "v1;preview=1;ext=1;size=1;modified=1;order=name,ext,size,modified;weights=auto,0,auto,auto,auto",
1214 )
1215 .unwrap_err();
1216 assert!(err.to_string().contains("weight must be finite and > 0"));
1217 }
1218
1219 #[test]
1220 fn file_list_columns_normalized_order_dedupes_and_fills_missing() {
1221 let normalized = normalized_order([
1222 FileListDataColumn::Name,
1223 FileListDataColumn::Name,
1224 FileListDataColumn::Modified,
1225 FileListDataColumn::Modified,
1226 ]);
1227 assert_eq!(
1228 normalized,
1229 [
1230 FileListDataColumn::Name,
1231 FileListDataColumn::Modified,
1232 FileListDataColumn::Extension,
1233 FileListDataColumn::Size,
1234 ]
1235 );
1236 }
1237}