stereokit_rust/tools/
file_browser.rs

1use crate::{
2    maths::{Pose, Quat, Vec2, Vec3},
3    prelude::*,
4    sprite::Sprite,
5    tools::os_api::PathEntry,
6    ui::{Ui, UiBtnLayout, UiVisual, UiWin},
7    util::{Color128, PickerMode},
8};
9use std::path::PathBuf;
10
11use super::os_api::get_files;
12
13pub const FILE_BROWSER_OPEN: &str = "File_Browser_open";
14pub const FILE_BROWSER_SAVE: &str = "File_Browser_save";
15
16/// A basic file browser to open existing file on PC and Android. Must be launched by an other stepper which has to be
17/// set in caller.
18/// ### Fields that can be changed before initialization:
19/// * `picker_mode` - What the file browser is for. Default is PickerMode::Open.
20/// * `caller` - The id of the stepper that launched the file browser and is waiting for a FILE_BROWSER_OPEN message.
21/// * `dir` - The directory to show. You can't browse outside of this directory.
22/// * `exts` - The file extensions to filter.
23/// * `window_pose` - The pose where to show the file browser window.
24/// * `window_size` - The size of the file browser window. Default is Vec2{x: 0.5, y: 0.0}.
25/// * `close_on_select` - If true, the file browser will close when a file is selected. Default is true.
26/// * `file_name_to_save` - The name of the file to save. Default is an empty string.
27/// * `dir_tint` - The tint to differenciate directories from files. Default is same as UiVisual::Separator.
28/// * `input_tint` - The tint of the input fields. Default is RED.to_gamma().
29///
30/// ### Examples
31/// ```
32/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
33/// use stereokit_rust::{maths::{Vec2, Vec3}, sk::SkInfo, ui::Ui, tools::os_api::get_external_path,
34///                      tools::file_browser::{FileBrowser, FILE_BROWSER_OPEN}, };
35///
36/// let id = "main".to_string();
37/// const BROWSER_SUFFIX: &str = "_file_browser";
38/// let mut file_browser = FileBrowser::default();
39/// let sk_info  = Some(sk.get_sk_info_clone());
40///
41/// if cfg!(target_os = "android") {
42///     if let Some(img_dir) = get_external_path(&sk_info) {
43///         file_browser.dir = img_dir;
44///     }
45/// }
46/// if !file_browser.dir.exists() {
47///     file_browser.dir = std::env::current_dir().unwrap_or_default().join("tests");
48/// }
49/// file_browser.caller = id.clone();
50/// file_browser.window_pose = Ui::popup_pose([-0.02, 0.04, 1.40]);
51/// file_browser.window_size = Vec2{x: 0.16, y: 0.0};
52/// SkInfo::send_event(&sk_info, StepperAction::add(id.clone() + BROWSER_SUFFIX, file_browser));
53///
54/// filename_scr = "screenshots/file_browser.jpeg";
55/// test_screenshot!( // !!!! Get a proper main loop !!!!
56///     for event in token.get_event_report() {
57///         if let StepperAction::Event(stepper_id, key, value) = event{
58///             if stepper_id == &id && key.eq(FILE_BROWSER_OPEN) {
59///                println!("Selected file: {}", value);
60///             }   
61///         }
62///     }
63/// );
64/// ```
65/// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/file_browser.jpeg" alt="screenshot" width="200">
66///
67/// ```
68/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
69/// use stereokit_rust::{maths::{Vec2, Vec3}, sk::SkInfo, ui::Ui, tools::os_api::get_external_path,
70///                      tools::file_browser::{FileBrowser, FILE_BROWSER_SAVE}, util::PickerMode, };
71///
72/// let id = "main".to_string();
73/// const BROWSER_SUFFIX: &str = "_file_to_save";
74/// let mut file_browser = FileBrowser::default();
75/// let sk_info  = Some(sk.get_sk_info_clone());
76///
77/// if cfg!(target_os = "android") {
78///     if let Some(img_dir) = get_external_path(&sk_info) {
79///         file_browser.dir = img_dir;
80///     }
81/// }
82/// if !file_browser.dir.exists() {
83///     file_browser.dir = std::env::current_dir().unwrap_or_default().join("tests");
84/// }
85/// file_browser.picker_mode = PickerMode::Save;
86/// file_browser.caller = id.clone();
87/// file_browser.window_pose = Ui::popup_pose([-0.02, 0.09, 1.37]);
88/// file_browser.window_size = Vec2{x: 0.25, y: 0.0};
89/// file_browser.file_name_to_save = "main.rs".into();
90/// SkInfo::send_event(&sk_info, StepperAction::add(id.clone() + BROWSER_SUFFIX, file_browser));
91///
92/// filename_scr = "screenshots/file_save.jpeg";
93/// test_screenshot!( // !!!! Get a proper main loop !!!!
94///     for event in token.get_event_report() {
95///         if let StepperAction::Event(stepper_id, key, value) = event{
96///             if stepper_id == &id && key.eq(FILE_BROWSER_SAVE) {
97///                println!("Save file: {}", value);
98///             }   
99///         }
100///     }
101/// );
102/// ```
103/// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/file_save.jpeg" alt="screenshot" width="200">
104#[derive(IStepper)]
105pub struct FileBrowser {
106    id: StepperId,
107    sk_info: Option<Rc<RefCell<SkInfo>>>,
108
109    pub picker_mode: PickerMode,
110    pub dir: PathBuf,
111    files_of_dir: Vec<PathEntry>,
112    pub exts: Vec<String>,
113    pub window_pose: Pose,
114    pub window_size: Vec2,
115    pub close_on_select: bool,
116    pub caller: StepperId,
117    pub dir_buttons_tint: Color128,
118    pub input_tint: Color128,
119    pub file_name_to_save: String,
120    start_dir: PathBuf,
121    replace_existing_file: bool,
122    file_selected: u32,
123    radio_off: Sprite,
124    radio_on: Sprite,
125    close: Sprite,
126}
127
128unsafe impl Send for FileBrowser {}
129
130impl Default for FileBrowser {
131    fn default() -> Self {
132        let yellow = Color128::new(1.0, 0.0, 0.0, 1.0).to_gamma();
133        Self {
134            id: "FileBrowser".to_string(),
135            sk_info: None,
136
137            picker_mode: PickerMode::Open,
138            files_of_dir: vec![],
139            dir: PathBuf::new(),
140            exts: vec![],
141            window_pose: Pose::new(Vec3::new(0.5, 1.5, -0.5), Some(Quat::from_angles(0.0, 180.0, 0.0))),
142            window_size: Vec2::new(0.5, 0.0),
143            close_on_select: true,
144            caller: "".into(),
145            dir_buttons_tint: Ui::get_element_color(UiVisual::Separator, 0.0),
146            input_tint: yellow,
147            start_dir: PathBuf::new(),
148            file_name_to_save: String::with_capacity(255),
149            replace_existing_file: false,
150            file_selected: 0,
151            radio_off: Sprite::radio_off(),
152            radio_on: Sprite::radio_on(),
153            close: Sprite::close(),
154        }
155    }
156}
157
158impl FileBrowser {
159    /// Called from IStepper::initialize here you can abort the initialization by returning false.
160    fn start(&mut self) -> bool {
161        self.files_of_dir = get_files(&self.sk_info, self.dir.clone(), &self.exts, true);
162
163        if self.caller.is_empty() {
164            Log::err(
165                "FileBrowser must be called by an other stepper (FileBrowser::caller) it will notify of the selected file ",
166            );
167            return false;
168        }
169        if self.picker_mode == PickerMode::Save && self.close_on_select {
170            //Log::warn("FileBrowser::close_on_select true is ignored when saving a file");
171            self.close_on_select = false;
172        }
173
174        Log::diag(format!("Browsing directory {:?}", self.dir));
175        self.start_dir = self.dir.clone();
176
177        true
178    }
179
180    /// Called from IStepper::step, here you can check the event report
181    fn check_event(&mut self, _id: &StepperId, _key: &str, _value: &str) {}
182
183    /// Called from IStepper::step after check_event, here you can draw your UI and scene
184    fn draw(&mut self, _token: &MainThreadToken) {
185        let mut dir_selected = None;
186
187        // The window to select existing file
188        let mut window_text2 = String::with_capacity(2048);
189        let window_text = if self.exts.is_empty() {
190            format!("{:?}", self.dir)
191        } else {
192            format!("{:?} with type {:?}", self.dir, self.exts)
193        };
194        window_text2.push_str(&window_text);
195
196        Ui::window_begin(&window_text, &mut self.window_pose, Some(self.window_size), Some(UiWin::Normal), None);
197        if Ui::button_img_at(
198            "a",
199            &self.close,
200            None,
201            Vec3::new(self.window_size.x / 2.0 + 0.04, 0.03, 0.01),
202            Vec2::new(0.03, 0.03),
203            None,
204        ) {
205            self.close_me();
206        }
207
208        if self.picker_mode == PickerMode::Save {
209            Ui::push_tint(self.input_tint);
210            Ui::label("File name: ", None, false);
211            Ui::same_line();
212            Ui::input("filename_to_save", &mut self.file_name_to_save, None, None);
213            let file = self.dir.join(&self.file_name_to_save);
214
215            let mut ok_to_save = false;
216            for ext in &self.exts {
217                if self.file_name_to_save.ends_with(ext) {
218                    ok_to_save = true;
219                    break;
220                }
221            }
222
223            if file.exists() && !self.file_name_to_save.is_empty() {
224                Ui::toggle("Replace existing file", &mut self.replace_existing_file, None);
225            } else {
226                self.replace_existing_file = false;
227            }
228            ok_to_save = ok_to_save && (!file.exists() || file.exists() && self.replace_existing_file);
229            Ui::push_enabled(ok_to_save, None);
230            Ui::same_line();
231            if Ui::button("Save", None) {
232                // Be sure we can save the file
233                SkInfo::send_event(
234                    &self.sk_info,
235                    StepperAction::event(
236                        self.caller.as_str(),
237                        FILE_BROWSER_SAVE,
238                        file.to_str().unwrap_or("problemo!!"),
239                    ),
240                );
241                self.close_me();
242            }
243            Ui::pop_enabled();
244            Ui::pop_tint();
245            Ui::next_line();
246        }
247
248        let mut i = 0;
249        for file_name in &self.files_of_dir {
250            i += 1;
251
252            if let PathEntry::File(name) = file_name {
253                let file_name_str = name.to_str().unwrap_or("OsString error!!");
254                Ui::same_line();
255                if Ui::radio_img(
256                    file_name_str,
257                    self.file_selected == i,
258                    &self.radio_off,
259                    &self.radio_on,
260                    UiBtnLayout::Left,
261                    None,
262                ) {
263                    self.file_selected = i;
264
265                    if self.picker_mode == PickerMode::Save {
266                        self.file_name_to_save = file_name_str.to_string();
267                        self.replace_existing_file = false;
268                    } else {
269                        let file = self.dir.join(file_name_str);
270                        SkInfo::send_event(
271                            &self.sk_info,
272                            StepperAction::event(
273                                self.caller.as_str(),
274                                FILE_BROWSER_OPEN,
275                                file.to_str().unwrap_or("problemo!!"),
276                            ),
277                        );
278                        if self.close_on_select {
279                            self.close_me()
280                        }
281                    }
282                }
283            }
284        }
285        Ui::next_line();
286        Ui::push_tint(self.dir_buttons_tint);
287        if let Some(sub_dir_name) = self.dir.to_str() {
288            if !sub_dir_name.is_empty() {
289                Ui::push_enabled(self.dir != self.start_dir, None);
290                //---back button
291                if Ui::button("..", None) {
292                    self.dir.pop();
293                    dir_selected = Some(get_files(&self.sk_info, self.dir.clone(), &self.exts, true));
294                }
295                Ui::pop_enabled();
296            }
297        }
298        let cur_dir = self.dir.clone();
299        // we add the dir at the end
300        let mut sub_dir: String = String::with_capacity(2048);
301        sub_dir += cur_dir.to_string_lossy().to_string().as_str();
302        if !sub_dir.is_empty() {
303            sub_dir.insert(sub_dir.len(), '/');
304        }
305        for file_name in &self.files_of_dir {
306            if let PathEntry::Dir(name) = file_name {
307                let dir_name = name.to_str().unwrap_or("OsString error!!");
308                Ui::same_line();
309                if Ui::button(dir_name, None) {
310                    self.dir.push(dir_name);
311                    dir_selected = Some(get_files(&self.sk_info, self.dir.clone(), &self.exts, true));
312                }
313            }
314        }
315        Ui::pop_tint();
316
317        if let Some(new_value) = dir_selected {
318            self.files_of_dir = new_value;
319            self.file_selected = 0;
320        }
321        Ui::window_end();
322    }
323
324    fn close_me(&self) {
325        SkInfo::send_event(&self.sk_info, StepperAction::remove(self.id.clone()));
326    }
327}