1use crate::{
22 border::BorderBuilder,
23 brush::Brush,
24 core::{
25 algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
26 type_traits::prelude::*, visitor::prelude::*,
27 },
28 define_constructor,
29 dock::DockingManager,
30 grid::{Column, GridBuilder, Row},
31 message::{CursorIcon, MessageDirection, UiMessage},
32 tab_control::{TabControl, TabControlBuilder, TabControlMessage, TabDefinition},
33 text::TextBuilder,
34 widget::{Widget, WidgetBuilder, WidgetMessage},
35 window::{Window, WindowMessage},
36 BuildContext, Control, Thickness, UiNode, UserInterface,
37};
38
39use core::f32;
40use fyrox_core::uuid_provider;
41use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
42use fyrox_graph::{BaseSceneGraph, SceneGraph};
43use std::{
44 cell::Cell,
45 ops::{Deref, DerefMut},
46};
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum TileMessage {
50 Content(TileContent),
51 Split {
53 window: Handle<UiNode>,
54 direction: SplitDirection,
55 first: bool,
56 },
57}
58
59impl TileMessage {
60 define_constructor!(TileMessage:Content => fn content(TileContent), layout: false);
61 define_constructor!(TileMessage:Split => fn split(window: Handle<UiNode>,
62 direction: SplitDirection,
63 first: bool), layout: false);
64}
65
66#[derive(Default, Debug, PartialEq, Clone, Visit, Reflect)]
67pub enum TileContent {
68 #[default]
69 Empty,
70 Window(Handle<UiNode>),
71 MultiWindow {
72 index: u32,
73 windows: Vec<Handle<UiNode>>,
74 },
75 VerticalTiles {
76 splitter: f32,
77 tiles: [Handle<UiNode>; 2],
81 },
82 HorizontalTiles {
83 splitter: f32,
84 tiles: [Handle<UiNode>; 2],
88 },
89}
90
91impl TileContent {
92 pub fn is_empty(&self) -> bool {
93 matches!(self, TileContent::Empty)
94 }
95 pub fn can_dock(&self) -> bool {
97 matches!(
98 self,
99 Self::Empty | Self::Window(_) | Self::MultiWindow { .. }
100 )
101 }
102 pub fn contains_window(&self, window: Handle<UiNode>) -> bool {
103 match self {
104 Self::Window(handle) => window == *handle,
105 Self::MultiWindow { windows, .. } => windows.contains(&window),
106 _ => false,
107 }
108 }
109 pub fn plus_window(self, window: Handle<UiNode>) -> Self {
112 match self {
113 Self::Empty => Self::Window(window),
114 Self::Window(handle) => Self::MultiWindow {
115 index: 0,
116 windows: vec![window, handle],
117 },
118 Self::MultiWindow { mut windows, .. } => {
119 windows.push(window);
120 Self::MultiWindow {
121 index: windows.len() as u32 - 1,
122 windows,
123 }
124 }
125 _ => panic!("Cannot add window to split tile"),
126 }
127 }
128 pub fn minus_window(self, window: Handle<UiNode>) -> Self {
132 match self {
133 Self::Empty => Self::Empty,
134 Self::Window(handle) => {
135 if window == handle {
136 Self::Empty
137 } else {
138 self
139 }
140 }
141 Self::MultiWindow { index, mut windows } => {
142 let current = windows.get(index as usize).copied();
143 windows.retain(|h| h != &window);
144 match windows.len() {
145 0 => Self::Empty,
146 1 => Self::Window(windows[0]),
147 _ => {
148 let index = if let Some(current) = current {
149 windows
150 .iter()
151 .position(|w| w == ¤t)
152 .unwrap_or_default() as u32
153 } else {
154 0
155 };
156 Self::MultiWindow { index, windows }
157 }
158 }
159 }
160 _ => panic!("Cannot subtract window from split tile"),
161 }
162 }
163 pub fn with_active(self, window: Handle<UiNode>) -> Self {
167 match self {
168 Self::MultiWindow { index, windows } => {
169 let index = if let Some(index) = windows.iter().position(|h| h == &window) {
170 index as u32
171 } else {
172 index
173 };
174 Self::MultiWindow { index, windows }
175 }
176 _ => self,
177 }
178 }
179}
180
181fn send_visibility(ui: &UserInterface, destination: Handle<UiNode>, visible: bool) {
182 ui.send_message(WidgetMessage::visibility(
183 destination,
184 MessageDirection::ToWidget,
185 visible,
186 ));
187}
188
189fn send_size(ui: &UserInterface, destination: Handle<UiNode>, width: f32, height: f32) {
190 ui.send_message(WidgetMessage::width(
191 destination,
192 MessageDirection::ToWidget,
193 width,
194 ));
195 ui.send_message(WidgetMessage::height(
196 destination,
197 MessageDirection::ToWidget,
198 height,
199 ));
200}
201
202fn send_background(ui: &UserInterface, destination: Handle<UiNode>, color: Color) {
203 ui.send_message(WidgetMessage::background(
204 destination,
205 MessageDirection::ToWidget,
206 Brush::Solid(color).into(),
207 ));
208}
209
210fn get_tile_window(ui: &UserInterface, tile: Handle<UiNode>) -> Option<&Window> {
213 let tile = ui.node(tile).cast::<Tile>()?;
214 let handle = match &tile.content {
215 TileContent::Window(handle) => handle,
216 TileContent::MultiWindow { index, windows } => windows.get(*index as usize)?,
217 _ => return None,
218 };
219 ui.node(*handle).cast::<Window>()
220}
221
222fn is_minimized_window(ui: &UserInterface, tile: Handle<UiNode>) -> bool {
224 let Some(window) = get_tile_window(ui, tile) else {
225 return false;
226 };
227 window.minimized()
228}
229
230fn has_one_minimized(ui: &UserInterface, content: &TileContent) -> bool {
236 let tiles = if let TileContent::VerticalTiles { tiles, .. } = content {
237 Some(tiles)
238 } else if let TileContent::HorizontalTiles { tiles, .. } = content {
239 Some(tiles)
240 } else {
241 None
242 };
243 if let Some(tiles) = tiles {
244 tiles
245 .iter()
246 .filter(|h| is_minimized_window(ui, **h))
247 .count()
248 == 1
249 } else {
250 false
251 }
252}
253
254fn deminimize_other_window(
259 this_window: Handle<UiNode>,
260 tiles: &[Handle<UiNode>; 2],
261 ui: &UserInterface,
262) {
263 let mut has_this_window = false;
264 let mut other_window: Option<Handle<UiNode>> = None;
265 for tile in tiles.iter() {
266 let Some(window) = get_tile_window(ui, *tile) else {
267 return;
268 };
269 if window.handle() == this_window {
270 has_this_window = true;
271 } else if window.minimized() {
272 other_window = Some(window.handle());
273 }
274 }
275 if !has_this_window {
276 return;
277 }
278 if let Some(handle) = other_window {
279 ui.send_message(WindowMessage::minimize(
280 handle,
281 MessageDirection::ToWidget,
282 false,
283 ));
284 }
285}
286
287#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
288#[reflect(derived_type = "UiNode")]
289pub struct Tile {
290 pub widget: Widget,
291 pub left_anchor: Handle<UiNode>,
292 pub right_anchor: Handle<UiNode>,
293 pub top_anchor: Handle<UiNode>,
294 pub bottom_anchor: Handle<UiNode>,
295 pub center_anchor: Handle<UiNode>,
296 pub tabs: Handle<UiNode>,
297 pub content: TileContent,
298 pub splitter: Handle<UiNode>,
299 pub dragging_splitter: bool,
300 pub drop_anchor: Cell<Handle<UiNode>>,
301}
302
303impl ConstructorProvider<UiNode, UserInterface> for Tile {
304 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
305 GraphNodeConstructor::new::<Self>()
306 .with_variant("Tile", |ui| {
307 TileBuilder::new(WidgetBuilder::new().with_name("Tile"))
308 .build(&mut ui.build_ctx())
309 .into()
310 })
311 .with_group("Layout")
312 }
313}
314
315crate::define_widget_deref!(Tile);
316
317uuid_provider!(Tile = "8ed17fa9-890e-4dd7-b4f9-a24660882234");
318
319impl Control for Tile {
320 fn measure_override(
321 &self,
322 ui: &UserInterface,
323 mut available_size: Vector2<f32>,
324 ) -> Vector2<f32> {
325 if has_one_minimized(ui, &self.content) {
326 return self.measure_vertical_with_minimized(ui, available_size);
327 }
328 ui.measure_node(self.tabs, Vector2::new(available_size.x, f32::INFINITY));
329 available_size.y -= ui.node(self.tabs).desired_size().y;
330 for &child_handle in self.children() {
331 if child_handle == self.tabs {
332 continue;
333 }
334 let available_size = match &self.content {
339 TileContent::VerticalTiles {
340 splitter,
341 ref tiles,
342 } => {
343 if tiles[0] == child_handle {
344 Vector2::new(available_size.x, available_size.y * splitter)
345 } else if tiles[1] == child_handle {
346 Vector2::new(available_size.x, available_size.y * (1.0 - splitter))
347 } else {
348 available_size
349 }
350 }
351 TileContent::HorizontalTiles {
352 splitter,
353 ref tiles,
354 } => {
355 if tiles[0] == child_handle {
356 Vector2::new(available_size.x * splitter, available_size.y)
357 } else if tiles[1] == child_handle {
358 Vector2::new(available_size.x * (1.0 - splitter), available_size.y)
359 } else {
360 available_size
361 }
362 }
363 _ => available_size,
364 };
365
366 ui.measure_node(child_handle, available_size);
367 }
368 match &self.content {
369 TileContent::Empty => Vector2::default(),
370 TileContent::Window(handle) => ui.node(*handle).desired_size(),
371 TileContent::MultiWindow { index, windows } => {
372 let tabs = ui.node(self.tabs).desired_size();
373 let body = windows
374 .get(*index as usize)
375 .map(|w| ui.node(*w).desired_size())
376 .unwrap_or_default();
377 let y = if available_size.y.is_finite() {
378 (available_size.y - tabs.y).max(0.0)
379 } else {
380 tabs.y + body.y
381 };
382 Vector2::new(tabs.x.max(body.x), y)
383 }
384 TileContent::VerticalTiles { tiles, .. } => {
385 let mut w = 0.0f32;
386 let mut h = DEFAULT_SPLITTER_SIZE;
387 for size in tiles.map(|c| ui.node(c).desired_size()) {
388 w = w.max(size.x);
389 h += size.y;
390 }
391 Vector2::new(w, h)
392 }
393 TileContent::HorizontalTiles { tiles, .. } => {
394 let mut w = DEFAULT_SPLITTER_SIZE;
395 let mut h = 0.0f32;
396 for size in tiles.map(|c| ui.node(c).desired_size()) {
397 w += size.x;
398 h = h.max(size.y);
399 }
400 Vector2::new(w, h)
401 }
402 }
403 }
404
405 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
406 let splitter_size = ui.node(self.splitter).desired_size();
407
408 if has_one_minimized(ui, &self.content) {
409 return self.arrange_vertical_with_minimized(ui, final_size);
410 }
411
412 let tabs_height = ui.node(self.tabs).desired_size().y;
413 ui.arrange_node(self.tabs, &Rect::new(0.0, 0.0, final_size.x, tabs_height));
414 let full_bounds = Rect::new(0.0, tabs_height, final_size.x, final_size.y - tabs_height);
415 for &child_handle in self.children() {
416 if child_handle == self.tabs {
417 continue;
418 }
419 let bounds = match &self.content {
420 TileContent::VerticalTiles {
421 splitter,
422 ref tiles,
423 } => {
424 if tiles[0] == child_handle {
425 Rect::new(
426 0.0,
427 0.0,
428 final_size.x,
429 final_size.y * splitter - DEFAULT_SPLITTER_SIZE * 0.5,
430 )
431 } else if tiles[1] == child_handle {
432 Rect::new(
433 0.0,
434 final_size.y * splitter + splitter_size.y * 0.5,
435 final_size.x,
436 final_size.y * (1.0 - splitter) - DEFAULT_SPLITTER_SIZE * 0.5,
437 )
438 } else if self.splitter == child_handle {
439 Rect::new(
440 0.0,
441 final_size.y * splitter - DEFAULT_SPLITTER_SIZE * 0.5,
442 final_size.x,
443 DEFAULT_SPLITTER_SIZE,
444 )
445 } else {
446 full_bounds
447 }
448 }
449 TileContent::HorizontalTiles {
450 splitter,
451 ref tiles,
452 } => {
453 if tiles[0] == child_handle {
454 Rect::new(
455 0.0,
456 0.0,
457 final_size.x * splitter - DEFAULT_SPLITTER_SIZE * 0.5,
458 final_size.y,
459 )
460 } else if tiles[1] == child_handle {
461 Rect::new(
462 final_size.x * splitter + DEFAULT_SPLITTER_SIZE * 0.5,
463 0.0,
464 final_size.x * (1.0 - splitter) - DEFAULT_SPLITTER_SIZE * 0.5,
465 final_size.y,
466 )
467 } else if self.splitter == child_handle {
468 Rect::new(
469 final_size.x * splitter - DEFAULT_SPLITTER_SIZE * 0.5,
470 0.0,
471 DEFAULT_SPLITTER_SIZE,
472 final_size.y,
473 )
474 } else {
475 full_bounds
476 }
477 }
478 _ => full_bounds,
479 };
480
481 ui.arrange_node(child_handle, &bounds);
482 }
483
484 final_size
485 }
486
487 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
488 self.widget.handle_routed_message(ui, message);
489
490 if let Some(TabControlMessage::ActiveTabUuid(Some(id))) = message.data() {
491 if message.destination() == self.tabs
492 && message.direction() == MessageDirection::FromWidget
493 {
494 self.change_active_tab(id, ui);
495 }
496 } else if let Some(msg) = message.data::<TileMessage>() {
497 if message.destination() == self.handle() {
498 match msg {
499 TileMessage::Content(content) => {
500 self.content = content.clone();
501
502 send_visibility(
503 ui,
504 self.tabs,
505 matches!(self.content, TileContent::MultiWindow { .. }),
506 );
507
508 match content {
509 TileContent::Empty => {
510 send_visibility(ui, self.splitter, false);
511 }
512 &TileContent::Window(window) => {
513 send_visibility(ui, self.splitter, false);
514 send_visibility(ui, window, true);
515 self.dock(window, ui);
516 }
517 TileContent::MultiWindow { index, windows } => {
518 send_visibility(ui, self.splitter, false);
519 let tabs = ui.node(self.tabs).cast::<TabControl>().unwrap();
520 for tab in tabs.tabs.iter() {
521 let uuid = tab.uuid;
522 if !windows.iter().any(|&h| ui.node(h).id == uuid) {
523 ui.send_message(TabControlMessage::remove_tab_by_uuid(
524 self.tabs,
525 MessageDirection::ToWidget,
526 uuid,
527 ));
528 }
529 }
530 for (i, &w) in windows.iter().enumerate() {
531 let is_active = i as u32 == *index;
532 let uuid = ui.node(w).id;
533 let tabs = ui.node(self.tabs).cast::<TabControl>().unwrap();
534 if tabs.get_tab_by_uuid(uuid).is_none() {
535 self.add_tab(w, ui);
536 }
537 send_visibility(ui, w, is_active);
538 self.dock(w, ui);
539 }
540 if let Some(&w) = windows.get(*index as usize) {
541 let uuid = ui.node(w).id;
542 ui.send_message(TabControlMessage::active_tab_uuid(
543 self.tabs,
544 MessageDirection::ToWidget,
545 Some(uuid),
546 ));
547 }
548 }
549 TileContent::VerticalTiles { tiles, .. }
550 | TileContent::HorizontalTiles { tiles, .. } => {
551 for &tile in tiles {
552 ui.send_message(WidgetMessage::link(
553 tile,
554 MessageDirection::ToWidget,
555 self.handle(),
556 ));
557 }
558
559 send_visibility(ui, self.splitter, true);
560 match content {
561 TileContent::HorizontalTiles { .. } => {
562 ui.send_message(WidgetMessage::cursor(
563 self.splitter,
564 MessageDirection::ToWidget,
565 Some(CursorIcon::WResize),
566 ));
567 }
568 TileContent::VerticalTiles { .. } => {
569 ui.send_message(WidgetMessage::cursor(
570 self.splitter,
571 MessageDirection::ToWidget,
572 Some(CursorIcon::NResize),
573 ));
574 }
575 _ => (),
576 }
577 }
578 }
579 }
580 &TileMessage::Split {
581 window,
582 direction,
583 first,
584 } => {
585 if matches!(
586 self.content,
587 TileContent::Window(_) | TileContent::MultiWindow { .. }
588 ) {
589 self.split(ui, window, direction, first);
590 }
591 }
592 }
593 }
594 } else if let Some(msg) = message.data::<WidgetMessage>() {
595 match msg {
596 &WidgetMessage::Topmost => {
597 if let TileContent::MultiWindow { ref windows, .. } = self.content {
598 if windows.contains(&message.destination()) {
599 let id = ui.node(message.destination()).id;
600 self.change_active_tab(&id, ui);
601 }
602 }
603 }
604 &WidgetMessage::MouseDown { .. } => {
605 if !message.handled()
606 && message.destination() == self.splitter
607 && !has_one_minimized(ui, &self.content)
608 {
609 message.set_handled(true);
610 self.dragging_splitter = true;
611 ui.capture_mouse(self.splitter);
612 }
613 }
614 &WidgetMessage::MouseUp { .. } => {
615 if !message.handled() && message.destination() == self.splitter {
616 message.set_handled(true);
617 self.dragging_splitter = false;
618 ui.release_mouse_capture();
619 }
620 }
621 &WidgetMessage::MouseMove { pos, .. } => {
622 if self.dragging_splitter {
623 let bounds = self.screen_bounds();
624 match self.content {
625 TileContent::VerticalTiles {
626 ref mut splitter, ..
627 } => {
628 *splitter = ((pos.y - bounds.y()) / bounds.h()).clamp(0.0, 1.0);
629 self.invalidate_layout();
630 }
631 TileContent::HorizontalTiles {
632 ref mut splitter, ..
633 } => {
634 *splitter = ((pos.x - bounds.x()) / bounds.w()).clamp(0.0, 1.0);
635 self.invalidate_layout();
636 }
637 _ => (),
638 }
639 }
640 }
641 WidgetMessage::Unlink => {
642 match self.content {
644 TileContent::VerticalTiles { tiles, .. }
645 | TileContent::HorizontalTiles { tiles, .. } => {
646 let mut has_empty_sub_tile = false;
647 for &tile in &tiles {
648 if let Some(sub_tile) = ui.node(tile).cast::<Tile>() {
649 if let TileContent::Empty = sub_tile.content {
650 has_empty_sub_tile = true;
651 break;
652 }
653 }
654 }
655 if has_empty_sub_tile {
656 for &tile in &tiles {
657 if let Some(sub_tile) = ui.node(tile).cast::<Tile>() {
658 match sub_tile.content {
659 TileContent::Window(sub_tile_wnd) => {
660 ui.send_message(WidgetMessage::unlink(
663 sub_tile_wnd,
664 MessageDirection::ToWidget,
665 ));
666
667 ui.send_message(TileMessage::content(
668 self.handle,
669 MessageDirection::ToWidget,
670 TileContent::Window(sub_tile_wnd),
671 ));
672 send_visibility(ui, self.splitter, false);
674 }
675 TileContent::MultiWindow { index, ref windows } => {
676 for &sub_tile_wnd in windows {
677 ui.send_message(WidgetMessage::unlink(
678 sub_tile_wnd,
679 MessageDirection::ToWidget,
680 ));
681 }
682
683 ui.send_message(TileMessage::content(
684 self.handle,
685 MessageDirection::ToWidget,
686 TileContent::MultiWindow {
687 index,
688 windows: windows.clone(),
689 },
690 ));
691 send_visibility(ui, self.splitter, false);
693 }
694 TileContent::VerticalTiles {
697 splitter,
698 tiles: sub_tiles,
699 } => {
700 for &sub_tile in &sub_tiles {
701 ui.send_message(WidgetMessage::unlink(
702 sub_tile,
703 MessageDirection::ToWidget,
704 ));
705 }
706 ui.send_message(TileMessage::content(
708 self.handle,
709 MessageDirection::ToWidget,
710 TileContent::VerticalTiles {
711 splitter,
712 tiles: sub_tiles,
713 },
714 ));
715 }
716 TileContent::HorizontalTiles {
717 splitter,
718 tiles: sub_tiles,
719 } => {
720 for &sub_tile in &sub_tiles {
721 ui.send_message(WidgetMessage::unlink(
722 sub_tile,
723 MessageDirection::ToWidget,
724 ));
725 }
726 ui.send_message(TileMessage::content(
728 self.handle,
729 MessageDirection::ToWidget,
730 TileContent::HorizontalTiles {
731 splitter,
732 tiles: sub_tiles,
733 },
734 ));
735 }
736 _ => {}
737 }
738 }
739 }
740
741 for &tile in &tiles {
743 ui.send_message(WidgetMessage::remove(
744 tile,
745 MessageDirection::ToWidget,
746 ));
747 }
748 }
749 }
750 _ => (),
751 }
752 }
753 _ => {}
754 }
755 } else if let Some(msg) = message.data::<WindowMessage>() {
757 match msg {
758 WindowMessage::Maximize(true) => {
759 let content_moved = self.content.contains_window(message.destination());
761 if content_moved {
762 if let Some(window) = ui.node(message.destination()).cast::<Window>() {
765 self.undock(window, ui);
766 ui.send_message(WindowMessage::maximize(
767 window.handle(),
768 MessageDirection::ToWidget,
769 true,
770 ));
771 }
772 }
773 }
774 WindowMessage::Minimize(true) => {
775 let tiles = match &self.content {
776 TileContent::VerticalTiles { tiles, .. } => Some(tiles),
777 TileContent::HorizontalTiles { tiles, .. } => Some(tiles),
778 _ => None,
779 };
780 if let Some(tiles) = tiles {
781 deminimize_other_window(message.destination(), tiles, ui);
782 }
783 }
784 WindowMessage::Move(_) => {
785 let content_moved = self.content.contains_window(message.destination());
787
788 if content_moved {
789 if let Some(window) = ui.node(message.destination()).cast::<Window>() {
790 if window.drag_delta.norm() > 20.0 {
791 self.undock(window, ui);
792 }
793 }
794 }
795 }
796 WindowMessage::Close => match self.content {
797 TileContent::MultiWindow { ref windows, .. } => {
798 if windows.contains(&message.destination()) {
799 let window = ui
800 .node(message.destination())
801 .cast::<Window>()
802 .expect("must be window");
803 self.undock(window, ui);
804 }
805 }
806 TileContent::VerticalTiles { tiles, .. }
807 | TileContent::HorizontalTiles { tiles, .. } => {
808 let closed_window = message.destination();
809
810 fn tile_has_window(
811 tile: Handle<UiNode>,
812 ui: &UserInterface,
813 window: Handle<UiNode>,
814 ) -> bool {
815 if let Some(tile_ref) = ui.node(tile).query_component::<Tile>() {
816 if let TileContent::Window(tile_window) = tile_ref.content {
817 tile_window == window
818 } else {
819 false
820 }
821 } else {
822 false
823 }
824 }
825
826 for (tile_a_index, tile_b_index) in [(0, 1), (1, 0)] {
827 let tile_a = tiles[tile_a_index];
828 let tile_b = tiles[tile_b_index];
829 if tile_has_window(tile_a, ui, closed_window) {
830 if let Some(tile_a_ref) = ui.node(tile_a).query_component::<Tile>()
831 {
832 let window = ui
833 .node(closed_window)
834 .cast::<Window>()
835 .expect("must be window");
836 tile_a_ref.undock(window, ui);
837 }
838 if let Some(tile_b_ref) = ui.node(tile_b).query_component::<Tile>()
839 {
840 ui.send_message(WidgetMessage::unlink(
841 closed_window,
842 MessageDirection::ToWidget,
843 ));
844
845 tile_b_ref.unlink_content(ui);
846
847 ui.send_message(TileMessage::content(
848 self.handle,
849 MessageDirection::ToWidget,
850 tile_b_ref.content.clone(),
851 ));
852
853 for &tile in &tiles {
855 ui.send_message(WidgetMessage::remove(
856 tile,
857 MessageDirection::ToWidget,
858 ));
859 }
860
861 if let Some((_, docking_manager)) =
862 ui.find_component_up::<DockingManager>(self.parent())
863 {
864 docking_manager
865 .floating_windows
866 .borrow_mut()
867 .push(closed_window);
868 }
869
870 break;
871 }
872 }
873 }
874 }
875 _ => {}
876 },
877 _ => (),
878 }
879 }
880 }
881
882 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
885 if let Some(msg) = message.data::<WindowMessage>() {
886 if let Some((_, docking_manager)) =
887 ui.find_component_up::<DockingManager>(self.parent())
888 {
889 if message.direction() == MessageDirection::FromWidget
891 && docking_manager
892 .floating_windows
893 .borrow_mut()
894 .contains(&message.destination())
895 {
896 match msg {
897 &WindowMessage::Move(_) => {
898 if self.content.can_dock() {
900 for &anchor in &self.anchors() {
902 send_visibility(ui, anchor, true);
903 }
904 let pos = ui.cursor_position;
906 for &anchor in &self.anchors() {
907 send_background(ui, anchor, DEFAULT_ANCHOR_COLOR);
908 }
909 if ui.node(self.left_anchor).screen_bounds().contains(pos) {
910 send_background(ui, self.left_anchor, Color::WHITE);
911 self.drop_anchor.set(self.left_anchor);
912 } else if ui.node(self.right_anchor).screen_bounds().contains(pos) {
913 send_background(ui, self.right_anchor, Color::WHITE);
914 self.drop_anchor.set(self.right_anchor);
915 } else if ui.node(self.top_anchor).screen_bounds().contains(pos) {
916 send_background(ui, self.top_anchor, Color::WHITE);
917 self.drop_anchor.set(self.top_anchor);
918 } else if ui.node(self.bottom_anchor).screen_bounds().contains(pos)
919 {
920 send_background(ui, self.bottom_anchor, Color::WHITE);
921 self.drop_anchor.set(self.bottom_anchor);
922 } else if ui.node(self.center_anchor).screen_bounds().contains(pos)
923 {
924 send_background(ui, self.center_anchor, Color::WHITE);
925 self.drop_anchor.set(self.center_anchor);
926 } else {
927 self.drop_anchor.set(Handle::NONE);
928 }
929 }
930 }
931 WindowMessage::MoveEnd => {
932 for &anchor in &self.anchors() {
934 send_visibility(ui, anchor, false);
935 }
936
937 if self.drop_anchor.get().is_some() {
939 match &self.content {
940 TileContent::Empty => {
941 if self.drop_anchor.get() == self.center_anchor {
942 ui.send_message(TileMessage::content(
943 self.handle,
944 MessageDirection::ToWidget,
945 TileContent::Window(message.destination()),
946 ));
947 ui.send_message(WidgetMessage::link(
948 message.destination(),
949 MessageDirection::ToWidget,
950 self.handle,
951 ));
952 }
953 }
954 TileContent::Window(_) | TileContent::MultiWindow { .. } => {
955 if self.drop_anchor.get() == self.left_anchor {
956 ui.send_message(TileMessage::split(
958 self.handle,
959 MessageDirection::ToWidget,
960 message.destination(),
961 SplitDirection::Horizontal,
962 true,
963 ));
964 } else if self.drop_anchor.get() == self.right_anchor {
965 ui.send_message(TileMessage::split(
967 self.handle,
968 MessageDirection::ToWidget,
969 message.destination(),
970 SplitDirection::Horizontal,
971 false,
972 ));
973 } else if self.drop_anchor.get() == self.top_anchor {
974 ui.send_message(TileMessage::split(
976 self.handle,
977 MessageDirection::ToWidget,
978 message.destination(),
979 SplitDirection::Vertical,
980 true,
981 ));
982 } else if self.drop_anchor.get() == self.bottom_anchor {
983 ui.send_message(TileMessage::split(
985 self.handle,
986 MessageDirection::ToWidget,
987 message.destination(),
988 SplitDirection::Vertical,
989 false,
990 ));
991 } else if self.drop_anchor.get() == self.center_anchor {
992 ui.send_message(TileMessage::content(
993 self.handle,
994 MessageDirection::ToWidget,
995 self.content
996 .clone()
997 .plus_window(message.destination()),
998 ));
999 }
1000 }
1001 _ => (),
1003 }
1004 }
1005 }
1006 _ => (),
1007 }
1008 }
1009 }
1010 }
1011 }
1012}
1013
1014#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1015pub enum SplitDirection {
1016 Horizontal,
1017 Vertical,
1018}
1019
1020fn create_tab_header(label: String, ctx: &mut BuildContext) -> Handle<UiNode> {
1021 let min_size = Vector2::new(50.0, 12.0);
1022 let margin = Thickness {
1023 left: 4.0,
1024 top: 2.0,
1025 right: 4.0,
1026 bottom: 2.0,
1027 };
1028 TextBuilder::new(
1029 WidgetBuilder::new()
1030 .with_min_size(min_size)
1031 .with_margin(margin),
1032 )
1033 .with_text(label)
1034 .build(ctx)
1035}
1036
1037impl Tile {
1038 fn change_active_tab(&mut self, id: &Uuid, ui: &mut UserInterface) {
1039 let TileContent::MultiWindow { index, windows } = &self.content else {
1040 return;
1041 };
1042 let mut window = None;
1043 for (i, w) in windows.iter().enumerate() {
1044 let window_id = ui.node(*w).id;
1045 if &window_id == id {
1046 if i as u32 == *index {
1047 return;
1048 } else {
1049 window = Some(*w);
1050 break;
1051 }
1052 }
1053 }
1054 let Some(window) = window else {
1055 return;
1056 };
1057 let new_content = self.content.clone().with_active(window);
1058 ui.send_message(TileMessage::content(
1059 self.handle(),
1060 MessageDirection::ToWidget,
1061 new_content,
1062 ));
1063 }
1064 fn unlink_content(&self, ui: &UserInterface) {
1065 match &self.content {
1066 TileContent::Empty => {}
1067 TileContent::Window(window) => {
1068 ui.send_message(WidgetMessage::unlink(*window, MessageDirection::ToWidget));
1069 }
1070 TileContent::MultiWindow { windows, .. } => {
1071 for tile in windows.iter() {
1072 ui.send_message(WidgetMessage::unlink(*tile, MessageDirection::ToWidget));
1073 }
1074 }
1075 TileContent::VerticalTiles {
1076 tiles: sub_tiles, ..
1077 }
1078 | TileContent::HorizontalTiles {
1079 tiles: sub_tiles, ..
1080 } => {
1081 for tile in sub_tiles {
1082 ui.send_message(WidgetMessage::unlink(*tile, MessageDirection::ToWidget));
1083 }
1084 }
1085 }
1086 }
1087 fn add_tab(&self, window: Handle<UiNode>, ui: &mut UserInterface) {
1089 let window = ui.node(window).cast::<Window>().expect("must be window");
1090 let uuid = window.id;
1091 let header = create_tab_header(window.tab_label().to_owned(), &mut ui.build_ctx());
1092 let definition = TabDefinition {
1093 can_be_closed: false,
1094 header,
1095 content: Handle::NONE,
1096 user_data: None,
1097 };
1098 ui.send_message(TabControlMessage::add_tab_with_uuid(
1099 self.tabs,
1100 MessageDirection::ToWidget,
1101 uuid,
1102 definition,
1103 ));
1104 }
1105 fn dock(&self, window: Handle<UiNode>, ui: &UserInterface) {
1108 ui.send_message(WidgetMessage::link(
1109 window,
1110 MessageDirection::ToWidget,
1111 self.handle(),
1112 ));
1113
1114 ui.send_message(WindowMessage::can_resize(
1115 window,
1116 MessageDirection::ToWidget,
1117 false,
1118 ));
1119
1120 send_size(ui, window, f32::NAN, f32::NAN);
1123 }
1124
1125 fn undock(&self, window: &Window, ui: &UserInterface) {
1129 ui.send_message(TileMessage::content(
1130 self.handle,
1131 MessageDirection::ToWidget,
1132 self.content.clone().minus_window(window.handle()),
1133 ));
1134
1135 ui.send_message(WidgetMessage::unlink(
1136 window.handle(),
1137 MessageDirection::ToWidget,
1138 ));
1139
1140 ui.send_message(WindowMessage::can_resize(
1141 window.handle(),
1142 MessageDirection::ToWidget,
1143 true,
1144 ));
1145
1146 let height = if window.minimized() {
1147 f32::NAN
1148 } else {
1149 self.actual_local_size().y
1150 };
1151
1152 send_size(ui, window.handle(), self.actual_local_size().x, height);
1153
1154 if let Some((_, docking_manager)) = ui.find_component_up::<DockingManager>(self.parent()) {
1155 docking_manager
1156 .floating_windows
1157 .borrow_mut()
1158 .push(window.handle());
1159 }
1160 }
1161 fn measure_vertical_with_minimized(
1165 &self,
1166 ui: &UserInterface,
1167 available_size: Vector2<f32>,
1168 ) -> Vector2<f32> {
1169 let tiles = match self.content {
1170 TileContent::VerticalTiles { ref tiles, .. } => tiles,
1171 TileContent::HorizontalTiles { ref tiles, .. } => tiles,
1172 _ => return Vector2::default(),
1173 };
1174 let minimized_index = tiles
1175 .iter()
1176 .position(|h| is_minimized_window(ui, *h))
1177 .unwrap();
1178 let minimized_handle = tiles[minimized_index];
1179 let mut size = Vector2::new(available_size.x, f32::INFINITY);
1180 ui.measure_node(minimized_handle, size);
1181 let d_1 = ui.node(minimized_handle).desired_size();
1182 size.y = available_size.y - d_1.y;
1183 let other_index = if minimized_index == 0 { 1 } else { 0 };
1184 ui.measure_node(tiles[other_index], size);
1185 size.y = 0.0;
1186 ui.measure_node(self.splitter, size);
1187 let d_2 = ui.node(tiles[other_index]).desired_size();
1188 Vector2::new(d_1.x.max(d_2.x), d_1.y + d_2.y)
1189 }
1190 fn arrange_vertical_with_minimized(
1194 &self,
1195 ui: &UserInterface,
1196 final_size: Vector2<f32>,
1197 ) -> Vector2<f32> {
1198 let tiles = match self.content {
1199 TileContent::VerticalTiles { ref tiles, .. } => tiles,
1200 TileContent::HorizontalTiles { ref tiles, .. } => tiles,
1201 _ => return final_size,
1202 };
1203 let minimized_index = tiles
1204 .iter()
1205 .position(|h| is_minimized_window(ui, *h))
1206 .unwrap();
1207 let minimized_handle = tiles[minimized_index];
1208 let height = ui.node(minimized_handle).desired_size().y;
1209 let mut bounds = if minimized_index == 0 {
1210 Rect::new(0.0, 0.0, final_size.x, height)
1211 } else {
1212 Rect::new(0.0, final_size.y - height, final_size.x, height)
1213 };
1214 ui.arrange_node(minimized_handle, &bounds);
1215 let remaining_height = final_size.y - height;
1216 bounds.position.y = if minimized_index == 0 {
1217 height
1218 } else {
1219 remaining_height
1220 };
1221 bounds.size.y = 0.0;
1222 ui.arrange_node(self.splitter, &bounds);
1223 bounds.position.y = if minimized_index == 0 { height } else { 0.0 };
1224 bounds.size.y = remaining_height;
1225 let other_index = if minimized_index == 0 { 1 } else { 0 };
1226 ui.arrange_node(tiles[other_index], &bounds);
1227 final_size
1228 }
1229
1230 pub fn anchors(&self) -> [Handle<UiNode>; 5] {
1231 [
1232 self.left_anchor,
1233 self.right_anchor,
1234 self.top_anchor,
1235 self.bottom_anchor,
1236 self.center_anchor,
1237 ]
1238 }
1239
1240 fn split(
1241 &mut self,
1242 ui: &mut UserInterface,
1243 window: Handle<UiNode>,
1244 direction: SplitDirection,
1245 first: bool,
1246 ) {
1247 let first_tile = TileBuilder::new(WidgetBuilder::new())
1248 .with_content({
1249 if first {
1250 TileContent::Window(window)
1251 } else {
1252 TileContent::Empty
1253 }
1254 })
1255 .build(&mut ui.build_ctx());
1256
1257 let second_tile = TileBuilder::new(WidgetBuilder::new())
1258 .with_content({
1259 if first {
1260 TileContent::Empty
1261 } else {
1262 TileContent::Window(window)
1263 }
1264 })
1265 .build(&mut ui.build_ctx());
1266
1267 ui.send_message(TileMessage::content(
1268 if first { second_tile } else { first_tile },
1269 MessageDirection::ToWidget,
1270 std::mem::take(&mut self.content),
1271 ));
1272
1273 ui.send_message(TileMessage::content(
1274 self.handle,
1275 MessageDirection::ToWidget,
1276 match direction {
1277 SplitDirection::Horizontal => TileContent::HorizontalTiles {
1278 tiles: [first_tile, second_tile],
1279 splitter: 0.5,
1280 },
1281 SplitDirection::Vertical => TileContent::VerticalTiles {
1282 tiles: [first_tile, second_tile],
1283 splitter: 0.5,
1284 },
1285 },
1286 ));
1287 }
1288}
1289
1290pub struct TileBuilder {
1291 widget_builder: WidgetBuilder,
1292 content: TileContent,
1293}
1294
1295pub const DEFAULT_SPLITTER_SIZE: f32 = 5.0;
1296pub const DEFAULT_ANCHOR_COLOR: Color = Color::opaque(150, 150, 150);
1297
1298pub fn make_default_anchor(ctx: &mut BuildContext, row: usize, column: usize) -> Handle<UiNode> {
1299 let default_anchor_size = 30.0;
1300 BorderBuilder::new(
1301 WidgetBuilder::new()
1302 .with_width(default_anchor_size)
1303 .with_height(default_anchor_size)
1304 .with_visibility(false)
1305 .on_row(row)
1306 .on_column(column)
1307 .with_draw_on_top(true)
1308 .with_background(Brush::Solid(DEFAULT_ANCHOR_COLOR).into()),
1309 )
1310 .build(ctx)
1311}
1312
1313impl TileBuilder {
1314 pub fn new(widget_builder: WidgetBuilder) -> Self {
1315 Self {
1316 widget_builder,
1317 content: TileContent::Empty,
1318 }
1319 }
1320
1321 pub fn with_content(mut self, content: TileContent) -> Self {
1322 self.content = content;
1323 self
1324 }
1325
1326 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
1327 let left_anchor = make_default_anchor(ctx, 2, 1);
1328 let right_anchor = make_default_anchor(ctx, 2, 3);
1329 let dock_anchor = make_default_anchor(ctx, 2, 2);
1330 let top_anchor = make_default_anchor(ctx, 1, 2);
1331 let bottom_anchor = make_default_anchor(ctx, 3, 2);
1332
1333 let grid = GridBuilder::new(
1334 WidgetBuilder::new()
1335 .with_child(left_anchor)
1336 .with_child(dock_anchor)
1337 .with_child(right_anchor)
1338 .with_child(top_anchor)
1339 .with_child(bottom_anchor),
1340 )
1341 .add_row(Row::stretch())
1342 .add_row(Row::auto())
1343 .add_row(Row::auto())
1344 .add_row(Row::auto())
1345 .add_row(Row::stretch())
1346 .add_column(Column::stretch())
1347 .add_column(Column::auto())
1348 .add_column(Column::auto())
1349 .add_column(Column::auto())
1350 .add_column(Column::stretch())
1351 .build(ctx);
1352
1353 let splitter = BorderBuilder::new(
1354 WidgetBuilder::new()
1355 .with_visibility(matches!(
1356 self.content,
1357 TileContent::VerticalTiles { .. } | TileContent::HorizontalTiles { .. }
1358 ))
1359 .with_cursor(match self.content {
1360 TileContent::HorizontalTiles { .. } => Some(CursorIcon::WResize),
1361 TileContent::VerticalTiles { .. } => Some(CursorIcon::NResize),
1362 _ => None,
1363 }),
1364 )
1365 .with_stroke_thickness(Thickness::uniform(0.0).into())
1366 .build(ctx);
1367
1368 let mut tabs = TabControlBuilder::new(
1369 WidgetBuilder::new().with_background(Brush::Solid(Color::BLACK).into()),
1370 )
1371 .with_tab_drag(true);
1372
1373 match self.content {
1374 TileContent::Window(window) => {
1375 if let Some(window) = ctx[window].cast_mut::<Window>() {
1376 window.can_resize = false;
1379
1380 window.width.set_value_and_mark_modified(f32::NAN);
1383 window.height.set_value_and_mark_modified(f32::NAN);
1384 }
1385 }
1386 TileContent::MultiWindow { ref windows, index } => {
1387 for (i, &window) in windows.iter().enumerate() {
1388 let window = ctx[window].cast_mut::<Window>().expect("must be window");
1389 window.can_resize = false;
1390 window.width.set_value_and_mark_modified(f32::NAN);
1391 window.height.set_value_and_mark_modified(f32::NAN);
1392 window.set_visibility(index as usize == i);
1393 let id = window.id;
1394 let header = create_tab_header(window.tab_label().to_owned(), ctx);
1395 let definition = TabDefinition {
1396 can_be_closed: false,
1397 content: Handle::NONE,
1398 user_data: None,
1399 header,
1400 };
1401 tabs = tabs.with_tab_uuid(id, definition);
1402 }
1403 tabs = tabs.with_initial_tab(index as usize);
1404 }
1405 _ => (),
1406 }
1407
1408 let tabs = tabs.build(ctx);
1409
1410 let children = match &self.content {
1411 TileContent::Window(window) => vec![*window],
1412 TileContent::MultiWindow { windows, .. } => windows.clone(),
1413 TileContent::VerticalTiles { tiles, .. } => vec![tiles[0], tiles[1]],
1414 TileContent::HorizontalTiles { tiles, .. } => vec![tiles[0], tiles[1]],
1415 TileContent::Empty => vec![],
1416 };
1417
1418 let tile = Tile {
1419 widget: self
1420 .widget_builder
1421 .with_preview_messages(true)
1422 .with_child(grid)
1423 .with_child(splitter)
1424 .with_child(tabs)
1425 .with_children(children)
1426 .build(ctx),
1427 tabs,
1428 left_anchor,
1429 right_anchor,
1430 top_anchor,
1431 bottom_anchor,
1432 center_anchor: dock_anchor,
1433 content: self.content,
1434 splitter,
1435 dragging_splitter: false,
1436 drop_anchor: Default::default(),
1437 };
1438
1439 ctx.add_node(UiNode::new(tile))
1440 }
1441}
1442
1443#[cfg(test)]
1444mod test {
1445 use crate::dock::TileBuilder;
1446 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1447
1448 #[test]
1449 fn test_deletion() {
1450 test_widget_deletion(|ctx| TileBuilder::new(WidgetBuilder::new()).build(ctx));
1451 }
1452}