Skip to main content

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