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}