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}