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