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