fyrox_ui/dock/tile.rs
1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::{
22 border::BorderBuilder,
23 brush::Brush,
24 core::{algebra::Vector2, color::Color, math::Rect, pool::Handle},
25 core::{reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*},
26 define_constructor,
27 dock::DockingManager,
28 grid::{Column, GridBuilder, Row},
29 message::{CursorIcon, MessageDirection, UiMessage},
30 widget::{Widget, WidgetBuilder, WidgetMessage},
31 window::{Window, WindowMessage},
32 BuildContext, Control, Thickness, UiNode, UserInterface,
33};
34use fyrox_core::uuid_provider;
35use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
36use fyrox_graph::{BaseSceneGraph, SceneGraph};
37use std::{
38 cell::Cell,
39 ops::{Deref, DerefMut},
40};
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum TileMessage {
44 Content(TileContent),
45 /// Internal. Do not use.
46 Split {
47 window: Handle<UiNode>,
48 direction: SplitDirection,
49 first: bool,
50 },
51}
52
53impl TileMessage {
54 define_constructor!(TileMessage:Content => fn content(TileContent), layout: false);
55 define_constructor!(TileMessage:Split => fn split(window: Handle<UiNode>,
56 direction: SplitDirection,
57 first: bool), layout: false);
58}
59
60#[derive(Default, Debug, PartialEq, Clone, Visit, Reflect)]
61pub enum TileContent {
62 #[default]
63 Empty,
64 Window(Handle<UiNode>),
65 VerticalTiles {
66 splitter: f32,
67 /// Docking system requires tiles to be handles to Tile instances.
68 /// However any node handle is acceptable, but in this case docking
69 /// will most likely not work.
70 tiles: [Handle<UiNode>; 2],
71 },
72 HorizontalTiles {
73 splitter: f32,
74 /// Docking system requires tiles to be handles to Tile instances.
75 /// However any node handle is acceptable, but in this case docking
76 /// will most likely not work.
77 tiles: [Handle<UiNode>; 2],
78 },
79}
80
81impl TileContent {
82 pub fn is_empty(&self) -> bool {
83 matches!(self, TileContent::Empty)
84 }
85}
86
87fn send_visibility(ui: &UserInterface, destination: Handle<UiNode>, visible: bool) {
88 ui.send_message(WidgetMessage::visibility(
89 destination,
90 MessageDirection::ToWidget,
91 visible,
92 ));
93}
94
95fn send_size(ui: &UserInterface, destination: Handle<UiNode>, width: f32, height: f32) {
96 ui.send_message(WidgetMessage::width(
97 destination,
98 MessageDirection::ToWidget,
99 width,
100 ));
101 ui.send_message(WidgetMessage::height(
102 destination,
103 MessageDirection::ToWidget,
104 height,
105 ));
106}
107
108fn send_background(ui: &UserInterface, destination: Handle<UiNode>, color: Color) {
109 ui.send_message(WidgetMessage::background(
110 destination,
111 MessageDirection::ToWidget,
112 Brush::Solid(color).into(),
113 ));
114}
115
116#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
117pub struct Tile {
118 pub widget: Widget,
119 pub left_anchor: Handle<UiNode>,
120 pub right_anchor: Handle<UiNode>,
121 pub top_anchor: Handle<UiNode>,
122 pub bottom_anchor: Handle<UiNode>,
123 pub center_anchor: Handle<UiNode>,
124 pub content: TileContent,
125 pub splitter: Handle<UiNode>,
126 pub dragging_splitter: bool,
127 pub drop_anchor: Cell<Handle<UiNode>>,
128}
129
130impl ConstructorProvider<UiNode, UserInterface> for Tile {
131 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
132 GraphNodeConstructor::new::<Self>()
133 .with_variant("Tile", |ui| {
134 TileBuilder::new(WidgetBuilder::new().with_name("Tile"))
135 .build(&mut ui.build_ctx())
136 .into()
137 })
138 .with_group("Layout")
139 }
140}
141
142crate::define_widget_deref!(Tile);
143
144uuid_provider!(Tile = "8ed17fa9-890e-4dd7-b4f9-a24660882234");
145
146impl Control for Tile {
147 fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
148 for &child_handle in self.children() {
149 // Determine available size for each child by its kind:
150 // - Every child not in content of tile just takes whole available size.
151 // - Every content's child uses specific available measure size.
152 // This is a bit weird, but it is how it works.
153 let available_size = match self.content {
154 TileContent::VerticalTiles {
155 splitter,
156 ref tiles,
157 } => {
158 if tiles[0] == child_handle {
159 Vector2::new(available_size.x, available_size.y * splitter)
160 } else if tiles[1] == child_handle {
161 Vector2::new(available_size.x, available_size.y * (1.0 - splitter))
162 } else {
163 available_size
164 }
165 }
166 TileContent::HorizontalTiles {
167 splitter,
168 ref tiles,
169 } => {
170 if tiles[0] == child_handle {
171 Vector2::new(available_size.x * splitter, available_size.y)
172 } else if tiles[1] == child_handle {
173 Vector2::new(available_size.x * (1.0 - splitter), available_size.y)
174 } else {
175 available_size
176 }
177 }
178 _ => available_size,
179 };
180
181 ui.measure_node(child_handle, available_size);
182 }
183
184 available_size
185 }
186
187 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
188 let splitter_size = ui.node(self.splitter).desired_size();
189
190 for &child_handle in self.children() {
191 let full_bounds = Rect::new(0.0, 0.0, final_size.x, final_size.y);
192
193 let bounds = match self.content {
194 TileContent::VerticalTiles {
195 splitter,
196 ref tiles,
197 } => {
198 if tiles[0] == child_handle {
199 Rect::new(
200 0.0,
201 0.0,
202 final_size.x,
203 final_size.y * splitter - splitter_size.y * 0.5,
204 )
205 } else if tiles[1] == child_handle {
206 Rect::new(
207 0.0,
208 final_size.y * splitter + splitter_size.y * 0.5,
209 final_size.x,
210 final_size.y * (1.0 - splitter) - splitter_size.y * 0.5,
211 )
212 } else if self.splitter == child_handle {
213 Rect::new(
214 0.0,
215 final_size.y * splitter - splitter_size.y * 0.5,
216 final_size.x,
217 splitter_size.y,
218 )
219 } else {
220 full_bounds
221 }
222 }
223 TileContent::HorizontalTiles {
224 splitter,
225 ref tiles,
226 } => {
227 if tiles[0] == child_handle {
228 Rect::new(
229 0.0,
230 0.0,
231 final_size.x * splitter - splitter_size.x * 0.5,
232 final_size.y,
233 )
234 } else if tiles[1] == child_handle {
235 Rect::new(
236 final_size.x * splitter + splitter_size.x * 0.5,
237 0.0,
238 final_size.x * (1.0 - splitter) - splitter_size.x * 0.5,
239 final_size.y,
240 )
241 } else if self.splitter == child_handle {
242 Rect::new(
243 final_size.x * splitter - splitter_size.x * 0.5,
244 0.0,
245 splitter_size.x,
246 final_size.y,
247 )
248 } else {
249 full_bounds
250 }
251 }
252 _ => full_bounds,
253 };
254
255 ui.arrange_node(child_handle, &bounds);
256 }
257
258 final_size
259 }
260
261 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
262 self.widget.handle_routed_message(ui, message);
263
264 if let Some(msg) = message.data::<TileMessage>() {
265 if message.destination() == self.handle() {
266 match msg {
267 TileMessage::Content(content) => {
268 self.content = content.clone();
269
270 match content {
271 TileContent::Empty => {
272 send_visibility(ui, self.splitter, false);
273 }
274 &TileContent::Window(window) => {
275 ui.send_message(WidgetMessage::link(
276 window,
277 MessageDirection::ToWidget,
278 self.handle(),
279 ));
280
281 send_visibility(ui, self.splitter, false);
282
283 ui.send_message(WindowMessage::can_resize(
284 window,
285 MessageDirection::ToWidget,
286 false,
287 ));
288
289 // Make the window size undefined, so it will be stretched to the tile
290 // size correctly.
291 send_size(ui, window, f32::NAN, f32::NAN);
292 }
293 TileContent::VerticalTiles { tiles, .. }
294 | TileContent::HorizontalTiles { tiles, .. } => {
295 for &tile in tiles {
296 ui.send_message(WidgetMessage::link(
297 tile,
298 MessageDirection::ToWidget,
299 self.handle(),
300 ));
301 }
302
303 send_visibility(ui, self.splitter, true);
304 match content {
305 TileContent::HorizontalTiles { .. } => {
306 send_size(
307 ui,
308 self.splitter,
309 DEFAULT_SPLITTER_SIZE,
310 f32::INFINITY,
311 );
312 ui.send_message(WidgetMessage::cursor(
313 self.splitter,
314 MessageDirection::ToWidget,
315 Some(CursorIcon::WResize),
316 ));
317 }
318 TileContent::VerticalTiles { .. } => {
319 send_size(
320 ui,
321 self.splitter,
322 f32::INFINITY,
323 DEFAULT_SPLITTER_SIZE,
324 );
325 ui.send_message(WidgetMessage::cursor(
326 self.splitter,
327 MessageDirection::ToWidget,
328 Some(CursorIcon::NResize),
329 ));
330 }
331 _ => (),
332 }
333 }
334 }
335 }
336 &TileMessage::Split {
337 window,
338 direction,
339 first,
340 } => {
341 if matches!(self.content, TileContent::Window(_)) {
342 self.split(ui, window, direction, first);
343 }
344 }
345 }
346 }
347 } else if let Some(msg) = message.data::<WidgetMessage>() {
348 match msg {
349 &WidgetMessage::MouseDown { .. } => {
350 if !message.handled() && message.destination() == self.splitter {
351 message.set_handled(true);
352 self.dragging_splitter = true;
353 ui.capture_mouse(self.splitter);
354 }
355 }
356 &WidgetMessage::MouseUp { .. } => {
357 if !message.handled() && message.destination() == self.splitter {
358 message.set_handled(true);
359 self.dragging_splitter = false;
360 ui.release_mouse_capture();
361 }
362 }
363 &WidgetMessage::MouseMove { pos, .. } => {
364 if self.dragging_splitter {
365 let bounds = self.screen_bounds();
366 match self.content {
367 TileContent::VerticalTiles {
368 ref mut splitter, ..
369 } => {
370 *splitter = ((pos.y - bounds.y()) / bounds.h()).clamp(0.0, 1.0);
371 self.invalidate_layout();
372 }
373 TileContent::HorizontalTiles {
374 ref mut splitter, ..
375 } => {
376 *splitter = ((pos.x - bounds.x()) / bounds.w()).clamp(0.0, 1.0);
377 self.invalidate_layout();
378 }
379 _ => (),
380 }
381 }
382 }
383 WidgetMessage::Unlink => {
384 // Check if this tile can be removed: only if it is split and sub-tiles are empty.
385 match self.content {
386 TileContent::VerticalTiles { tiles, .. }
387 | TileContent::HorizontalTiles { tiles, .. } => {
388 let mut has_empty_sub_tile = false;
389 for &tile in &tiles {
390 if let Some(sub_tile) = ui.node(tile).cast::<Tile>() {
391 if let TileContent::Empty = sub_tile.content {
392 has_empty_sub_tile = true;
393 break;
394 }
395 }
396 }
397 if has_empty_sub_tile {
398 for &tile in &tiles {
399 if let Some(sub_tile) = ui.node(tile).cast::<Tile>() {
400 match sub_tile.content {
401 TileContent::Window(sub_tile_wnd) => {
402 // If we have only a tile with a window, then detach window and schedule
403 // linking with current tile.
404 ui.send_message(WidgetMessage::unlink(
405 sub_tile_wnd,
406 MessageDirection::ToWidget,
407 ));
408
409 ui.send_message(TileMessage::content(
410 self.handle,
411 MessageDirection::ToWidget,
412 TileContent::Window(sub_tile_wnd),
413 ));
414 // Splitter must be hidden.
415 send_visibility(ui, self.splitter, false);
416 }
417 // In case if we have a split tile (vertically or horizontally) left in current tile
418 // (which is split too) we must set content of current tile to content of sub tile.
419 TileContent::VerticalTiles {
420 splitter,
421 tiles: sub_tiles,
422 } => {
423 for &sub_tile in &sub_tiles {
424 ui.send_message(WidgetMessage::unlink(
425 sub_tile,
426 MessageDirection::ToWidget,
427 ));
428 }
429 // Transfer sub tiles to current tile.
430 ui.send_message(TileMessage::content(
431 self.handle,
432 MessageDirection::ToWidget,
433 TileContent::VerticalTiles {
434 splitter,
435 tiles: sub_tiles,
436 },
437 ));
438 }
439 TileContent::HorizontalTiles {
440 splitter,
441 tiles: sub_tiles,
442 } => {
443 for &sub_tile in &sub_tiles {
444 ui.send_message(WidgetMessage::unlink(
445 sub_tile,
446 MessageDirection::ToWidget,
447 ));
448 }
449 // Transfer sub tiles to current tile.
450 ui.send_message(TileMessage::content(
451 self.handle,
452 MessageDirection::ToWidget,
453 TileContent::HorizontalTiles {
454 splitter,
455 tiles: sub_tiles,
456 },
457 ));
458 }
459 _ => {}
460 }
461 }
462 }
463
464 // Destroy tiles.
465 for &tile in &tiles {
466 ui.send_message(WidgetMessage::remove(
467 tile,
468 MessageDirection::ToWidget,
469 ));
470 }
471 }
472 }
473 _ => (),
474 }
475 }
476 _ => {}
477 }
478 // We can catch any message from window while it docked.
479 } else if let Some(msg) = message.data::<WindowMessage>() {
480 match msg {
481 WindowMessage::Move(_) => {
482 // Check if we dragging child window.
483 let content_moved = match self.content {
484 TileContent::Window(window) => window == message.destination(),
485 _ => false,
486 };
487
488 if content_moved {
489 if let Some(window) = ui.node(message.destination()).cast::<Window>() {
490 if window.drag_delta.norm() > 20.0 {
491 ui.send_message(TileMessage::content(
492 self.handle,
493 MessageDirection::ToWidget,
494 TileContent::Empty,
495 ));
496
497 ui.send_message(WidgetMessage::unlink(
498 message.destination(),
499 MessageDirection::ToWidget,
500 ));
501
502 ui.send_message(WindowMessage::can_resize(
503 message.destination(),
504 MessageDirection::ToWidget,
505 true,
506 ));
507
508 send_size(
509 ui,
510 message.destination(),
511 self.actual_local_size().x,
512 self.actual_local_size().y,
513 );
514
515 if let Some((_, docking_manager)) =
516 ui.find_component_up::<DockingManager>(self.parent())
517 {
518 docking_manager
519 .floating_windows
520 .borrow_mut()
521 .push(message.destination());
522 }
523 }
524 }
525 }
526 }
527 WindowMessage::Close => match self.content {
528 TileContent::VerticalTiles { tiles, .. }
529 | TileContent::HorizontalTiles { tiles, .. } => {
530 let closed_window = message.destination();
531
532 fn try_get_tile_window(
533 tile: Handle<UiNode>,
534 ui: &UserInterface,
535 window: Handle<UiNode>,
536 ) -> Option<Handle<UiNode>> {
537 if let Some(tile_ref) = ui.node(tile).query_component::<Tile>() {
538 if let TileContent::Window(tile_window) = tile_ref.content {
539 if tile_window == window {
540 return Some(tile_window);
541 }
542 }
543 }
544 None
545 }
546
547 for (tile_a_index, tile_b_index) in [(0, 1), (1, 0)] {
548 let tile_a = tiles[tile_a_index];
549 let tile_b = tiles[tile_b_index];
550 if let Some(tile_window) =
551 try_get_tile_window(tile_a, ui, closed_window)
552 {
553 if let Some(tile_b_ref) = ui.node(tile_b).query_component::<Tile>()
554 {
555 ui.send_message(WidgetMessage::unlink(
556 tile_window,
557 MessageDirection::ToWidget,
558 ));
559
560 match tile_b_ref.content {
561 TileContent::Empty => {}
562 TileContent::Window(window) => {
563 ui.send_message(WidgetMessage::unlink(
564 window,
565 MessageDirection::ToWidget,
566 ));
567 }
568 TileContent::VerticalTiles {
569 tiles: sub_tiles, ..
570 }
571 | TileContent::HorizontalTiles {
572 tiles: sub_tiles, ..
573 } => {
574 for tile in sub_tiles {
575 ui.send_message(WidgetMessage::unlink(
576 tile,
577 MessageDirection::ToWidget,
578 ));
579 }
580 }
581 }
582
583 ui.send_message(TileMessage::content(
584 self.handle,
585 MessageDirection::ToWidget,
586 tile_b_ref.content.clone(),
587 ));
588
589 // Destroy tiles.
590 for &tile in &tiles {
591 ui.send_message(WidgetMessage::remove(
592 tile,
593 MessageDirection::ToWidget,
594 ));
595 }
596
597 if let Some((_, docking_manager)) =
598 ui.find_component_up::<DockingManager>(self.parent())
599 {
600 docking_manager
601 .floating_windows
602 .borrow_mut()
603 .push(closed_window);
604 }
605
606 break;
607 }
608 }
609 }
610 }
611 _ => {}
612 },
613 _ => (),
614 }
615 }
616 }
617
618 // We have to use preview_message for docking purposes because dragged window detached
619 // from docking manager and handle_routed_message won't receive any messages from window.
620 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
621 if let Some(msg) = message.data::<WindowMessage>() {
622 if let Some((_, docking_manager)) =
623 ui.find_component_up::<DockingManager>(self.parent())
624 {
625 // Make sure we are dragging one of floating windows of parent docking manager.
626 if message.direction() == MessageDirection::FromWidget
627 && docking_manager
628 .floating_windows
629 .borrow_mut()
630 .contains(&message.destination())
631 {
632 match msg {
633 &WindowMessage::Move(_) => {
634 if let TileContent::Empty | TileContent::Window(_) = self.content {
635 // Show anchors.
636 for &anchor in &self.anchors() {
637 send_visibility(ui, anchor, true);
638 }
639 }
640
641 // Window can be docked only if current tile is not split already.
642 if let TileContent::Empty | TileContent::Window(_) = self.content {
643 // When window is being dragged, we should check which tile can accept it.
644 let pos = ui.cursor_position;
645 for &anchor in &self.anchors() {
646 send_background(ui, anchor, DEFAULT_ANCHOR_COLOR);
647 }
648 if ui.node(self.left_anchor).screen_bounds().contains(pos) {
649 send_background(ui, self.left_anchor, Color::WHITE);
650 self.drop_anchor.set(self.left_anchor);
651 } else if ui.node(self.right_anchor).screen_bounds().contains(pos) {
652 send_background(ui, self.right_anchor, Color::WHITE);
653 self.drop_anchor.set(self.right_anchor);
654 } else if ui.node(self.top_anchor).screen_bounds().contains(pos) {
655 send_background(ui, self.top_anchor, Color::WHITE);
656 self.drop_anchor.set(self.top_anchor);
657 } else if ui.node(self.bottom_anchor).screen_bounds().contains(pos)
658 {
659 send_background(ui, self.bottom_anchor, Color::WHITE);
660 self.drop_anchor.set(self.bottom_anchor);
661 } else if ui.node(self.center_anchor).screen_bounds().contains(pos)
662 {
663 send_background(ui, self.center_anchor, Color::WHITE);
664 self.drop_anchor.set(self.center_anchor);
665 } else {
666 self.drop_anchor.set(Handle::NONE);
667 }
668 }
669 }
670 WindowMessage::MoveEnd => {
671 // Hide anchors.
672 for &anchor in &self.anchors() {
673 send_visibility(ui, anchor, false);
674 }
675
676 // Drop if has any drop anchor.
677 if self.drop_anchor.get().is_some() {
678 match self.content {
679 TileContent::Empty => {
680 if self.drop_anchor.get() == self.center_anchor {
681 ui.send_message(TileMessage::content(
682 self.handle,
683 MessageDirection::ToWidget,
684 TileContent::Window(message.destination()),
685 ));
686 ui.send_message(WidgetMessage::link(
687 message.destination(),
688 MessageDirection::ToWidget,
689 self.handle,
690 ));
691 }
692 }
693 TileContent::Window(_) => {
694 if self.drop_anchor.get() == self.left_anchor {
695 // Split horizontally, dock to left.
696 ui.send_message(TileMessage::split(
697 self.handle,
698 MessageDirection::ToWidget,
699 message.destination(),
700 SplitDirection::Horizontal,
701 true,
702 ));
703 } else if self.drop_anchor.get() == self.right_anchor {
704 // Split horizontally, dock to right.
705 ui.send_message(TileMessage::split(
706 self.handle,
707 MessageDirection::ToWidget,
708 message.destination(),
709 SplitDirection::Horizontal,
710 false,
711 ));
712 } else if self.drop_anchor.get() == self.top_anchor {
713 // Split vertically, dock to top.
714 ui.send_message(TileMessage::split(
715 self.handle,
716 MessageDirection::ToWidget,
717 message.destination(),
718 SplitDirection::Vertical,
719 true,
720 ));
721 } else if self.drop_anchor.get() == self.bottom_anchor {
722 // Split vertically, dock to bottom.
723 ui.send_message(TileMessage::split(
724 self.handle,
725 MessageDirection::ToWidget,
726 message.destination(),
727 SplitDirection::Vertical,
728 false,
729 ));
730 }
731 }
732 // Rest cannot accept windows.
733 _ => (),
734 }
735 }
736 }
737 _ => (),
738 }
739 }
740 }
741 }
742 }
743}
744
745#[derive(Debug, Clone, Copy, Eq, PartialEq)]
746pub enum SplitDirection {
747 Horizontal,
748 Vertical,
749}
750
751impl Tile {
752 pub fn anchors(&self) -> [Handle<UiNode>; 5] {
753 [
754 self.left_anchor,
755 self.right_anchor,
756 self.top_anchor,
757 self.bottom_anchor,
758 self.center_anchor,
759 ]
760 }
761
762 fn split(
763 &mut self,
764 ui: &mut UserInterface,
765 window: Handle<UiNode>,
766 direction: SplitDirection,
767 first: bool,
768 ) {
769 let existing_content = match self.content {
770 TileContent::Window(existing_window) => existing_window,
771 _ => Handle::NONE,
772 };
773
774 let first_tile = TileBuilder::new(WidgetBuilder::new())
775 .with_content({
776 if first {
777 TileContent::Window(window)
778 } else {
779 TileContent::Empty
780 }
781 })
782 .build(&mut ui.build_ctx());
783
784 let second_tile = TileBuilder::new(WidgetBuilder::new())
785 .with_content({
786 if first {
787 TileContent::Empty
788 } else {
789 TileContent::Window(window)
790 }
791 })
792 .build(&mut ui.build_ctx());
793
794 if existing_content.is_some() {
795 ui.send_message(TileMessage::content(
796 if first { second_tile } else { first_tile },
797 MessageDirection::ToWidget,
798 TileContent::Window(existing_content),
799 ));
800 }
801
802 ui.send_message(TileMessage::content(
803 self.handle,
804 MessageDirection::ToWidget,
805 match direction {
806 SplitDirection::Horizontal => TileContent::HorizontalTiles {
807 tiles: [first_tile, second_tile],
808 splitter: 0.5,
809 },
810 SplitDirection::Vertical => TileContent::VerticalTiles {
811 tiles: [first_tile, second_tile],
812 splitter: 0.5,
813 },
814 },
815 ));
816 }
817}
818
819pub struct TileBuilder {
820 widget_builder: WidgetBuilder,
821 content: TileContent,
822}
823
824pub const DEFAULT_SPLITTER_SIZE: f32 = 5.0;
825pub const DEFAULT_ANCHOR_COLOR: Color = Color::opaque(150, 150, 150);
826
827pub fn make_default_anchor(ctx: &mut BuildContext, row: usize, column: usize) -> Handle<UiNode> {
828 let default_anchor_size = 30.0;
829 BorderBuilder::new(
830 WidgetBuilder::new()
831 .with_width(default_anchor_size)
832 .with_height(default_anchor_size)
833 .with_visibility(false)
834 .on_row(row)
835 .on_column(column)
836 .with_draw_on_top(true)
837 .with_background(Brush::Solid(DEFAULT_ANCHOR_COLOR).into()),
838 )
839 .build(ctx)
840}
841
842impl TileBuilder {
843 pub fn new(widget_builder: WidgetBuilder) -> Self {
844 Self {
845 widget_builder,
846 content: TileContent::Empty,
847 }
848 }
849
850 pub fn with_content(mut self, content: TileContent) -> Self {
851 self.content = content;
852 self
853 }
854
855 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
856 let left_anchor = make_default_anchor(ctx, 2, 1);
857 let right_anchor = make_default_anchor(ctx, 2, 3);
858 let dock_anchor = make_default_anchor(ctx, 2, 2);
859 let top_anchor = make_default_anchor(ctx, 1, 2);
860 let bottom_anchor = make_default_anchor(ctx, 3, 2);
861
862 let grid = GridBuilder::new(
863 WidgetBuilder::new()
864 .with_child(left_anchor)
865 .with_child(dock_anchor)
866 .with_child(right_anchor)
867 .with_child(top_anchor)
868 .with_child(bottom_anchor),
869 )
870 .add_row(Row::stretch())
871 .add_row(Row::auto())
872 .add_row(Row::auto())
873 .add_row(Row::auto())
874 .add_row(Row::stretch())
875 .add_column(Column::stretch())
876 .add_column(Column::auto())
877 .add_column(Column::auto())
878 .add_column(Column::auto())
879 .add_column(Column::stretch())
880 .build(ctx);
881
882 let splitter = BorderBuilder::new(
883 WidgetBuilder::new()
884 .with_width({
885 if let TileContent::HorizontalTiles { .. } = self.content {
886 DEFAULT_SPLITTER_SIZE
887 } else {
888 f32::INFINITY
889 }
890 })
891 .with_height({
892 if let TileContent::VerticalTiles { .. } = self.content {
893 DEFAULT_SPLITTER_SIZE
894 } else {
895 f32::INFINITY
896 }
897 })
898 .with_visibility(matches!(
899 self.content,
900 TileContent::VerticalTiles { .. } | TileContent::HorizontalTiles { .. }
901 ))
902 .with_cursor(match self.content {
903 TileContent::HorizontalTiles { .. } => Some(CursorIcon::WResize),
904 TileContent::VerticalTiles { .. } => Some(CursorIcon::NResize),
905 _ => None,
906 }),
907 )
908 .with_stroke_thickness(Thickness::uniform(0.0).into())
909 .build(ctx);
910
911 if let TileContent::Window(window) = self.content {
912 if let Some(window) = ctx[window].cast_mut::<Window>() {
913 // Every docked window must be non-resizable (it means that it cannot be resized by user
914 // and it still can be resized by a proper message).
915 window.can_resize = false;
916
917 // Make the window size undefined, so it will be stretched to the tile
918 // size correctly.
919 window.width.set_value_and_mark_modified(f32::NAN);
920 window.height.set_value_and_mark_modified(f32::NAN);
921 }
922 }
923
924 let children = match self.content {
925 TileContent::Window(window) => vec![window],
926 TileContent::VerticalTiles { tiles, .. } => vec![tiles[0], tiles[1]],
927 TileContent::HorizontalTiles { tiles, .. } => vec![tiles[0], tiles[1]],
928 _ => vec![],
929 };
930
931 let tile = Tile {
932 widget: self
933 .widget_builder
934 .with_preview_messages(true)
935 .with_child(grid)
936 .with_child(splitter)
937 .with_children(children)
938 .build(ctx),
939 left_anchor,
940 right_anchor,
941 top_anchor,
942 bottom_anchor,
943 center_anchor: dock_anchor,
944 content: self.content,
945 splitter,
946 dragging_splitter: false,
947 drop_anchor: Default::default(),
948 };
949
950 ctx.add_node(UiNode::new(tile))
951 }
952}
953
954#[cfg(test)]
955mod test {
956 use crate::dock::TileBuilder;
957 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
958
959 #[test]
960 fn test_deletion() {
961 test_widget_deletion(|ctx| TileBuilder::new(WidgetBuilder::new()).build(ctx));
962 }
963}