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}