1use crate::{
22 button::{ButtonBuilder, ButtonMessage},
23 core::{
24 algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
25 visitor::prelude::*,
26 },
27 define_constructor, define_widget_deref,
28 draw::DrawingContext,
29 file_browser::{FileBrowser, FileBrowserBuilder, FileBrowserMessage, FileBrowserMode, Filter},
30 grid::{Column, GridBuilder, Row},
31 message::{MessageDirection, OsEvent, UiMessage},
32 stack_panel::StackPanelBuilder,
33 text::TextMessage,
34 text_box::TextBoxBuilder,
35 widget::{Widget, WidgetBuilder, WidgetMessage},
36 window::{Window, WindowBuilder, WindowMessage, WindowTitle},
37 BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
38 VerticalAlignment,
39};
40
41use fyrox_core::uuid_provider;
42use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
43use fyrox_graph::BaseSceneGraph;
44use std::{
45 ops::{Deref, DerefMut},
46 path::{Path, PathBuf},
47};
48
49#[derive(Debug, Clone, PartialEq)]
50pub enum FileSelectorMessage {
51 Root(Option<PathBuf>),
52 Path(PathBuf),
53 Commit(PathBuf),
54 FocusCurrentPath,
55 Cancel,
56 Filter(Option<Filter>),
57}
58
59impl FileSelectorMessage {
60 define_constructor!(FileSelectorMessage:Commit => fn commit(PathBuf), layout: false);
61 define_constructor!(FileSelectorMessage:Root => fn root(Option<PathBuf>), layout: false);
62 define_constructor!(FileSelectorMessage:Path => fn path(PathBuf), layout: false);
63 define_constructor!(FileSelectorMessage:Cancel => fn cancel(), layout: false);
64 define_constructor!(FileSelectorMessage:FocusCurrentPath => fn focus_current_path(), layout: false);
65 define_constructor!(FileSelectorMessage:Filter => fn filter(Option<Filter>), layout: false);
66}
67
68#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
71#[reflect(derived_type = "UiNode")]
72pub struct FileSelector {
73 #[component(include)]
74 pub window: Window,
75 pub browser: Handle<UiNode>,
76 pub ok: Handle<UiNode>,
77 pub cancel: Handle<UiNode>,
78}
79
80impl ConstructorProvider<UiNode, UserInterface> for FileSelector {
81 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
82 GraphNodeConstructor::new::<Self>()
83 .with_variant("File Selector", |ui| {
84 FileSelectorBuilder::new(WindowBuilder::new(
85 WidgetBuilder::new().with_name("File Selector"),
86 ))
87 .build(&mut ui.build_ctx())
88 .into()
89 })
90 .with_group("File System")
91 }
92}
93
94impl Deref for FileSelector {
95 type Target = Widget;
96
97 fn deref(&self) -> &Self::Target {
98 &self.window
99 }
100}
101
102impl DerefMut for FileSelector {
103 fn deref_mut(&mut self) -> &mut Self::Target {
104 &mut self.window
105 }
106}
107
108uuid_provider!(FileSelector = "878b2220-03e6-4a50-a97d-3a8e5397b6cb");
109
110impl Control for FileSelector {
113 fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
114 self.window.measure_override(ui, available_size)
115 }
116
117 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
118 self.window.arrange_override(ui, final_size)
119 }
120
121 fn draw(&self, drawing_context: &mut DrawingContext) {
122 self.window.draw(drawing_context)
123 }
124
125 fn update(&mut self, dt: f32, ui: &mut UserInterface) {
126 self.window.update(dt, ui);
127 }
128
129 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
130 self.window.handle_routed_message(ui, message);
131
132 if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
133 if message.destination() == self.ok {
134 let path = ui
135 .node(self.browser)
136 .cast::<FileBrowser>()
137 .expect("self.browser must be FileBrowser")
138 .path
139 .clone();
140
141 ui.send_message(FileSelectorMessage::commit(
142 self.handle,
143 MessageDirection::ToWidget,
144 path,
145 ));
146 } else if message.destination() == self.cancel {
147 ui.send_message(FileSelectorMessage::cancel(
148 self.handle,
149 MessageDirection::ToWidget,
150 ))
151 }
152 } else if let Some(msg) = message.data::<FileSelectorMessage>() {
153 if message.destination() == self.handle {
154 match msg {
155 FileSelectorMessage::Commit(_) | FileSelectorMessage::Cancel => ui
156 .send_message(WindowMessage::close(
157 self.handle,
158 MessageDirection::ToWidget,
159 )),
160 FileSelectorMessage::Path(path) => ui.send_message(FileBrowserMessage::path(
161 self.browser,
162 MessageDirection::ToWidget,
163 path.clone(),
164 )),
165 FileSelectorMessage::Root(root) => {
166 ui.send_message(FileBrowserMessage::root(
167 self.browser,
168 MessageDirection::ToWidget,
169 root.clone(),
170 ));
171 }
172 FileSelectorMessage::Filter(filter) => {
173 ui.send_message(FileBrowserMessage::filter(
174 self.browser,
175 MessageDirection::ToWidget,
176 filter.clone(),
177 ));
178 }
179 FileSelectorMessage::FocusCurrentPath => {
180 ui.send_message(FileBrowserMessage::focus_current_path(
181 self.browser,
182 MessageDirection::ToWidget,
183 ));
184 }
185 }
186 }
187 }
188 }
189
190 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
191 self.window.preview_message(ui, message);
192 }
193
194 fn handle_os_event(
195 &mut self,
196 self_handle: Handle<UiNode>,
197 ui: &mut UserInterface,
198 event: &OsEvent,
199 ) {
200 self.window.handle_os_event(self_handle, ui, event);
201 }
202}
203
204pub struct FileSelectorBuilder {
205 window_builder: WindowBuilder,
206 filter: Option<Filter>,
207 mode: FileBrowserMode,
208 path: PathBuf,
209 root: Option<PathBuf>,
210}
211
212impl FileSelectorBuilder {
213 pub fn new(window_builder: WindowBuilder) -> Self {
214 Self {
215 window_builder,
216 filter: None,
217 mode: FileBrowserMode::Open,
218 path: "./".into(),
219 root: None,
220 }
221 }
222
223 pub fn with_filter(mut self, filter: Filter) -> Self {
224 self.filter = Some(filter);
225 self
226 }
227
228 pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
229 path.as_ref().clone_into(&mut self.path);
230 self
231 }
232
233 pub fn with_mode(mut self, mode: FileBrowserMode) -> Self {
234 self.mode = mode;
235 self
236 }
237
238 pub fn with_root(mut self, root: PathBuf) -> Self {
239 self.root = Some(root);
240 self
241 }
242
243 pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
244 let browser;
245 let ok;
246 let cancel;
247
248 if self.window_builder.title.is_none() {
249 self.window_builder.title = Some(WindowTitle::text("Select File"));
250 }
251
252 let window = self
253 .window_builder
254 .with_content(
255 GridBuilder::new(
256 WidgetBuilder::new()
257 .with_child(
258 StackPanelBuilder::new(
259 WidgetBuilder::new()
260 .with_margin(Thickness::uniform(1.0))
261 .with_horizontal_alignment(HorizontalAlignment::Right)
262 .on_column(0)
263 .on_row(1)
264 .with_child({
265 ok = ButtonBuilder::new(
266 WidgetBuilder::new()
267 .with_tab_index(Some(1))
268 .with_margin(Thickness::uniform(1.0))
269 .with_width(100.0)
270 .with_height(30.0),
271 )
272 .with_text(match &self.mode {
273 FileBrowserMode::Open => "Open",
274 FileBrowserMode::Save { .. } => "Save",
275 })
276 .build(ctx);
277 ok
278 })
279 .with_child({
280 cancel = ButtonBuilder::new(
281 WidgetBuilder::new()
282 .with_tab_index(Some(2))
283 .with_margin(Thickness::uniform(1.0))
284 .with_width(100.0)
285 .with_height(30.0),
286 )
287 .with_text("Cancel")
288 .build(ctx);
289 cancel
290 }),
291 )
292 .with_orientation(Orientation::Horizontal)
293 .build(ctx),
294 )
295 .with_child({
296 browser = FileBrowserBuilder::new(
297 WidgetBuilder::new().on_column(0).with_tab_index(Some(0)),
298 )
299 .with_mode(self.mode)
300 .with_opt_filter(self.filter)
301 .with_path(self.path)
302 .with_opt_root(self.root)
303 .build(ctx);
304 browser
305 }),
306 )
307 .add_column(Column::stretch())
308 .add_row(Row::stretch())
309 .add_row(Row::auto())
310 .build(ctx),
311 )
312 .build_window(ctx);
313
314 let file_selector = FileSelector {
315 window,
316 browser,
317 ok,
318 cancel,
319 };
320
321 ctx.add_node(UiNode::new(file_selector))
322 }
323}
324
325#[derive(Debug, Clone, PartialEq)]
326pub enum FileSelectorFieldMessage {
327 Path(PathBuf),
328}
329
330impl FileSelectorFieldMessage {
331 define_constructor!(FileSelectorFieldMessage:Path => fn path(PathBuf), layout: false);
332}
333
334#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
335#[reflect(derived_type = "UiNode")]
336pub struct FileSelectorField {
337 widget: Widget,
338 path: PathBuf,
339 path_field: Handle<UiNode>,
340 select: Handle<UiNode>,
341 file_selector: Handle<UiNode>,
342}
343
344impl ConstructorProvider<UiNode, UserInterface> for FileSelectorField {
345 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
346 GraphNodeConstructor::new::<Self>()
347 .with_variant("File Selector Field", |ui| {
348 FileSelectorFieldBuilder::new(WidgetBuilder::new().with_name("File Selector Field"))
349 .build(&mut ui.build_ctx())
350 .into()
351 })
352 .with_group("File System")
353 }
354}
355
356define_widget_deref!(FileSelectorField);
357
358uuid_provider!(FileSelectorField = "2dbda730-8a60-4f62-aee8-2ff0ccd15bf2");
359
360impl Control for FileSelectorField {
361 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
362 self.widget.handle_routed_message(ui, message);
363
364 if let Some(TextMessage::Text(text)) = message.data() {
365 if message.destination() == self.path_field
366 && message.direction() == MessageDirection::FromWidget
367 && Path::new(text.as_str()) != self.path
368 {
369 ui.send_message(FileSelectorFieldMessage::path(
370 self.handle,
371 MessageDirection::ToWidget,
372 text.into(),
373 ));
374 }
375 } else if let Some(ButtonMessage::Click) = message.data() {
376 if message.destination() == self.select {
377 let file_selector = FileSelectorBuilder::new(
378 WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(400.0))
379 .open(false)
380 .can_minimize(false),
381 )
382 .with_path(self.path.clone())
383 .with_root(std::env::current_dir().unwrap_or_default())
384 .with_mode(FileBrowserMode::Open)
385 .build(&mut ui.build_ctx());
386
387 self.file_selector = file_selector;
388
389 ui.send_message(WindowMessage::open_modal(
390 file_selector,
391 MessageDirection::ToWidget,
392 true,
393 true,
394 ));
395 }
396 } else if let Some(FileSelectorFieldMessage::Path(new_path)) = message.data() {
397 if message.destination() == self.handle
398 && message.direction() == MessageDirection::ToWidget
399 && &self.path != new_path
400 {
401 self.path.clone_from(new_path);
402 ui.send_message(TextMessage::text(
403 self.path_field,
404 MessageDirection::ToWidget,
405 self.path.to_string_lossy().to_string(),
406 ));
407
408 ui.send_message(message.reverse());
409 }
410 }
411 }
412
413 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
414 if let Some(FileSelectorMessage::Commit(new_path)) = message.data() {
415 if message.destination() == self.file_selector {
416 ui.send_message(FileSelectorFieldMessage::path(
417 self.handle,
418 MessageDirection::ToWidget,
419 new_path.clone(),
420 ));
421 }
422 } else if let Some(WindowMessage::Close) = message.data() {
423 if message.destination() == self.file_selector {
424 ui.send_message(WidgetMessage::remove(
425 self.file_selector,
426 MessageDirection::ToWidget,
427 ));
428 }
429 }
430 }
431}
432
433pub struct FileSelectorFieldBuilder {
434 widget_builder: WidgetBuilder,
435 path: PathBuf,
436}
437
438impl FileSelectorFieldBuilder {
439 pub fn new(widget_builder: WidgetBuilder) -> Self {
440 Self {
441 widget_builder,
442 path: Default::default(),
443 }
444 }
445
446 pub fn with_path(mut self, path: PathBuf) -> Self {
447 self.path = path;
448 self
449 }
450
451 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
452 let select;
453 let path_field;
454 let field = FileSelectorField {
455 widget: self
456 .widget_builder
457 .with_preview_messages(true)
458 .with_child(
459 GridBuilder::new(
460 WidgetBuilder::new()
461 .with_child({
462 path_field = TextBoxBuilder::new(WidgetBuilder::new().on_column(0))
463 .with_text(self.path.to_string_lossy())
464 .with_vertical_text_alignment(VerticalAlignment::Center)
465 .build(ctx);
466 path_field
467 })
468 .with_child({
469 select = ButtonBuilder::new(
470 WidgetBuilder::new().on_column(1).with_width(25.0),
471 )
472 .with_text("...")
473 .build(ctx);
474 select
475 }),
476 )
477 .add_row(Row::stretch())
478 .add_column(Column::stretch())
479 .add_column(Column::auto())
480 .build(ctx),
481 )
482 .build(ctx),
483 path: self.path,
484 path_field,
485 select,
486 file_selector: Default::default(),
487 };
488
489 ctx.add_node(UiNode::new(field))
490 }
491}
492
493#[cfg(test)]
494mod test {
495 use crate::file_browser::FileSelectorBuilder;
496 use crate::window::WindowBuilder;
497 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
498
499 #[test]
500 fn test_deletion() {
501 test_widget_deletion(|ctx| {
502 FileSelectorBuilder::new(WindowBuilder::new(WidgetBuilder::new())).build(ctx)
503 });
504 }
505}