Skip to main content

dear_file_browser/
dialog_manager.rs

1use std::collections::HashMap;
2
3use dear_imgui_rs::Ui;
4
5use crate::dialog_state::FileDialogState;
6use crate::fs::{FileSystem, StdFileSystem};
7use crate::ui::{FileDialogExt, WindowHostConfig};
8use crate::{FileDialogError, Selection};
9
10/// Opaque identifier for an in-UI file browser dialog instance.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct DialogId(u64);
13
14/// Manager for multiple in-UI file browser dialogs (IGFD-style open/display separation).
15///
16/// This is an incremental step toward IGFD-grade behavior:
17/// - Multiple dialogs can exist concurrently (each keyed by a `DialogId`).
18/// - The caller opens a dialog (`open_browser*`) and later drives rendering per-frame via
19///   `show_*` / `draw_*`.
20pub struct DialogManager {
21    next_id: u64,
22    browsers: HashMap<DialogId, FileDialogState>,
23    fs: Box<dyn FileSystem>,
24}
25
26impl DialogManager {
27    /// Create a new manager.
28    pub fn new() -> Self {
29        Self::with_fs(Box::new(StdFileSystem))
30    }
31
32    /// Create a new manager using a custom filesystem.
33    pub fn with_fs(fs: Box<dyn FileSystem>) -> Self {
34        Self {
35            next_id: 0,
36            browsers: HashMap::new(),
37            fs,
38        }
39    }
40
41    /// Replace the manager filesystem.
42    pub fn set_fs(&mut self, fs: Box<dyn FileSystem>) {
43        self.fs = fs;
44    }
45
46    /// Get a shared reference to the manager filesystem.
47    pub fn fs(&self) -> &dyn FileSystem {
48        self.fs.as_ref()
49    }
50
51    /// Open a new in-UI file browser dialog with a default state.
52    pub fn open_browser(&mut self, mode: crate::DialogMode) -> DialogId {
53        self.open_browser_with_state(FileDialogState::new(mode))
54    }
55
56    /// Open a new in-UI file browser dialog with a fully configured state.
57    pub fn open_browser_with_state(&mut self, state: FileDialogState) -> DialogId {
58        let mut state = state;
59        // `open_browser*` mirrors IGFD's `OpenDialog` step: the returned dialog is immediately
60        // visible and ready to be displayed via `show_*` / `draw_*`.
61        state.open();
62        self.next_id = self.next_id.wrapping_add(1);
63        let id = DialogId(self.next_id);
64        self.browsers.insert(id, state);
65        id
66    }
67
68    /// Close an open dialog and return its state (if any).
69    pub fn close(&mut self, id: DialogId) -> Option<FileDialogState> {
70        self.browsers.remove(&id)
71    }
72
73    /// Returns `true` if the dialog exists in the manager.
74    pub fn contains(&self, id: DialogId) -> bool {
75        self.browsers.contains_key(&id)
76    }
77
78    /// Get immutable access to a dialog state.
79    pub fn dialog_state(&self, id: DialogId) -> Option<&FileDialogState> {
80        self.browsers.get(&id)
81    }
82
83    /// Get mutable access to a dialog state (to tweak filters/layout/etc).
84    pub fn dialog_state_mut(&mut self, id: DialogId) -> Option<&mut FileDialogState> {
85        self.browsers.get_mut(&id)
86    }
87
88    /// Draw a dialog hosted in its own ImGui window (default host config).
89    ///
90    /// If a result is produced (confirm/cancel), the dialog is removed from the manager and the
91    /// result is returned.
92    pub fn show_browser(
93        &mut self,
94        ui: &Ui,
95        id: DialogId,
96    ) -> Option<Result<Selection, FileDialogError>> {
97        let state = self.browsers.get_mut(&id)?;
98        let cfg = WindowHostConfig::for_mode(state.core.mode);
99        let res = ui
100            .file_browser()
101            .show_windowed_with(state, &cfg, self.fs.as_ref(), None, None);
102        if res.is_some() {
103            self.browsers.remove(&id);
104        }
105        res
106    }
107
108    /// Draw a dialog hosted in an ImGui window using custom window configuration.
109    ///
110    /// If a result is produced (confirm/cancel), the dialog is removed from the manager and the
111    /// result is returned.
112    pub fn show_browser_windowed(
113        &mut self,
114        ui: &Ui,
115        id: DialogId,
116        cfg: &WindowHostConfig,
117    ) -> Option<Result<Selection, FileDialogError>> {
118        let state = self.browsers.get_mut(&id)?;
119        let res = ui
120            .file_browser()
121            .show_windowed_with(state, cfg, self.fs.as_ref(), None, None);
122        if res.is_some() {
123            self.browsers.remove(&id);
124        }
125        res
126    }
127
128    /// Draw only the dialog contents (no host window) for embedding.
129    ///
130    /// If a result is produced (confirm/cancel), the dialog is removed from the manager and the
131    /// result is returned.
132    pub fn draw_browser_contents(
133        &mut self,
134        ui: &Ui,
135        id: DialogId,
136    ) -> Option<Result<Selection, FileDialogError>> {
137        let state = self.browsers.get_mut(&id)?;
138        let res = ui
139            .file_browser()
140            .draw_contents_with(state, self.fs.as_ref(), None, None);
141        if res.is_some() {
142            self.browsers.remove(&id);
143        }
144        res
145    }
146}
147
148impl std::fmt::Debug for DialogManager {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("DialogManager")
151            .field("next_id", &self.next_id)
152            .field("browsers_len", &self.browsers.len())
153            .finish()
154    }
155}
156
157impl Default for DialogManager {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::DialogMode;
167
168    #[test]
169    fn open_close_roundtrip() {
170        let mut mgr = DialogManager::new();
171        let id1 = mgr.open_browser(DialogMode::OpenFile);
172        let id2 = mgr.open_browser(DialogMode::SaveFile);
173        assert_ne!(id1, id2);
174
175        assert!(mgr.contains(id1));
176        assert!(mgr.contains(id2));
177
178        assert!(mgr.dialog_state(id1).unwrap().is_open());
179        assert!(mgr.dialog_state(id2).unwrap().is_open());
180
181        let s1 = mgr.close(id1).unwrap();
182        assert_eq!(s1.core.mode, DialogMode::OpenFile);
183        assert!(!mgr.contains(id1));
184        assert!(mgr.contains(id2));
185    }
186}