Skip to main content

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