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}