1use std::path::PathBuf;
2
3use dear_imgui_rs::FontId;
4
5use crate::core::{ClickAction, DialogMode, LayoutStyle};
6use crate::dialog_core::{EntryId, FileDialogCore, ScanPolicy, ScanStatus};
7use crate::file_style::FileStyleRegistry;
8use crate::thumbnails::{ThumbnailCache, ThumbnailCacheConfig};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum FileListViewMode {
13 List,
15 ThumbnailsList,
19 Grid,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum FileListDataColumn {
26 Name,
28 Extension,
30 Size,
32 Modified,
34}
35
36impl FileListDataColumn {
37 fn compact_token(self) -> &'static str {
38 match self {
39 Self::Name => "name",
40 Self::Extension => "ext",
41 Self::Size => "size",
42 Self::Modified => "modified",
43 }
44 }
45
46 fn from_compact_token(token: &str) -> Option<Self> {
47 match token {
48 "name" => Some(Self::Name),
49 "ext" => Some(Self::Extension),
50 "size" => Some(Self::Size),
51 "modified" => Some(Self::Modified),
52 _ => None,
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq)]
62pub struct FileListColumnWeightOverrides {
63 pub preview: Option<f32>,
65 pub name: Option<f32>,
67 pub extension: Option<f32>,
69 pub size: Option<f32>,
71 pub modified: Option<f32>,
73}
74
75impl Default for FileListColumnWeightOverrides {
76 fn default() -> Self {
77 Self {
78 preview: None,
79 name: None,
80 extension: None,
81 size: None,
82 modified: None,
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq)]
89pub struct FileListColumnsConfig {
90 pub show_preview: bool,
92 pub show_extension: bool,
94 pub show_size: bool,
96 pub show_modified: bool,
98 pub order: [FileListDataColumn; 4],
102 pub weight_overrides: FileListColumnWeightOverrides,
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct FileListColumnsDeserializeError {
109 msg: String,
110}
111
112impl FileListColumnsDeserializeError {
113 fn new(msg: impl Into<String>) -> Self {
114 Self { msg: msg.into() }
115 }
116}
117
118impl std::fmt::Display for FileListColumnsDeserializeError {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "file list columns deserialize error: {}", self.msg)
121 }
122}
123
124impl std::error::Error for FileListColumnsDeserializeError {}
125
126impl FileListColumnsConfig {
127 pub fn serialize_compact(&self) -> String {
131 let order = self
132 .normalized_order()
133 .iter()
134 .map(|c| c.compact_token())
135 .collect::<Vec<_>>()
136 .join(",");
137 let weights = [
138 self.weight_overrides.preview,
139 self.weight_overrides.name,
140 self.weight_overrides.extension,
141 self.weight_overrides.size,
142 self.weight_overrides.modified,
143 ]
144 .into_iter()
145 .map(|v| {
146 v.map(|w| format!("{w:.4}"))
147 .unwrap_or_else(|| "auto".to_string())
148 })
149 .collect::<Vec<_>>()
150 .join(",");
151
152 format!(
153 "v1;preview={};ext={};size={};modified={};order={};weights={}",
154 u8::from(self.show_preview),
155 u8::from(self.show_extension),
156 u8::from(self.show_size),
157 u8::from(self.show_modified),
158 order,
159 weights,
160 )
161 }
162
163 pub fn deserialize_compact(input: &str) -> Result<Self, FileListColumnsDeserializeError> {
165 let mut version_ok = false;
166 let mut preview = None;
167 let mut ext = None;
168 let mut size = None;
169 let mut modified = None;
170 let mut order = None;
171 let mut weights = None;
172
173 for token in input.split(';').filter(|s| !s.trim().is_empty()) {
174 if token == "v1" {
175 version_ok = true;
176 continue;
177 }
178 if token.starts_with('v') {
179 return Err(FileListColumnsDeserializeError::new(format!(
180 "unsupported version token `{token}`"
181 )));
182 }
183 let (key, value) = token.split_once('=').ok_or_else(|| {
184 FileListColumnsDeserializeError::new(format!("invalid token `{token}`"))
185 })?;
186 match key {
187 "preview" => preview = Some(parse_compact_bool(value)?),
188 "ext" => ext = Some(parse_compact_bool(value)?),
189 "size" => size = Some(parse_compact_bool(value)?),
190 "modified" => modified = Some(parse_compact_bool(value)?),
191 "order" => order = Some(parse_compact_order(value)?),
192 "weights" => weights = Some(parse_compact_weights(value)?),
193 _ => {
194 return Err(FileListColumnsDeserializeError::new(format!(
195 "unknown key `{key}`"
196 )));
197 }
198 }
199 }
200
201 if !version_ok {
202 return Err(FileListColumnsDeserializeError::new(
203 "missing or unsupported version token",
204 ));
205 }
206
207 Ok(Self {
208 show_preview: preview
209 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `preview`"))?,
210 show_extension: ext
211 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `ext`"))?,
212 show_size: size
213 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `size`"))?,
214 show_modified: modified
215 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `modified`"))?,
216 order: order
217 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `order`"))?,
218 weight_overrides: weights
219 .ok_or_else(|| FileListColumnsDeserializeError::new("missing key `weights`"))?,
220 })
221 }
222
223 pub fn normalized_order(&self) -> [FileListDataColumn; 4] {
225 normalized_order(self.order)
226 }
227}
228
229impl Default for FileListColumnsConfig {
230 fn default() -> Self {
231 Self {
232 show_preview: true,
233 show_extension: true,
234 show_size: true,
235 show_modified: true,
236 order: [
237 FileListDataColumn::Name,
238 FileListDataColumn::Extension,
239 FileListDataColumn::Size,
240 FileListDataColumn::Modified,
241 ],
242 weight_overrides: FileListColumnWeightOverrides::default(),
243 }
244 }
245}
246
247fn normalized_order(order: [FileListDataColumn; 4]) -> [FileListDataColumn; 4] {
248 let mut out = Vec::with_capacity(4);
249 for c in order {
250 if !out.contains(&c) {
251 out.push(c);
252 }
253 }
254 for c in [
255 FileListDataColumn::Name,
256 FileListDataColumn::Extension,
257 FileListDataColumn::Size,
258 FileListDataColumn::Modified,
259 ] {
260 if !out.contains(&c) {
261 out.push(c);
262 }
263 }
264 [out[0], out[1], out[2], out[3]]
265}
266
267fn parse_compact_bool(value: &str) -> Result<bool, FileListColumnsDeserializeError> {
268 match value {
269 "0" => Ok(false),
270 "1" => Ok(true),
271 _ => Err(FileListColumnsDeserializeError::new(format!(
272 "invalid bool value `{value}`"
273 ))),
274 }
275}
276
277fn parse_compact_order(
278 value: &str,
279) -> Result<[FileListDataColumn; 4], FileListColumnsDeserializeError> {
280 let cols = value
281 .split(',')
282 .map(FileListDataColumn::from_compact_token)
283 .collect::<Option<Vec<_>>>()
284 .ok_or_else(|| FileListColumnsDeserializeError::new("invalid column token in `order`"))?;
285 if cols.len() != 4 {
286 return Err(FileListColumnsDeserializeError::new(
287 "`order` must contain exactly 4 columns",
288 ));
289 }
290 let order = [cols[0], cols[1], cols[2], cols[3]];
291 let normalized = normalized_order(order);
292 if normalized != order {
293 return Err(FileListColumnsDeserializeError::new(
294 "`order` must contain each column exactly once",
295 ));
296 }
297 Ok(order)
298}
299
300fn parse_compact_optional_weight(
301 value: &str,
302) -> Result<Option<f32>, FileListColumnsDeserializeError> {
303 if value.eq_ignore_ascii_case("auto") {
304 return Ok(None);
305 }
306 let parsed = value.parse::<f32>().map_err(|_| {
307 FileListColumnsDeserializeError::new(format!("invalid weight value `{value}`"))
308 })?;
309 if !parsed.is_finite() || parsed <= 0.0 {
310 return Err(FileListColumnsDeserializeError::new(format!(
311 "weight must be finite and > 0, got `{value}`"
312 )));
313 }
314 Ok(Some(parsed))
315}
316
317fn parse_compact_weights(
318 value: &str,
319) -> Result<FileListColumnWeightOverrides, FileListColumnsDeserializeError> {
320 let parts: Vec<&str> = value.split(',').collect();
321 if parts.len() != 5 {
322 return Err(FileListColumnsDeserializeError::new(
323 "`weights` must contain exactly 5 values",
324 ));
325 }
326 Ok(FileListColumnWeightOverrides {
327 preview: parse_compact_optional_weight(parts[0])?,
328 name: parse_compact_optional_weight(parts[1])?,
329 extension: parse_compact_optional_weight(parts[2])?,
330 size: parse_compact_optional_weight(parts[3])?,
331 modified: parse_compact_optional_weight(parts[4])?,
332 })
333}
334
335impl Default for FileListViewMode {
336 fn default() -> Self {
337 Self::List
338 }
339}
340
341#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
343pub enum ValidationButtonsAlign {
344 #[default]
346 Left,
347 Right,
349}
350
351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
353pub enum ValidationButtonsOrder {
354 #[default]
356 ConfirmCancel,
357 CancelConfirm,
359}
360
361#[derive(Clone, Debug)]
363pub struct ValidationButtonsConfig {
364 pub align: ValidationButtonsAlign,
366 pub order: ValidationButtonsOrder,
368 pub confirm_label: Option<String>,
370 pub cancel_label: Option<String>,
372 pub button_width: Option<f32>,
374 pub confirm_width: Option<f32>,
376 pub cancel_width: Option<f32>,
378}
379
380impl Default for ValidationButtonsConfig {
381 fn default() -> Self {
382 Self {
383 align: ValidationButtonsAlign::Left,
384 order: ValidationButtonsOrder::ConfirmCancel,
385 confirm_label: None,
386 cancel_label: None,
387 button_width: None,
388 confirm_width: None,
389 cancel_width: None,
390 }
391 }
392}
393
394#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
396pub enum ToolbarDensity {
397 #[default]
399 Normal,
400 Compact,
402 Spacious,
404}
405
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
408pub enum ToolbarIconMode {
409 #[default]
411 Text,
412 IconOnly,
414 IconAndText,
416}
417
418#[derive(Clone, Debug, Default)]
420pub struct ToolbarIcons {
421 pub mode: ToolbarIconMode,
423 pub places: Option<String>,
425 pub refresh: Option<String>,
427 pub new_folder: Option<String>,
429 pub columns: Option<String>,
431 pub options: Option<String>,
433}
434
435#[derive(Clone, Debug)]
437pub struct ToolbarConfig {
438 pub density: ToolbarDensity,
440 pub icons: ToolbarIcons,
442 pub show_tooltips: bool,
444}
445
446impl Default for ToolbarConfig {
447 fn default() -> Self {
448 Self {
449 density: ToolbarDensity::Normal,
450 icons: ToolbarIcons::default(),
451 show_tooltips: true,
452 }
453 }
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub enum ClipboardOp {
459 Copy,
461 Cut,
463}
464
465#[derive(Clone, Debug)]
467pub struct FileClipboard {
468 pub op: ClipboardOp,
470 pub sources: Vec<PathBuf>,
472}
473
474#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476pub(crate) enum PasteConflictAction {
477 Overwrite,
479 Skip,
481 KeepBoth,
483}
484
485#[derive(Clone, Debug)]
487pub(crate) struct PasteConflictPrompt {
488 pub source: PathBuf,
490 pub dest: PathBuf,
492 pub apply_to_all: bool,
494}
495
496#[derive(Clone, Debug)]
498pub(crate) struct PendingPasteJob {
499 pub clipboard: FileClipboard,
501 pub dest_dir: PathBuf,
503 pub next_index: usize,
505 pub created: Vec<String>,
507 pub apply_all_conflicts: Option<PasteConflictAction>,
509 pub pending_conflict_action: Option<PasteConflictAction>,
511 pub conflict: Option<PasteConflictPrompt>,
513}
514
515#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
517pub(crate) enum PlacesIoMode {
518 #[default]
520 Export,
521 Import,
523}
524
525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
527pub(crate) enum PlacesEditMode {
528 #[default]
530 AddGroup,
531 RenameGroup,
533 AddPlace,
535 EditPlace,
537 RemoveGroupConfirm,
539}
540
541#[derive(Debug)]
546pub struct FileDialogUiState {
547 pub visible: bool,
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 path_input_mode: bool,
572 pub breadcrumbs_quick_select: bool,
576 pub breadcrumbs_max_segments: usize,
578 pub empty_hint_enabled: bool,
580 pub empty_hint_color: [f32; 4],
582 pub empty_hint_static_message: Option<String>,
584 pub path_edit: bool,
588 pub path_edit_buffer: String,
590 pub(crate) path_edit_last_cwd: String,
591 pub(crate) breadcrumbs_scroll_to_end_next: bool,
592 pub(crate) opened_cwd: Option<PathBuf>,
593 pub(crate) path_history_index: Option<usize>,
594 pub(crate) path_history_saved_buffer: Option<String>,
595 pub(crate) path_bar_programmatic_edit: bool,
596 pub focus_path_edit_next: bool,
598 pub focus_search_next: bool,
600 pub ui_error: Option<String>,
602 pub new_folder_enabled: bool,
604 pub new_folder_inline_active: bool,
606 pub new_folder_open_next: bool,
608 pub new_folder_name: String,
610 pub new_folder_focus_next: bool,
612 pub new_folder_error: Option<String>,
614 pub rename_open_next: bool,
616 pub rename_focus_next: bool,
618 pub rename_target_id: Option<EntryId>,
620 pub rename_to: String,
622 pub rename_error: Option<String>,
624 pub delete_open_next: bool,
626 pub delete_target_ids: Vec<EntryId>,
628 pub delete_recursive: bool,
630 pub delete_error: Option<String>,
632 pub clipboard: Option<FileClipboard>,
634 pub file_style_fonts: std::collections::HashMap<String, FontId>,
636 pub(crate) paste_job: Option<PendingPasteJob>,
638 pub(crate) paste_conflict_open_next: bool,
640 pub(crate) reveal_id_next: Option<EntryId>,
642 pub file_styles: FileStyleRegistry,
644 pub thumbnails_enabled: bool,
646 pub thumbnail_size: [f32; 2],
648 pub thumbnails: ThumbnailCache,
650 pub type_select_enabled: bool,
652 pub type_select_timeout_ms: u64,
654 pub custom_pane_enabled: bool,
656 pub custom_pane_dock: CustomPaneDock,
658 pub custom_pane_height: f32,
660 pub custom_pane_width: f32,
662
663 pub(crate) places_io_mode: PlacesIoMode,
665 pub(crate) places_io_buffer: String,
667 pub(crate) places_io_open_next: bool,
669 pub(crate) places_io_include_code: bool,
671 pub(crate) places_io_error: Option<String>,
673
674 pub(crate) places_edit_mode: PlacesEditMode,
676 pub(crate) places_edit_open_next: bool,
678 pub(crate) places_edit_focus_next: bool,
680 pub(crate) places_edit_error: Option<String>,
682 pub(crate) places_edit_group: String,
684 pub(crate) places_edit_group_from: Option<String>,
686 pub(crate) places_edit_place_from_path: Option<PathBuf>,
688 pub(crate) places_edit_place_label: String,
690 pub(crate) places_edit_place_path: String,
692
693 pub(crate) type_select_buffer: String,
694 pub(crate) type_select_last_input: Option<std::time::Instant>,
695 pub(crate) places_selected: Option<(String, PathBuf)>,
697
698 pub(crate) places_inline_edit: Option<(String, PathBuf)>,
700 pub(crate) places_inline_edit_buffer: String,
702 pub(crate) places_inline_edit_focus_next: bool,
704
705 pub(crate) breadcrumb_quick_parent: Option<PathBuf>,
707
708 pub(crate) footer_height_last: f32,
711
712 pub(crate) footer_file_name_buffer: String,
718 pub(crate) footer_file_name_last_display: String,
721}
722
723impl Default for FileDialogUiState {
724 fn default() -> Self {
725 Self {
726 visible: true,
727 header_style: HeaderStyle::ToolbarAndAddress,
728 layout: LayoutStyle::Standard,
729 validation_buttons: ValidationButtonsConfig::default(),
730 toolbar: ToolbarConfig::default(),
731 places_pane_shown: true,
732 places_pane_width: 150.0,
733 file_list_view: FileListViewMode::default(),
734 file_list_columns: FileListColumnsConfig::default(),
735 path_bar_style: PathBarStyle::TextInput,
736 path_input_mode: false,
737 breadcrumbs_quick_select: true,
738 breadcrumbs_max_segments: 6,
739 empty_hint_enabled: true,
740 empty_hint_color: [0.7, 0.7, 0.7, 1.0],
741 empty_hint_static_message: None,
742 path_edit: false,
743 path_edit_buffer: String::new(),
744 path_edit_last_cwd: String::new(),
745 breadcrumbs_scroll_to_end_next: false,
746 opened_cwd: None,
747 path_history_index: None,
748 path_history_saved_buffer: None,
749 path_bar_programmatic_edit: false,
750 focus_path_edit_next: false,
751 focus_search_next: false,
752 ui_error: None,
753 new_folder_enabled: true,
754 new_folder_inline_active: false,
755 new_folder_open_next: false,
756 new_folder_name: String::new(),
757 new_folder_focus_next: false,
758 new_folder_error: None,
759 rename_open_next: false,
760 rename_focus_next: false,
761 rename_target_id: None,
762 rename_to: String::new(),
763 rename_error: None,
764 delete_open_next: false,
765 delete_target_ids: Vec::new(),
766 delete_recursive: false,
767 delete_error: None,
768 clipboard: None,
769 file_style_fonts: std::collections::HashMap::new(),
770 paste_job: None,
771 paste_conflict_open_next: false,
772 reveal_id_next: None,
773 file_styles: FileStyleRegistry::default(),
774 thumbnails_enabled: false,
775 thumbnail_size: [32.0, 32.0],
776 thumbnails: ThumbnailCache::new(ThumbnailCacheConfig::default()),
777 type_select_enabled: true,
778 type_select_timeout_ms: 750,
779 custom_pane_enabled: true,
780 custom_pane_dock: CustomPaneDock::default(),
781 custom_pane_height: 120.0,
782 custom_pane_width: 250.0,
783 places_io_mode: PlacesIoMode::Export,
784 places_io_buffer: String::new(),
785 places_io_open_next: false,
786 places_io_include_code: false,
787 places_io_error: None,
788 places_edit_mode: PlacesEditMode::default(),
789 places_edit_open_next: false,
790 places_edit_focus_next: false,
791 places_edit_error: None,
792 places_edit_group: String::new(),
793 places_edit_group_from: None,
794 places_edit_place_from_path: None,
795 places_edit_place_label: String::new(),
796 places_edit_place_path: String::new(),
797 type_select_buffer: String::new(),
798 type_select_last_input: None,
799 places_selected: None,
800 places_inline_edit: None,
801 places_inline_edit_buffer: String::new(),
802 places_inline_edit_focus_next: false,
803 breadcrumb_quick_parent: None,
804 footer_height_last: 0.0,
805 footer_file_name_buffer: String::new(),
806 footer_file_name_last_display: String::new(),
807 }
808 }
809}
810
811impl FileDialogUiState {
812 pub fn apply_igfd_classic_preset(&mut self) {
821 self.header_style = HeaderStyle::IgfdClassic;
822 self.layout = LayoutStyle::Standard;
823 self.places_pane_shown = true;
824 self.places_pane_width = 150.0;
825 self.file_list_view = FileListViewMode::List;
826 self.thumbnails_enabled = false;
827 self.toolbar.density = ToolbarDensity::Compact;
828 self.path_bar_style = PathBarStyle::Breadcrumbs;
829 self.path_input_mode = false;
830 self.breadcrumbs_scroll_to_end_next = true;
831 self.breadcrumbs_quick_select = true;
832
833 if self.file_styles.rules.is_empty() && self.file_styles.callback.is_none() {
834 self.file_styles = crate::file_style::FileStyleRegistry::igfd_ascii_preset();
835 }
836
837 self.file_list_columns.show_preview = false;
838 self.file_list_columns.show_extension = false;
839 self.file_list_columns.show_size = true;
840 self.file_list_columns.show_modified = true;
841 self.file_list_columns.order = [
842 FileListDataColumn::Name,
843 FileListDataColumn::Extension,
844 FileListDataColumn::Size,
845 FileListDataColumn::Modified,
846 ];
847
848 self.custom_pane_enabled = true;
849 self.custom_pane_dock = CustomPaneDock::Right;
850 self.custom_pane_width = 250.0;
851 self.custom_pane_height = 120.0;
852
853 self.validation_buttons.align = ValidationButtonsAlign::Right;
854 self.validation_buttons.order = ValidationButtonsOrder::CancelConfirm;
855 self.validation_buttons.confirm_label = Some("OK".to_string());
856 self.validation_buttons.cancel_label = Some("Cancel".to_string());
857 self.validation_buttons.button_width = None;
858 self.validation_buttons.confirm_width = None;
859 self.validation_buttons.cancel_width = None;
860 }
861}
862
863#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
865pub enum HeaderStyle {
866 #[default]
868 ToolbarAndAddress,
869 IgfdClassic,
871}
872
873#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
875pub enum PathBarStyle {
876 #[default]
878 TextInput,
879 Breadcrumbs,
881}
882
883#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
885pub enum CustomPaneDock {
886 #[default]
888 Bottom,
889 Right,
891}
892
893#[derive(Debug)]
895pub struct FileDialogState {
896 pub core: FileDialogCore,
898 pub ui: FileDialogUiState,
900}
901
902impl FileDialogState {
903 pub fn new(mode: DialogMode) -> Self {
905 let mut core = FileDialogCore::new(mode);
906 core.set_scan_policy(ScanPolicy::tuned_incremental());
907 Self {
908 core,
909 ui: FileDialogUiState::default(),
910 }
911 }
912
913 pub fn open(&mut self) {
917 self.ui.visible = true;
918 self.ui.opened_cwd = Some(self.core.cwd.clone());
919 }
920
921 pub fn reopen(&mut self) {
925 self.open();
926 }
927
928 pub fn close(&mut self) {
932 self.ui.visible = false;
933 }
934
935 pub fn is_open(&self) -> bool {
937 self.ui.visible
938 }
939
940 pub fn scan_policy(&self) -> ScanPolicy {
942 self.core.scan_policy()
943 }
944
945 pub fn set_scan_policy(&mut self, policy: ScanPolicy) {
947 self.core.set_scan_policy(policy);
948 }
949
950 pub fn scan_status(&self) -> &ScanStatus {
952 self.core.scan_status()
953 }
954
955 pub fn request_rescan(&mut self) {
957 self.core.request_rescan();
958 }
959
960 pub fn set_scan_hook<F>(&mut self, hook: F)
964 where
965 F: FnMut(&mut crate::FsEntry) -> crate::ScanHookAction + 'static,
966 {
967 self.core.set_scan_hook(hook);
968 }
969
970 pub fn clear_scan_hook(&mut self) {
972 self.core.clear_scan_hook();
973 }
974
975 pub fn apply_igfd_classic_preset(&mut self) {
980 self.ui.apply_igfd_classic_preset();
981 self.core.click_action = ClickAction::Navigate;
982 self.core.sort_mode = crate::core::SortMode::Natural;
983 self.core.sort_by = crate::core::SortBy::Name;
984 self.core.sort_ascending = true;
985 self.core.dirs_first = true;
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn igfd_classic_preset_updates_ui_and_core() {
995 let mut state = FileDialogState::new(DialogMode::OpenFile);
996 state.apply_igfd_classic_preset();
997
998 assert_eq!(state.ui.layout, LayoutStyle::Standard);
999 assert_eq!(state.ui.file_list_view, FileListViewMode::List);
1000 assert_eq!(state.ui.custom_pane_dock, CustomPaneDock::Right);
1001 assert!(!state.ui.file_list_columns.show_extension);
1002 assert_eq!(
1003 state.ui.validation_buttons.align,
1004 ValidationButtonsAlign::Right
1005 );
1006 assert_eq!(
1007 state.ui.validation_buttons.order,
1008 ValidationButtonsOrder::CancelConfirm
1009 );
1010 assert_eq!(state.core.click_action, ClickAction::Navigate);
1011 assert_eq!(state.core.sort_mode, crate::core::SortMode::Natural);
1012 }
1013
1014 #[test]
1015 fn open_close_roundtrip() {
1016 let mut state = FileDialogState::new(DialogMode::OpenFile);
1017
1018 assert!(state.is_open());
1019 state.close();
1020 assert!(!state.is_open());
1021
1022 state.open();
1023 assert!(state.is_open());
1024
1025 state.close();
1026 assert!(!state.is_open());
1027
1028 state.reopen();
1029 assert!(state.is_open());
1030 }
1031
1032 #[test]
1033 fn default_scan_policy_is_tuned_incremental() {
1034 let state = FileDialogState::new(DialogMode::OpenFile);
1035 assert_eq!(state.scan_policy(), ScanPolicy::tuned_incremental());
1036 }
1037
1038 #[test]
1039 fn file_list_columns_compact_roundtrip() {
1040 let cfg = FileListColumnsConfig {
1041 show_preview: false,
1042 show_extension: true,
1043 show_size: true,
1044 show_modified: false,
1045 order: [
1046 FileListDataColumn::Name,
1047 FileListDataColumn::Size,
1048 FileListDataColumn::Modified,
1049 FileListDataColumn::Extension,
1050 ],
1051 weight_overrides: FileListColumnWeightOverrides {
1052 preview: Some(0.15),
1053 name: Some(0.61),
1054 extension: Some(0.1),
1055 size: Some(0.17),
1056 modified: None,
1057 },
1058 };
1059
1060 let encoded = cfg.serialize_compact();
1061 let decoded = FileListColumnsConfig::deserialize_compact(&encoded).unwrap();
1062 assert_eq!(decoded, cfg);
1063 }
1064
1065 #[test]
1066 fn file_list_columns_deserialize_rejects_duplicate_order_entries() {
1067 let err = FileListColumnsConfig::deserialize_compact(
1068 "v1;preview=1;ext=1;size=1;modified=1;order=name,name,size,modified;weights=auto,auto,auto,auto,auto",
1069 )
1070 .unwrap_err();
1071 assert!(
1072 err.to_string()
1073 .contains("order` must contain each column exactly once")
1074 );
1075 }
1076
1077 #[test]
1078 fn file_list_columns_deserialize_rejects_non_positive_weight() {
1079 let err = FileListColumnsConfig::deserialize_compact(
1080 "v1;preview=1;ext=1;size=1;modified=1;order=name,ext,size,modified;weights=auto,0,auto,auto,auto",
1081 )
1082 .unwrap_err();
1083 assert!(err.to_string().contains("weight must be finite and > 0"));
1084 }
1085
1086 #[test]
1087 fn file_list_columns_normalized_order_dedupes_and_fills_missing() {
1088 let normalized = normalized_order([
1089 FileListDataColumn::Name,
1090 FileListDataColumn::Name,
1091 FileListDataColumn::Modified,
1092 FileListDataColumn::Modified,
1093 ]);
1094 assert_eq!(
1095 normalized,
1096 [
1097 FileListDataColumn::Name,
1098 FileListDataColumn::Modified,
1099 FileListDataColumn::Extension,
1100 FileListDataColumn::Size,
1101 ]
1102 );
1103 }
1104}