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