fyrox_ui/dock/
mod.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
21//! Docking manager allows you to dock windows and hold them in-place.
22//!
23//! # Notes
24//!
25//! Docking manager can hold any types of UI elements, but dragging works only
26//! for windows.
27
28use crate::{
29    core::{
30        log::Log, pool::Handle, reflect::prelude::*, type_traits::prelude::*, uuid_provider,
31        visitor::prelude::*,
32    },
33    define_constructor,
34    dock::config::{DockingManagerLayoutDescriptor, FloatingWindowDescriptor, TileDescriptor},
35    message::{MessageDirection, UiMessage},
36    widget::{Widget, WidgetBuilder, WidgetMessage},
37    window::WindowMessage,
38    BuildContext, Control, UiNode, UserInterface,
39};
40
41use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
42use fyrox_graph::{BaseSceneGraph, SceneGraph};
43use std::{
44    cell::RefCell,
45    ops::{Deref, DerefMut},
46};
47
48pub mod config;
49mod tile;
50
51pub use tile::*;
52
53/// Supported docking manager-specific messages.
54#[derive(Debug, Clone, PartialEq)]
55pub enum DockingManagerMessage {
56    Layout(DockingManagerLayoutDescriptor),
57    AddFloatingWindow(Handle<UiNode>),
58    RemoveFloatingWindow(Handle<UiNode>),
59}
60
61impl DockingManagerMessage {
62    define_constructor!(
63        /// Creates a new [Self::Layout] message.
64        DockingManagerMessage:Layout => fn layout(DockingManagerLayoutDescriptor), layout: false
65    );
66    define_constructor!(
67        /// Creates a new [Self::AddFloatingWindow] message.
68        DockingManagerMessage:AddFloatingWindow => fn add_floating_window(Handle<UiNode>), layout: false
69    );
70    define_constructor!(
71        /// Creates a new [Self::RemoveFloatingWindow] message.
72        DockingManagerMessage:RemoveFloatingWindow => fn remove_floating_window(Handle<UiNode>), layout: false
73    );
74}
75
76/// Docking manager is a special container widget, that holds a bunch of children widgets in-place
77/// using [`Tile`]s and a bunch of floating windows. Any window can be undocked and become a floating
78/// window and vice versa. Docking manager is typically used to "pack" multiple windows into a
79/// rectangular. The most notable use case is IDEs where you can drag,
80/// dock, undock, stack windows.
81///
82/// ## Tiles
83///
84/// The main element of the docking manager is the [`Tile`] widget, which can be in two major states:
85///
86/// 1) It can hold a window
87/// 2) It can be split into two more sub-tiles (either vertically or horizontally), which can in
88/// their turn either contain some other window or a sub-tile.
89///
90/// This structure essentially forms a tree of pretty much unlimited depth. This approach basically
91/// allows you to "pack" multiple windows in a rectangular area with no free space between the tiles.
92/// Split tiles have a special parameter called splitter, which is simply a fraction that shows how
93/// much space each half takes. In the case of a horizontal tile, if the splitter is 0.25, then the left
94/// tile will take 25% of the width of the tile and the right tile will take the rest 75% of the
95/// width.
96///
97/// ## Floating Windows
98///
99/// The docking manager can control an unlimited number of floating windows, floating windows can be
100/// docked and vice versa. When a window is undocked, it is automatically placed into a list of floating
101/// windows. Only the windows from this list can be docked.
102///
103/// ## Example
104///
105/// The following example shows how to create a docking manager with one root tile split vertically
106/// into two smaller tiles where each tile holds a separate window.
107///
108/// ```rust
109/// # use fyrox_ui::{
110/// #     core::pool::Handle,
111/// #     dock::{DockingManagerBuilder, TileBuilder, TileContent},
112/// #     widget::WidgetBuilder,
113/// #     window::{WindowBuilder, WindowTitle},
114/// #     BuildContext, UiNode,
115/// # };
116/// #
117/// fn create_docking_manager(ctx: &mut BuildContext) -> Handle<UiNode> {
118///     let top_window = WindowBuilder::new(WidgetBuilder::new())
119///         .with_title(WindowTitle::text("Top Window"))
120///         .build(ctx);
121///
122///     let bottom_window = WindowBuilder::new(WidgetBuilder::new())
123///         .with_title(WindowTitle::text("Bottom Window"))
124///         .build(ctx);
125///
126///     let root_tile = TileBuilder::new(WidgetBuilder::new())
127///         .with_content(TileContent::VerticalTiles {
128///             splitter: 0.5,
129///             tiles: [
130///                 TileBuilder::new(WidgetBuilder::new())
131///                     // Note that you have to put the window into a separate tile, otherwise
132///                     // you'll get unexpected results.
133///                     .with_content(TileContent::Window(top_window))
134///                     .build(ctx),
135///                 TileBuilder::new(WidgetBuilder::new())
136///                     .with_content(TileContent::Window(bottom_window))
137///                     .build(ctx),
138///             ],
139///         })
140///         .build(ctx);
141///
142///     DockingManagerBuilder::new(
143///         WidgetBuilder::new()
144///             .with_child(root_tile)
145///             .with_width(500.0)
146///             .with_height(500.0),
147///     )
148///     .build(ctx)
149/// }
150/// ```
151///
152/// ## Layout
153///
154/// The current docking manager layout can be saved and restored later if needed. This is a very useful
155/// option for customizable user interfaces, where users can adjust the interface as they like,
156/// save it and then load on the next session. Use the following code to save the layout:
157///
158/// ```rust
159/// # use fyrox_ui::{
160/// #     dock::config::DockingManagerLayoutDescriptor, dock::DockingManager, UiNode, UserInterface,
161/// # };
162/// # use fyrox_core::pool::Handle;
163/// # use fyrox_graph::SceneGraph;
164/// #
165/// fn save_layout(
166///     ui: &UserInterface,
167///     docking_manager_handle: Handle<UiNode>,
168/// ) -> Option<DockingManagerLayoutDescriptor> {
169///     ui.try_get_of_type::<DockingManager>(docking_manager_handle)
170///         .as_ref()
171///         .map(|docking_manager| docking_manager.layout(ui))
172/// }
173/// ```
174///
175/// The layout can be restored by sending a [`DockingManagerMessage::Layout`] message to the docking
176/// manager. Use [`DockingManagerMessage::layout`] builder method to make one.
177///
178/// To be able to restore the layout to its defaults, just create a desired layout from code,
179/// save the layout and use the returned layout descriptor when you need to restore the layout
180/// to its defaults.
181#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
182#[reflect(derived_type = "UiNode")]
183pub struct DockingManager {
184    pub widget: Widget,
185    pub floating_windows: RefCell<Vec<Handle<UiNode>>>,
186}
187
188impl ConstructorProvider<UiNode, UserInterface> for DockingManager {
189    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
190        GraphNodeConstructor::new::<Self>()
191            .with_variant("Docking Manager", |ui| {
192                DockingManagerBuilder::new(WidgetBuilder::new().with_name("Docking Manager"))
193                    .build(&mut ui.build_ctx())
194                    .into()
195            })
196            .with_group("Layout")
197    }
198}
199
200crate::define_widget_deref!(DockingManager);
201
202uuid_provider!(DockingManager = "b04299f7-3f6b-45f1-89a6-0dce4ad929e1");
203
204impl Control for DockingManager {
205    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
206        self.widget.handle_routed_message(ui, message);
207
208        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
209        {
210            if let Some(msg) = message.data() {
211                match msg {
212                    DockingManagerMessage::Layout(layout_descriptor) => {
213                        self.set_layout(layout_descriptor, ui);
214                    }
215                    DockingManagerMessage::AddFloatingWindow(window) => {
216                        self.add_floating_window(*window)
217                    }
218                    DockingManagerMessage::RemoveFloatingWindow(window) => {
219                        self.remove_floating_window(*window)
220                    }
221                }
222            }
223        }
224    }
225
226    fn preview_message(&self, _ui: &UserInterface, message: &mut UiMessage) {
227        if let Some(WidgetMessage::LinkWith(_)) = message.data::<WidgetMessage>() {
228            let pos = self
229                .floating_windows
230                .borrow()
231                .iter()
232                .position(|&i| i == message.destination());
233            if let Some(pos) = pos {
234                self.floating_windows.borrow_mut().remove(pos);
235            }
236        }
237    }
238}
239
240impl DockingManager {
241    pub fn layout(&self, ui: &UserInterface) -> DockingManagerLayoutDescriptor {
242        DockingManagerLayoutDescriptor {
243            floating_windows: self
244                .floating_windows
245                .borrow()
246                .iter()
247                .filter_map(|h| {
248                    ui.try_get_node(*h).map(|w| FloatingWindowDescriptor {
249                        name: w.name.clone(),
250                        position: w.actual_local_position(),
251                        size: w.actual_local_size(),
252                        is_open: w.is_globally_visible(),
253                    })
254                })
255                .collect::<Vec<_>>(),
256            root_tile_descriptor: self
257                .children()
258                .first()
259                .map(|c| TileDescriptor::from_tile_handle(*c, ui)),
260        }
261    }
262
263    fn set_layout(
264        &mut self,
265        layout_descriptor: &DockingManagerLayoutDescriptor,
266        ui: &mut UserInterface,
267    ) {
268        if let Some(root_tile_handle) = self.children.first().cloned() {
269            let mut windows = Vec::new();
270            let mut stack = vec![root_tile_handle];
271            while let Some(tile_handle) = stack.pop() {
272                if let Some(tile) = ui
273                    .try_get_node(tile_handle)
274                    .and_then(|n| n.query_component::<Tile>())
275                {
276                    match tile.content {
277                        TileContent::Window(window) => {
278                            if ui.is_valid_handle(window) {
279                                // Detach the window from the tile, this is needed to prevent
280                                // deletion of the window when the tile is deleted.
281                                ui.unlink_node(window);
282
283                                windows.push(window);
284                            }
285                        }
286                        TileContent::MultiWindow {
287                            windows: ref tile_windows,
288                            ..
289                        } => {
290                            for w in tile_windows.clone() {
291                                ui.unlink_node(w);
292                                windows.push(w);
293                            }
294                        }
295                        TileContent::VerticalTiles { tiles, .. }
296                        | TileContent::HorizontalTiles { tiles, .. } => {
297                            stack.extend_from_slice(&tiles);
298                        }
299                        TileContent::Empty => (),
300                    }
301                }
302            }
303
304            // Destroy the root tile with all descendant tiles.
305            ui.send_message(WidgetMessage::remove(
306                root_tile_handle,
307                MessageDirection::ToWidget,
308            ));
309
310            // Re-create the tiles according to the layout and attach it to the docking manager.
311            if let Some(root_tile_descriptor) = layout_descriptor.root_tile_descriptor.as_ref() {
312                let root_tile = root_tile_descriptor.create_tile(ui, &windows);
313                ui.send_message(WidgetMessage::link(
314                    root_tile,
315                    MessageDirection::ToWidget,
316                    self.handle,
317                ));
318            }
319
320            // Restore floating windows.
321            self.floating_windows.borrow_mut().clear();
322            for floating_window_desc in layout_descriptor.floating_windows.iter() {
323                if floating_window_desc.name.is_empty() {
324                    Log::warn(
325                        "Floating window name is empty, wrong widget will be used as a \
326                        floating window. Assign a unique name to the floating window used in a docking \
327                        manager!",
328                    );
329                }
330
331                let floating_window =
332                    ui.find_handle(ui.root(), &mut |n| n.name == floating_window_desc.name);
333                if floating_window.is_some() {
334                    self.floating_windows.borrow_mut().push(floating_window);
335
336                    if floating_window_desc.is_open {
337                        ui.send_message(WindowMessage::open(
338                            floating_window,
339                            MessageDirection::ToWidget,
340                            false,
341                            false,
342                        ));
343                    } else {
344                        ui.send_message(WindowMessage::close(
345                            floating_window,
346                            MessageDirection::ToWidget,
347                        ));
348                    }
349
350                    ui.send_message(WidgetMessage::desired_position(
351                        floating_window,
352                        MessageDirection::ToWidget,
353                        floating_window_desc.position,
354                    ));
355
356                    if floating_window_desc.size.x != 0.0 {
357                        ui.send_message(WidgetMessage::width(
358                            floating_window,
359                            MessageDirection::ToWidget,
360                            floating_window_desc.size.x,
361                        ));
362                    }
363
364                    if floating_window_desc.size.y != 0.0 {
365                        ui.send_message(WidgetMessage::height(
366                            floating_window,
367                            MessageDirection::ToWidget,
368                            floating_window_desc.size.y,
369                        ));
370                    }
371                }
372            }
373        }
374    }
375
376    fn add_floating_window(&mut self, window: Handle<UiNode>) {
377        let mut windows = self.floating_windows.borrow_mut();
378        if !windows.contains(&window) {
379            windows.push(window);
380        }
381    }
382
383    fn remove_floating_window(&mut self, window: Handle<UiNode>) {
384        let mut windows = self.floating_windows.borrow_mut();
385        if let Some(position) = windows.iter().position(|&w| w == window) {
386            windows.remove(position);
387        }
388    }
389}
390
391pub struct DockingManagerBuilder {
392    widget_builder: WidgetBuilder,
393    floating_windows: Vec<Handle<UiNode>>,
394}
395
396impl DockingManagerBuilder {
397    pub fn new(widget_builder: WidgetBuilder) -> Self {
398        Self {
399            widget_builder,
400            floating_windows: Default::default(),
401        }
402    }
403
404    pub fn with_floating_windows(mut self, windows: Vec<Handle<UiNode>>) -> Self {
405        self.floating_windows = windows;
406        self
407    }
408
409    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
410        let docking_manager = DockingManager {
411            widget: self.widget_builder.with_preview_messages(true).build(ctx),
412            floating_windows: RefCell::new(self.floating_windows),
413        };
414
415        ctx.add_node(UiNode::new(docking_manager))
416    }
417}
418
419#[cfg(test)]
420mod test {
421    use crate::dock::DockingManagerBuilder;
422    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
423
424    #[test]
425    fn test_deletion() {
426        test_widget_deletion(|ctx| DockingManagerBuilder::new(WidgetBuilder::new()).build(ctx));
427    }
428}