diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0e921..8a8ef41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 0.7.0 - (to be determined) +This is the biggest update so far, introducing the long awaited undocking feature: tabs can now be dragged out into +new egui windows. Massive thanks to [Vickerinox](https://github.com/Vickerinox) for implementing it! + +This update also includes an overhaul of the documentation, aiming to not only be more readable and correct, but also +provide a guide of how to use the library. + ### Changed - Adjusted the styling of tabs to closer follow the egui default styling. ([#139](https://github.com/Adanos020/egui_dock/pull/139)) @@ -10,11 +16,12 @@ ### Fixed - Correctly draw a border around a dock area using the `Style::border` property. ([#139](https://github.com/Adanos020/egui_dock/pull/139)) +- Non-closable tabs now cannot be closed by clicking with the middle mouse button. ([9cdef8c](https://github.com/Adanos020/egui_dock/pull/149/commits/9cdef8cb77e73ef7a065d1313f7fb8feae0253b4)) ### Added - From [#139](https://github.com/Adanos020/egui_dock/pull/139): - - `Style::rounding` for the rounding of the dock area border. + - `Style::main_surface_border_rounding` for the rounding of the dock area border. - `TabStyle::active` for the active style of a tab. - `TabStyle::inactive` for the inactive style of a tab. - `TabStyle::focused` for the focused style of a tab. @@ -24,7 +31,26 @@ - `TabInteractionStyle` to style the active/inactive/focused/hovered states of a tab. - `AllowedSplits` enum which lets you choose in which directions a `DockArea` can be split. ([#145](https://github.com/Adanos020/egui_dock/pull/145)) - `TabViewer::closable` lets individual tabs be closable or not. ([#150](https://github.com/Adanos020/egui_dock/pull/150)) - +- From [#149](https://github.com/Adanos020/egui_dock/pull/149): + - `DockState` containing the entire state of the tab hierarchies stored in a collection of `Surfaces`. + - `Surface` enum which represents an area (e.g. a window) with its own `Tree`. + - `SurfaceIndex` to identify a `Surface` stored in the `DockState`. + - `Split::is_tob_bottom` and `Split::is_left_right`. + - `TabInsert` which replaces current `TabDestination` (see breaking changes). + - `impl From<(SurfaceIndex, NodeIndex, TabInsert)> for TabDestination`. + - `impl From for TabDestination`. + - `TabDestination::is_window` (see breaking changes). + - `Tree::root_node` and `Tree::root_node_mut`. + - `Node::rect` returning the `Rect` occupied by the node. + - `Node::tabs` and `Node::tabs_mut` returning an optional slice of tabs if the node is a leaf. + - `WindowState` representing the current state of a `Surface::Window` and allowing you to manipulate the window. + - `OverlayStyle` (stored as `Style::overlay`) and `OverlayFeel`: they specify the look and feel of the drag-and-drop overlay. + - `OverlayType` letting you choose if the overlay should be the new icon buttons or the old highlighted rectangles. + - `LeafHighlighting` specifying how a currently hovered leaf should be highlighted. + - `DockArea::window_bounds` setting the area which windows are constrained by. + - `DockArea::show_window_close_buttons` setting determining if windows should have a close button or not. + - `DockArea::show_window_collapse_buttons` setting determining if windows should have a collapse button or not. + - `TabViewer::allowed_in_windows` specifying if a given tab can be shown in a window. ### Breaking changes @@ -39,6 +65,13 @@ - Moved `TabStyle::text_color_active_unfocused` to `TabStyle::active.text_color`. - Renamed `Style::tabs` to `Style::tab`. - Removed `TabStyle::text_color_focused`. This style was practically never reachable. +- From [#149](https://github.com/Adanos020/egui_dock/pull/149): + - `TabDestination` now specifies if a tab will be moved to a `Window`, a `Node`, or an `EmptySurface`. Its original purpose is now served by `TabInsert`. + - `Tree::split` now panics if supplied `fraction` is not in range 0..=1. + - Moved `Tree::move_tab` to `DockState::move_tab`. + - Renamed `Style::border` to `Style::main_surface_border_stroke`. + - Moved `Style::selection_color` to `OverlayStyle::selection_color`. + - `DockArea::new` now takes in a `DockState` instead of a `Tree`. ## 0.6.3 - 2023-06-16 diff --git a/README.md b/README.md index f505f1b..fa13757 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,26 @@ [![docs.rs](https://img.shields.io/docsrs/egui_dock)](https://docs.rs/egui_dock/) Originally created by [@lain-dono](https://github.com/lain-dono), this library provides docking support for `egui`. -It lets you open and close tabs, freely move them around, insert them in selected parts of the `DockArea`, and resize them. -## How to contribute - -Feel free to open new issues and pull requests. +## Contributing Before contributing, please read [the contribution guide](CONTRIBUTING.md). +This library is a collaborative project developed with direct involvement of its users. + +Please feel free to open new issues and pull requests, and participate in discussions! +A lot of our discussions take place on [`egui`'s official Discord server](https://discord.gg/JFcEma9bJq), +in the `#egui_dock` channel. + +## Features + +- Opening and closing tabs. +- Moving tabs between nodes and resizing. +- Dragging tabs out into new `egui` windows. +- Highly customizable look and feel. +- High degree of control over behaviour of the whole dock area and of individual tabs. +- Manipulating tabs and dock layout from code. + ## Quick start Add `egui` and `egui_dock` to your project's dependencies. diff --git a/examples/hello.rs b/examples/hello.rs index e76bfc4..5260042 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -5,14 +5,52 @@ use std::collections::HashSet; use eframe::{egui, NativeOptions}; use egui::{ color_picker::{color_edit_button_srgba, Alpha}, - CentralPanel, ComboBox, Frame, Slider, TopBottomPanel, Ui, WidgetText, + CentralPanel, ComboBox, Frame, Rounding, Slider, TopBottomPanel, Ui, WidgetText, }; use egui_dock::{ - AllowedSplits, DockArea, Node, NodeIndex, Style, TabInteractionStyle, TabViewer, Tree, + AllowedSplits, DockArea, DockState, Node, NodeIndex, OverlayType, Style, SurfaceIndex, + TabInteractionStyle, TabViewer, }; +/// Adds a widget with a label next to it, can be given an extra parameter in order to show a hover text +macro_rules! labeled_widget { + ($ui:expr, $x:expr, $l:expr) => { + $ui.horizontal(|ui| { + ui.add($x); + ui.label($l); + }); + }; + ($ui:expr, $x:expr, $l:expr, $d:expr) => { + $ui.horizontal(|ui| { + ui.add($x).on_hover_text($d); + ui.label($l).on_hover_text($d); + }); + }; +} + +// Creates a slider which has a unit attached to it +// When given an extra parameter it will be used as a multiplier (e.g 100.0 when working with precentages) +macro_rules! unit_slider { + ($val:expr, $range:expr) => { + egui::Slider::new($val, $range) + }; + ($val:expr, $range:expr, $unit:expr) => { + egui::Slider::new($val, $range).custom_formatter(|value, decimal_range| { + egui::emath::format_with_decimals_in_range(value, decimal_range) + $unit + }) + }; + ($val:expr, $range:expr, $unit:expr, $mul:expr) => { + egui::Slider::new($val, $range) + .custom_formatter(|value, decimal_range| { + egui::emath::format_with_decimals_in_range(value * $mul, decimal_range) + $unit + }) + .custom_parser(|string| string.parse::().ok().map(|valid| valid / $mul)) + }; +} + fn main() -> eframe::Result<()> { + std::env::set_var("RUST_BACKTRACE", "1"); let options = NativeOptions { initial_window_size: Some(egui::vec2(1024.0, 1024.0)), ..Default::default() @@ -35,11 +73,13 @@ struct MyContext { draggable_tabs: bool, show_tab_name_on_hover: bool, allowed_splits: AllowedSplits, + show_window_close: bool, + show_window_collapse: bool, } struct MyApp { context: MyContext, - tree: Tree, + tree: DockState, } impl TabViewer for MyContext { @@ -109,6 +149,11 @@ impl MyContext { ui.checkbox(&mut self.show_add_buttons, "Show add buttons"); ui.checkbox(&mut self.draggable_tabs, "Draggable tabs"); ui.checkbox(&mut self.show_tab_name_on_hover, "Show tab name on hover"); + ui.checkbox(&mut self.show_window_close, "Show close button on windows"); + ui.checkbox( + &mut self.show_window_collapse, + "Show collaspse button on windows", + ); ComboBox::new("cbox:allowed_splits", "Split direction(s)") .selected_text(format!("{:?}", self.allowed_splits)) .show_ui(ui, |ui| { @@ -132,19 +177,18 @@ impl MyContext { ui.collapsing("Border", |ui| { egui::Grid::new("border").show(ui, |ui| { ui.label("Width:"); - ui.add(Slider::new(&mut style.border.width, 1.0..=50.0)); - ui.end_row(); - - ui.label("Color:"); - color_edit_button_srgba(ui, &mut style.border.color, Alpha::OnlyBlend); + ui.add(Slider::new( + &mut style.main_surface_border_stroke.width, + 1.0..=50.0, + )); ui.end_row(); - }); - }); - ui.collapsing("Selection", |ui| { - egui::Grid::new("selection").show(ui, |ui| { ui.label("Color:"); - color_edit_button_srgba(ui, &mut style.selection_color, Alpha::OnlyBlend); + color_edit_button_srgba( + ui, + &mut style.main_surface_border_stroke.color, + Alpha::OnlyBlend, + ); ui.end_row(); }); }); @@ -215,22 +259,26 @@ impl MyContext { ui.separator(); ui.label("Rounding"); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut tab_style.rounding.nw, 0.0..=15.0)); - ui.label("North-West"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut tab_style.rounding.ne, 0.0..=15.0)); - ui.label("North-East"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut tab_style.rounding.sw, 0.0..=15.0)); - ui.label("South-West"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut tab_style.rounding.se, 0.0..=15.0)); - ui.label("South-East"); - }); + labeled_widget!( + ui, + Slider::new(&mut tab_style.rounding.nw, 0.0..=15.0), + "North-West" + ); + labeled_widget!( + ui, + Slider::new(&mut tab_style.rounding.ne, 0.0..=15.0), + "North-East" + ); + labeled_widget!( + ui, + Slider::new(&mut tab_style.rounding.sw, 0.0..=15.0), + "South-West" + ); + labeled_widget!( + ui, + Slider::new(&mut tab_style.rounding.se, 0.0..=15.0), + "South-East" + ); ui.separator(); @@ -301,22 +349,7 @@ impl MyContext { ui.separator(); ui.label("Rounding"); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tab.tab_body.rounding.nw, 0.0..=15.0)); - ui.label("North-West"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tab.tab_body.rounding.ne, 0.0..=15.0)); - ui.label("North-East"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tab.tab_body.rounding.sw, 0.0..=15.0)); - ui.label("South-West"); - }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tab.tab_body.rounding.se, 0.0..=15.0)); - ui.label("South-East"); - }); + rounding_ui(ui, &mut style.tab.tab_body.rounding); ui.label("Stroke width:"); ui.add(Slider::new( @@ -335,23 +368,152 @@ impl MyContext { ui.end_row(); }); }); + ui.collapsing("Overlay", |ui| { + let selected_text = match style.overlay.overlay_type { + OverlayType::HighlightedAreas => "Highlighted Areas", + OverlayType::Widgets => "Widgets", + }; + ui.label("Overlay Style:"); + ComboBox::new("overlay styles", "") + .selected_text(selected_text) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut style.overlay.overlay_type, + OverlayType::HighlightedAreas, + "Highlighted Areas", + ); + ui.selectable_value( + &mut style.overlay.overlay_type, + OverlayType::Widgets, + "Widgets", + ); + }); + ui.collapsing("Feel", |ui|{ + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.feel.center_drop_coverage, 0.0..=1.0, "%", 100.0), + "Center drop coverage", + "how big the area where dropping a tab into the center of another should be." + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.feel.fade_hold_time, 0.0..=4.0, "s"), + "Fade hold time", + "How long faded windows should hold their fade before unfading, in seconds." + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.feel.max_preference_time, 0.0..=4.0, "s"), + "Max preference time", + "How long the overlay may prefer to stick to a surface despite hovering over another, in seconds." + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.feel.window_drop_coverage, 0.0..=1.0, "%", 100.0), + "Window drop coverage", + "How big the area for undocking a window should be. [is overshadowed by center drop coverage]" + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.feel.interact_expansion, 1.0..=100.0, "ps"), + "Interact expansion", + "How much extra interaction area should be allocated for buttons on the overlay" + ); + }); + + ui.collapsing("Visuals", |ui|{ + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.max_button_size, 10.0..=500.0, "ps"), + "Max button size", + "The max length of a side on a overlay button in egui points" + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.button_spacing, 0.0..=50.0, "ps"), + "Button spacing", + "Spacing between buttons on the overlay, in egui units." + ); + labeled_widget!( + ui, + unit_slider!(&mut style.overlay.surface_fade_opacity, 0.0..=1.0, "%", 100.0), + "Window fade opacity", + "how visible windows are when dragging a tab behind them." + ); + labeled_widget!( + ui, + egui::Slider::new(&mut style.overlay.selection_storke_width, 0.0..=50.0), + "Selection stroke width", + "width of a selection which uses a outline stroke instead of filled rect." + ); + egui::Grid::new("overlay style preferences").show(ui, |ui| { + ui.label("Button color:"); + color_edit_button_srgba(ui, &mut style.overlay.button_color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Button border color:"); + color_edit_button_srgba(ui, &mut style.overlay.button_border_stroke.color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Selection color:"); + color_edit_button_srgba(ui, &mut style.overlay.selection_color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Button stroke color:"); + color_edit_button_srgba(ui, &mut style.overlay.button_border_stroke.color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Button stroke width:"); + ui.add(Slider::new(&mut style.overlay.button_border_stroke.width, 0.0..=50.0)); + ui.end_row(); + }); + }); + + ui.collapsing("Hover highlight", |ui|{ + egui::Grid::new("leaf highlighting prefs").show(ui, |ui|{ + ui.label("Fill color:"); + color_edit_button_srgba(ui, &mut style.overlay.hovered_leaf_highlight.color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Stroke color:"); + color_edit_button_srgba(ui, &mut style.overlay.hovered_leaf_highlight.stroke.color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Stroke width:"); + ui.add(Slider::new(&mut style.overlay.hovered_leaf_highlight.stroke.width, 0.0..=50.0)); + ui.end_row(); + + ui.label("Expansion:"); + ui.add(Slider::new(&mut style.overlay.hovered_leaf_highlight.expansion, -50.0..=50.0)); + ui.end_row(); + }); + ui.label("Rounding:"); + rounding_ui(ui, &mut style.overlay.hovered_leaf_highlight.rounding); + }) + }); } } impl Default for MyApp { fn default() -> Self { - let mut tree = Tree::new(vec!["Simple Demo".to_owned(), "Style Editor".to_owned()]); - let [a, b] = tree.split_left(NodeIndex::root(), 0.3, vec!["Inspector".to_owned()]); - let [_, _] = tree.split_below( + let mut tree = DockState::new(vec!["Simple Demo".to_owned(), "Style Editor".to_owned()]); + let [a, b] = tree.main_surface_mut().split_left( + NodeIndex::root(), + 0.3, + vec!["Inspector".to_owned()], + ); + let [_, _] = tree.main_surface_mut().split_below( a, 0.7, vec!["File Browser".to_owned(), "Asset Manager".to_owned()], ); - let [_, _] = tree.split_below(b, 0.5, vec!["Hierarchy".to_owned()]); + let [_, _] = tree + .main_surface_mut() + .split_below(b, 0.5, vec!["Hierarchy".to_owned()]); let mut open_tabs = HashSet::new(); - for node in tree.iter() { + for node in tree[SurfaceIndex::main()].iter() { if let Node::Leaf { tabs, .. } = node { for tab in tabs { open_tabs.insert(tab.clone()); @@ -364,6 +526,8 @@ impl Default for MyApp { style: None, open_tabs, + show_window_close: true, + show_window_collapse: true, show_close_buttons: true, show_add_buttons: false, draggable_tabs: true, @@ -390,7 +554,8 @@ impl eframe::App for MyApp { self.tree.remove_tab(index); self.context.open_tabs.remove(*tab); } else { - self.tree.push_to_focused_leaf(tab.to_string()); + self.tree[SurfaceIndex::main()] + .push_to_focused_leaf(tab.to_string()); } ui.close_menu(); @@ -399,7 +564,6 @@ impl eframe::App for MyApp { }); }) }); - CentralPanel::default() // When displaying a DockArea in another UI, it looks better // to set inner margins to 0. @@ -418,7 +582,16 @@ impl eframe::App for MyApp { .draggable_tabs(self.context.draggable_tabs) .show_tab_name_on_hover(self.context.show_tab_name_on_hover) .allowed_splits(self.context.allowed_splits) + .show_window_close_buttons(self.context.show_window_close) + .show_window_collapse_buttons(self.context.show_window_collapse) .show_inside(ui, &mut self.context); }); } } + +fn rounding_ui(ui: &mut Ui, rounding: &mut Rounding) { + labeled_widget!(ui, Slider::new(&mut rounding.nw, 0.0..=15.0), "North-West"); + labeled_widget!(ui, Slider::new(&mut rounding.ne, 0.0..=15.0), "North-East"); + labeled_widget!(ui, Slider::new(&mut rounding.sw, 0.0..=15.0), "South-West"); + labeled_widget!(ui, Slider::new(&mut rounding.se, 0.0..=15.0), "South-East"); +} diff --git a/examples/reject_windows.rs b/examples/reject_windows.rs new file mode 100644 index 0000000..fa6227d --- /dev/null +++ b/examples/reject_windows.rs @@ -0,0 +1,115 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::{egui, NativeOptions}; + +use egui_dock::{DockArea, DockState, NodeIndex, Style}; + +fn main() -> eframe::Result<()> { + let options = NativeOptions::default(); + eframe::run_native( + "My egui App", + options, + Box::new(|_cc| Box::::default()), + ) +} + +struct TabViewer; + +struct OpinionatedTab { + can_become_window: Result, + title: String, + content: String, +} + +impl egui_dock::TabViewer for TabViewer { + type Tab = OpinionatedTab; + + fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { + ui.label(&tab.content); + match &mut tab.can_become_window { + Ok(changing_opinion) => { + ui.add(egui::Checkbox::new( + changing_opinion, + "can be turned into window", + )); + } + Err(fixed_opinion) => { + if *fixed_opinion { + ui.small("this tab can exist in a window"); + } else { + ui.small("this tab cannot exist in a window"); + } + } + } + } + + fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { + (&tab.title).into() + } + + fn allowed_in_windows(&self, tab: &mut Self::Tab) -> bool { + match tab.can_become_window { + Ok(opinion) | Err(opinion) => opinion, + } + } +} + +struct MyApp { + tree: DockState, +} + +impl Default for MyApp { + fn default() -> Self { + let mut tree = DockState::new(vec![ + OpinionatedTab { + can_become_window: Ok(false), + title: "old tab".to_owned(), + content: "since when could tabs become windows?".to_string(), + }, + OpinionatedTab { + can_become_window: Err(false), + title: "grumpy tab".to_owned(), + content: "I don't want to be a window!".to_string(), + }, + ]); + + // You can modify the tree before constructing the dock + let [a, _] = tree.main_surface_mut().split_right( + NodeIndex::root(), + 0.6, + vec![OpinionatedTab { + can_become_window: Ok(true), + title: "wise tab".to_owned(), + content: "egui_dock 0.7!".to_string(), + }], + ); + let [_, _] = tree.main_surface_mut().split_below( + a, + 0.4, + vec![OpinionatedTab { + can_become_window: Ok(true), + title: "instructional tab".to_owned(), + content: "This demo is meant to showcase the ability for tabs to become/be placed inside windows. + \nindividual tabs have the ability to accept/reject being put/turned into a window. + \nIn this demo some tabs have a fixed opinion on this, others can be swayed with the click of a checkbox. + \n\n In your app you yourself may decide how tabs behave, but for now try dragging some tabs into empty space to turn them into windows!" + .to_string(), + }], + ); + let _ = tree.add_window(vec![OpinionatedTab { + can_become_window: Err(true), + title: "egotistical tab".to_owned(), + content: "im above you all!".to_string(), + }]); + + Self { tree } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + DockArea::new(&mut self.tree) + .style(Style::from_egui(ctx.style().as_ref())) + .show(ctx, &mut TabViewer {}); + } +} diff --git a/examples/simple.rs b/examples/simple.rs index 8030c67..6b86198 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -2,7 +2,7 @@ use eframe::{egui, NativeOptions}; -use egui_dock::{DockArea, NodeIndex, Style, Tree}; +use egui_dock::{DockArea, DockState, NodeIndex, Style}; fn main() -> eframe::Result<()> { let options = NativeOptions::default(); @@ -28,17 +28,23 @@ impl egui_dock::TabViewer for TabViewer { } struct MyApp { - tree: Tree, + tree: DockState, } impl Default for MyApp { fn default() -> Self { - let mut tree = Tree::new(vec!["tab1".to_owned(), "tab2".to_owned()]); + let mut tree = DockState::new(vec!["tab1".to_owned(), "tab2".to_owned()]); // You can modify the tree before constructing the dock - let [a, b] = tree.split_left(NodeIndex::root(), 0.3, vec!["tab3".to_owned()]); - let [_, _] = tree.split_below(a, 0.7, vec!["tab4".to_owned()]); - let [_, _] = tree.split_below(b, 0.5, vec!["tab5".to_owned()]); + let [a, b] = + tree.main_surface_mut() + .split_left(NodeIndex::root(), 0.3, vec!["tab3".to_owned()]); + let [_, _] = tree + .main_surface_mut() + .split_below(a, 0.7, vec!["tab4".to_owned()]); + let [_, _] = tree + .main_surface_mut() + .split_below(b, 0.5, vec!["tab5".to_owned()]); Self { tree } } diff --git a/examples/tab_add.rs b/examples/tab_add.rs index a8ab39e..e7a63ac 100644 --- a/examples/tab_add.rs +++ b/examples/tab_add.rs @@ -2,7 +2,7 @@ use eframe::{egui, NativeOptions}; -use egui_dock::{DockArea, NodeIndex, Style, Tree}; +use egui_dock::{DockArea, DockState, NodeIndex, Style}; fn main() -> eframe::Result<()> { let options = NativeOptions::default(); @@ -34,18 +34,20 @@ impl egui_dock::TabViewer for TabViewer<'_> { } struct MyApp { - tree: Tree, + tree: DockState, counter: usize, } impl Default for MyApp { fn default() -> Self { - let mut tree = Tree::new(vec![1, 2]); + let mut tree = DockState::new(vec![1, 2]); // You can modify the tree before constructing the dock - let [a, b] = tree.split_left(NodeIndex::root(), 0.3, vec![3]); - let [_, _] = tree.split_below(a, 0.7, vec![4]); - let [_, _] = tree.split_below(b, 0.5, vec![5]); + let [a, b] = tree + .main_surface_mut() + .split_left(NodeIndex::root(), 0.3, vec![3]); + let [_, _] = tree.main_surface_mut().split_below(a, 0.7, vec![4]); + let [_, _] = tree.main_surface_mut().split_below(b, 0.5, vec![5]); Self { tree, counter: 6 } } @@ -69,7 +71,7 @@ impl eframe::App for MyApp { ); added_nodes.drain(..).for_each(|node| { - self.tree.set_focused_node(node); + self.tree.main_surface_mut().set_focused_node(node); self.tree.push_to_focused_leaf(self.counter); self.counter += 1; }); diff --git a/examples/tab_add_popup.rs b/examples/tab_add_popup.rs index aa4d8a1..da517c0 100644 --- a/examples/tab_add_popup.rs +++ b/examples/tab_add_popup.rs @@ -3,7 +3,7 @@ use eframe::{egui, NativeOptions}; use egui::{Color32, RichText}; -use egui_dock::{DockArea, NodeIndex, Style, Tree}; +use egui_dock::{DockArea, DockState, NodeIndex, Style}; fn main() -> eframe::Result<()> { let options = NativeOptions::default(); @@ -91,18 +91,24 @@ impl egui_dock::TabViewer for TabViewer<'_> { } struct MyApp { - tree: Tree, + tree: DockState, counter: usize, } impl Default for MyApp { fn default() -> Self { - let mut tree = Tree::new(vec![MyTab::regular(1), MyTab::fancy(2)]); + let mut tree = DockState::new(vec![MyTab::regular(1), MyTab::fancy(2)]); // You can modify the tree before constructing the dock - let [a, b] = tree.split_left(NodeIndex::root(), 0.3, vec![MyTab::fancy(3)]); - let [_, _] = tree.split_below(a, 0.7, vec![MyTab::fancy(4)]); - let [_, _] = tree.split_below(b, 0.5, vec![MyTab::regular(5)]); + let [a, b] = + tree.main_surface_mut() + .split_left(NodeIndex::root(), 0.3, vec![MyTab::fancy(3)]); + let [_, _] = tree + .main_surface_mut() + .split_below(a, 0.7, vec![MyTab::fancy(4)]); + let [_, _] = tree + .main_surface_mut() + .split_below(b, 0.5, vec![MyTab::regular(5)]); Self { tree, counter: 6 } } @@ -123,7 +129,7 @@ impl eframe::App for MyApp { ); added_nodes.drain(..).for_each(|node| { - self.tree.set_focused_node(node.node); + self.tree.main_surface_mut().set_focused_node(node.node); self.tree.push_to_focused_leaf(MyTab { kind: node.kind, node: NodeIndex(self.counter), diff --git a/examples/text_editor.rs b/examples/text_editor.rs index 932d036..bd8153a 100644 --- a/examples/text_editor.rs +++ b/examples/text_editor.rs @@ -37,7 +37,7 @@ impl egui_dock::TabViewer for Buffers { struct MyApp { buffers: Buffers, - tree: egui_dock::Tree, + tree: egui_dock::DockState, } impl Default for MyApp { @@ -53,7 +53,8 @@ impl Default for MyApp { include_str!("../README.md").to_owned(), ); - let tree = egui_dock::Tree::new(vec!["README.md".to_owned(), "CHANGELOG.md".to_owned()]); + let tree = + egui_dock::DockState::new(vec!["README.md".to_owned(), "CHANGELOG.md".to_owned()]); Self { buffers: Buffers { buffers }, @@ -69,8 +70,8 @@ impl eframe::App for MyApp { let tab_location = self.tree.find_tab(title); let is_open = tab_location.is_some(); if ui.selectable_label(is_open, title).clicked() { - if let Some((node_index, tab_index)) = tab_location { - self.tree.set_active_tab(node_index, tab_index); + if let Some(tab_location) = tab_location { + self.tree.set_active_tab(tab_location); } else { // Open the file for editing: self.tree.push_to_focused_leaf(title.clone()); diff --git a/src/dock_state/mod.rs b/src/dock_state/mod.rs new file mode 100644 index 0000000..9ae70d9 --- /dev/null +++ b/src/dock_state/mod.rs @@ -0,0 +1,412 @@ +/// Wrapper around indices to the collection of surfaces inside a [`DockState`]. +pub mod surface_index; + +pub mod tree; + +/// Represents an area in which a dock tree is rendered. +pub mod surface; +/// Window states which tells floating tabs how to be displayed inside their window, +pub mod window_state; + +pub use surface::Surface; +pub use surface_index::SurfaceIndex; +pub use window_state::WindowState; + +use egui::Rect; + +use crate::{Node, NodeIndex, Split, TabDestination, TabIndex, TabInsert, Tree}; + +/// The heart of `egui_dock`. +/// +/// This structure holds a collection of surfaces, each of which stores a tree in which tabs are arranged. +/// +/// Indexing it with a [`SurfaceIndex`] will yield a [`Tree`] which then contains nodes and tabs. +/// +/// [`DockState`] is generic, so you can use any type of data to represent a tab. +pub struct DockState { + surfaces: Vec>, + focused_surface: Option, // Part of the tree which is in focus. +} + +impl std::ops::Index for DockState { + type Output = Tree; + + #[inline(always)] + fn index(&self, index: SurfaceIndex) -> &Self::Output { + match self.surfaces[index.0].node_tree() { + Some(tree) => tree, + None => { + panic!("There did not exist a tree at surface index {}", index.0); + } + } + } +} + +impl std::ops::IndexMut for DockState { + #[inline(always)] + fn index_mut(&mut self, index: SurfaceIndex) -> &mut Self::Output { + match self.surfaces[index.0].node_tree_mut() { + Some(tree) => tree, + None => { + panic!("There did not exist a tree at surface index {}", index.0); + } + } + } +} + +impl DockState { + /// Create a new tree with given tabs at the main surface's root node. + pub fn new(tabs: Vec) -> Self { + Self { + surfaces: vec![Surface::Main(Tree::new(tabs))], + focused_surface: None, + } + } + + /// Get a mutable borrow to the tree at the main surface. + pub fn main_surface_mut(&mut self) -> &mut Tree { + &mut self[SurfaceIndex::main()] + } + + /// Get an immutable borrow to the tree at the main surface. + pub fn main_surface(&self) -> &Tree { + &self[SurfaceIndex::main()] + } + + /// Get the [`WindowState`] which corresponds to a [`SurfaceIndex`]. + /// + /// Returns `None` if the surface is [`Empty`](Surface::Empty), [`Main`](Surface::Main), or doesn't exist. + /// + /// This can be used to modify properties of a window, e.g. size and position. + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::DockState; + /// # use egui::{Vec2, Pos2}; + /// let mut dock_state = DockState::new(vec![]); + /// let mut surface_index = dock_state.add_window(vec!["Window Tab".to_string()]); + /// let window_state = dock_state.get_window_state_mut(surface_index).unwrap(); + /// + /// window_state.set_position(Pos2::ZERO); + /// window_state.set_size(Vec2::splat(100.0)); + /// ``` + pub fn get_window_state_mut(&mut self, surface: SurfaceIndex) -> Option<&mut WindowState> { + match &mut self.surfaces[surface.0] { + Surface::Window(_, state) => Some(state), + _ => None, + } + } + + /// Get the [`WindowState`] which corresponds to a [`SurfaceIndex`]. + /// + /// Returns `None` if the surface is an [`Empty`](Surface::Empty), [`Main`](Surface::Main), or doesn't exist. + pub fn get_window_state(&mut self, surface: SurfaceIndex) -> Option<&WindowState> { + match &self.surfaces[surface.0] { + Surface::Window(_, state) => Some(state), + _ => None, + } + } + + /// Returns the viewport [`Rect`] and the `Tab` inside the focused leaf node or `None` if no node is in focus. + #[inline] + pub fn find_active_focused(&mut self) -> Option<(Rect, &mut Tab)> { + self.focused_surface + .and_then(|surface| self[surface].find_active_focused()) + } + + /// Get a mutable borrow to the raw surface from a surface index. + #[inline] + pub fn get_surface_mut(&mut self, surface: SurfaceIndex) -> Option<&mut Surface> { + self.surfaces.get_mut(surface.0) + } + + /// Get an immutable borrow to the raw surface from a surface index. + #[inline] + pub fn get_surface(&self, surface: SurfaceIndex) -> Option<&Surface> { + self.surfaces.get(surface.0) + } + + /// Returns true if the specified surface exists and isn't [`Empty`](Surface::Empty). + #[inline] + pub fn is_surface_valid(&self, surface_index: SurfaceIndex) -> bool { + self.surfaces + .get(surface_index.0) + .map_or(false, |surface| !surface.is_empty()) + } + + /// Returns a list of all valid [`SurfaceIndex`]es. + #[inline] + pub(crate) fn valid_surface_indices(&self) -> Box<[SurfaceIndex]> { + (0..self.surfaces.len()) + .filter_map(|index| { + let index = SurfaceIndex(index); + self.is_surface_valid(index).then_some(index) + }) + .collect() + } + + /// Remove a surface based on its [`SurfaceIndex`] + /// + /// Returns the removed surface or `None` if it didn't exist. + /// + /// # Panics + /// + /// Panics if you try to remove the main surface: `SurfaceIndex::main()`. + pub fn remove_surface(&mut self, surface_index: SurfaceIndex) -> Option> { + assert!(!surface_index.is_main()); + (surface_index.0 < self.surfaces.len()).then(|| { + self.focused_surface = Some(SurfaceIndex::main()); + if surface_index.0 == self.surfaces.len() - 1 { + self.surfaces.pop().unwrap() + } else { + let dest = &mut self.surfaces[surface_index.0]; + std::mem::replace(dest, Surface::Empty) + } + }) + } + + /// Sets which is the active tab within a specific node on a given surface. + #[inline] + pub fn set_active_tab( + &mut self, + (surface_index, node_index, tab_index): (SurfaceIndex, NodeIndex, TabIndex), + ) { + if let Some(Node::Leaf { active, .. }) = self[surface_index].tree.get_mut(node_index.0) { + *active = tab_index; + } + } + + /// Sets the currently focused leaf to `node_index` if the node at `node_index` is a leaf. + #[inline] + pub fn set_focused_node_and_surface( + &mut self, + (surface_index, node_index): (SurfaceIndex, NodeIndex), + ) { + if self.is_surface_valid(surface_index) && node_index.0 < self[surface_index].len() { + // I don't want this code to be evaluated until im absolutely sure the surface index is valid. + if self[surface_index][node_index].is_leaf() { + self.focused_surface = Some(surface_index); + self[surface_index].set_focused_node(node_index); + return; + } + } + self.focused_surface = None; + } + + /// Moves a tab from a node to another node. + /// You need to specify with [`TabDestination`] how the tab should be moved. + pub fn move_tab( + &mut self, + (src_surface, src_node, src_tab): (SurfaceIndex, NodeIndex, TabIndex), + dst_tab: impl Into, + ) { + match dst_tab.into() { + TabDestination::Window(position) => { + self.detach_tab((src_surface, src_node, src_tab), position); + } + TabDestination::Node(dst_surface, dst_node, dst_tab) => { + // Moving a single tab inside its own node is a no-op + if src_surface == dst_surface + && src_node == dst_node + && self[src_surface][src_node].tabs_count() == 1 + { + return; + } + + // Call `Node::remove_tab` to avoid auto remove of the node by `Tree::remove_tab` from Tree. + let tab = self[src_surface][src_node].remove_tab(src_tab).unwrap(); + match dst_tab { + TabInsert::Split(split) => { + self[dst_surface].split(dst_node, split, 0.5, Node::leaf(tab)); + } + TabInsert::Insert(index) => self[dst_surface][dst_node].insert_tab(index, tab), + TabInsert::Append => self[dst_surface][dst_node].append_tab(tab), + } + + if self[src_surface][src_node].is_leaf() + && self[src_surface][src_node].tabs_count() == 0 + { + self[src_surface].remove_leaf(src_node); + } + if self[src_surface].is_empty() && !src_surface.is_main() { + self.remove_surface(src_surface); + } + } + TabDestination::EmptySurface(dst_surface) => { + assert!(self[dst_surface].is_empty()); + let tab = self[src_surface][src_node].remove_tab(src_tab).unwrap(); + self[dst_surface] = Tree::new(vec![tab]) + } + }; + } + + /// Takes a tab out of its current surface and puts it in a new window. + /// Returns the surface index of the new window. + pub fn detach_tab( + &mut self, + (src_surface, src_node, src_tab): (SurfaceIndex, NodeIndex, TabIndex), + window_rect: Rect, + ) -> SurfaceIndex { + // Remove the tab from the tree and it add to a new window. + let tab = self[src_surface][src_node].remove_tab(src_tab).unwrap(); + let surface_index = self.add_window(vec![tab]); + + // Set the window size and position to match `window_rect`. + let state = self.get_window_state_mut(surface_index).unwrap(); + state.set_position(window_rect.min); + if src_surface.is_main() { + state.set_size(window_rect.size() * 0.8); + } else { + state.set_size(window_rect.size()); + } + + // Clean up any empty leaves and surfaces which may be left behind from the detachment. + if self[src_surface][src_node].is_leaf() && self[src_surface][src_node].tabs_count() == 0 { + self[src_surface].remove_leaf(src_node); + } + if self[src_surface].is_empty() && !src_surface.is_main() { + self.remove_surface(src_surface); + } + surface_index + } + + /// Currently focused leaf. + #[inline] + pub fn focused_leaf(&self) -> Option<(SurfaceIndex, NodeIndex)> { + let surface = self.focused_surface?; + self[surface].focused_leaf().map(|leaf| (surface, leaf)) + } + + /// Remove a tab at the specified surface, node, and tab index. + /// This method will yield the removed tab, or `None` if it doesn't exist. + pub fn remove_tab( + &mut self, + (surface_index, node_index, tab_index): (SurfaceIndex, NodeIndex, TabIndex), + ) -> Option { + self[surface_index].remove_tab((node_index, tab_index)) + } + + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node + /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the + /// split. + /// + /// The new node is placed relatively to the old node, in the direction specified by `split`. + /// + /// Returns the indices of the old node and the new node. + pub fn split( + &mut self, + (surface, parent): (SurfaceIndex, NodeIndex), + split: Split, + fraction: f32, + new: Node, + ) -> [NodeIndex; 2] { + let index = self[surface].split(parent, split, fraction, new); + self.focused_surface = Some(surface); + index + } + + /// Adds a window with its own list of tabs. + /// + /// Returns the [`SurfaceIndex`] of the new window, which will remain constant through the windows lifetime. + pub fn add_window(&mut self, tabs: Vec) -> SurfaceIndex { + let surface = Surface::Window(Tree::new(tabs), WindowState::new()); + let index = self.find_empty_surface_index(); + if index.0 < self.surfaces.len() { + self.surfaces[index.0] = surface; + } else { + self.surfaces.push(surface); + } + index + } + + /// Finds the first empty surface index which may be used. + /// + /// **WARNING**: in cases where one isn't found, `SurfaceIndex(self.surfaces.len())` is used. + /// therefore it's not inherently safe to index the [`DockState`] with this index, as it may panic. + fn find_empty_surface_index(&self) -> SurfaceIndex { + // Find the first possible empty surface to insert our window into. + // Starts at 1 as 0 is always the main surface. + for i in 1..self.surfaces.len() { + if self.surfaces[i].is_empty() { + return SurfaceIndex(i); + } + } + SurfaceIndex(self.surfaces.len()) + } + + /// Pushes `tab` to the currently focused leaf. + /// + /// If no leaf is focused it will be pushed to the first available leaf. + /// + /// If no leaf is available then a new leaf will be created. + pub fn push_to_focused_leaf(&mut self, tab: Tab) { + if let Some(surface) = self.focused_surface { + self[surface].push_to_focused_leaf(tab) + } else { + self[SurfaceIndex::main()].push_to_focused_leaf(tab) + } + } + + /// Push a tab to the first available `Leaf` or create a new leaf if an `Empty` node is encountered. + pub fn push_to_first_leaf(&mut self, tab: Tab) { + self[SurfaceIndex::main()].push_to_first_leaf(tab); + } + + /// Returns an `Iterator` of the underlying collection of nodes on the **main surface**. + #[deprecated = "Use `iter_main_surface_nodes` or `iter_nodes` instead"] + pub fn iter(&self) -> std::slice::Iter<'_, Node> { + self.iter_main_surface_nodes() + } + + /// Returns an `Iterator` of the underlying collection of nodes on the main surface. + pub fn iter_main_surface_nodes(&self) -> std::slice::Iter<'_, Node> { + self[SurfaceIndex::main()].iter() + } + + /// Returns an `Iterator` of **all** underlying nodes in the dock state and all subsequent trees. + pub fn iter_nodes(&self) -> impl Iterator> { + self.surfaces + .iter() + .filter_map(|tree| tree.node_tree()) + .flat_map(|nodes| nodes.iter()) + } +} + +impl DockState +where + Tab: PartialEq, +{ + /// Find the given tab. + /// + /// Returns in which node and where in that node the tab is. + /// + /// The returned [`NodeIndex`] will always point to a [`Node::Leaf`]. + /// + /// In case there are several hits, only the first is returned. + /// + /// See also: [`find_main_surface_tab`](DockState::find_main_surface_tab) + pub fn find_tab(&self, needle_tab: &Tab) -> Option<(SurfaceIndex, NodeIndex, TabIndex)> { + for &surface_index in self.valid_surface_indices().iter() { + if !self.surfaces[surface_index.0].is_empty() { + if let Some((node_index, tab_index)) = self[surface_index].find_tab(needle_tab) { + return Some((surface_index, node_index, tab_index)); + } + } + } + None + } + + /// Find the given tab on the main surface. + /// + /// Returns which node and where in that node the tab is. + /// + /// The returned [`NodeIndex`] will always point to a [`Node::Leaf`]. + /// + /// In case there are several hits, only the first is returned. + pub fn find_main_surface_tab(&self, needle_tab: &Tab) -> Option<(NodeIndex, TabIndex)> { + self[SurfaceIndex::main()].find_tab(needle_tab) + } +} diff --git a/src/dock_state/surface.rs b/src/dock_state/surface.rs new file mode 100644 index 0000000..685db0f --- /dev/null +++ b/src/dock_state/surface.rs @@ -0,0 +1,40 @@ +use crate::{Tree, WindowState}; + +/// A [`Surface`] is the highest level component in a [`DockState`](crate::DockState). [`Surface`]s represent an area +/// in which nodes are placed. Typically, you're only using one surface, which is the main surface. However, if you drag +/// a tab out in a way which creates a window, you also create a new surface in which nodes can appear. +pub enum Surface { + /// An empty surface, with nothing inside (practically, a null surface). + Empty, + + /// The main surface of a [`DockState`](crate::DockState), only one should exist at surface index 0 at any one time. + Main(Tree), + + /// A windowed surface with a state. + Window(Tree, WindowState), +} + +impl Surface { + /// Is this surface [`Empty`](Self::Empty) (in practice null)? + pub const fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } + + /// Get mutable access to the node tree of this surface. + pub fn node_tree_mut(&mut self) -> Option<&mut Tree> { + match self { + Surface::Empty => None, + Surface::Main(tree) => Some(tree), + Surface::Window(tree, _) => Some(tree), + } + } + + /// Get access to the node tree of this surface. + pub fn node_tree(&self) -> Option<&Tree> { + match self { + Surface::Empty => None, + Surface::Main(tree) => Some(tree), + Surface::Window(tree, _) => Some(tree), + } + } +} diff --git a/src/dock_state/surface_index.rs b/src/dock_state/surface_index.rs new file mode 100644 index 0000000..338714d --- /dev/null +++ b/src/dock_state/surface_index.rs @@ -0,0 +1,25 @@ +/// Wrapper around indices to the collection of Surfaces inside a [`DockState`](crate::DockState). +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct SurfaceIndex(pub usize); + +impl From for SurfaceIndex { + #[inline(always)] + fn from(index: usize) -> Self { + SurfaceIndex(index) + } +} + +impl SurfaceIndex { + /// Returns the index of the main surface. + #[inline(always)] + pub const fn main() -> Self { + Self(0) + } + + /// Returns if this index is `SurfaceIndex::main()`. + #[inline(always)] + pub const fn is_main(self) -> bool { + self.0 == Self::main().0 + } +} diff --git a/src/tree/mod.rs b/src/dock_state/tree/mod.rs similarity index 58% rename from src/tree/mod.rs rename to src/dock_state/tree/mod.rs index 28be72d..d4c874a 100644 --- a/src/tree/mod.rs +++ b/src/dock_state/tree/mod.rs @@ -26,7 +26,13 @@ pub use tab_index::TabIndex; pub use tab_iter::TabIter; use egui::Rect; -use std::fmt; +use std::{ + fmt, + ops::{Index, IndexMut}, + slice::{Iter, IterMut}, +}; + +use crate::SurfaceIndex; // ---------------------------------------------------------------------------- @@ -40,17 +46,60 @@ pub enum Split { Below, } +impl Split { + /// Returns whether the split is vertical. + pub const fn is_top_bottom(self) -> bool { + matches!(self, Split::Above | Split::Below) + } + + /// Returns whether the split is horizontal. + pub const fn is_left_right(self) -> bool { + matches!(self, Split::Left | Split::Right) + } +} + /// Specify how a tab should be added to a Node. -pub enum TabDestination { +pub enum TabInsert { /// Split the node in the given direction. Split(Split), + /// Insert the tab at the given index. Insert(TabIndex), + /// Append the tab to the node. Append, } -// ---------------------------------------------------------------------------- +/// The destination for a tab which is being moved. +pub enum TabDestination { + /// Move to a new window with this rect. + Window(Rect), + + /// Move to a an existing node with this insertion. + Node(SurfaceIndex, NodeIndex, TabInsert), + + /// Move to an empty surface. + EmptySurface(SurfaceIndex), +} + +impl From<(SurfaceIndex, NodeIndex, TabInsert)> for TabDestination { + fn from(value: (SurfaceIndex, NodeIndex, TabInsert)) -> TabDestination { + TabDestination::Node(value.0, value.1, value.2) + } +} + +impl From for TabDestination { + fn from(value: SurfaceIndex) -> TabDestination { + TabDestination::EmptySurface(value) + } +} + +impl TabDestination { + /// Returns if this tab destination is a [`Window`](TabDestination::Window). + pub fn is_window(&self) -> bool { + matches!(self, Self::Window(_)) + } +} /// Binary tree representing the relationships between [`Node`]s. /// @@ -72,15 +121,14 @@ pub enum TabDestination { #[derive(Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Tree { - tree: Vec>, + // Binary tree vector + pub(super) tree: Vec>, focused_node: Option, } impl fmt::Debug for Tree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Tree") - .field("focused_node", &self.focused_node) - .finish_non_exhaustive() + f.debug_struct("Tree").finish_non_exhaustive() } } @@ -93,7 +141,7 @@ impl Default for Tree { } } -impl std::ops::Index for Tree { +impl Index for Tree { type Output = Node; #[inline(always)] @@ -102,7 +150,7 @@ impl std::ops::Index for Tree { } } -impl std::ops::IndexMut for Tree { +impl IndexMut for Tree { #[inline(always)] fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output { &mut self.tree[index.0] @@ -110,7 +158,7 @@ impl std::ops::IndexMut for Tree { } impl Tree { - /// Creates a new `Tree` with given `Vec` of `Tab`s in its root node. + /// Creates a new [`Tree`] with given `Vec` of `Tab`s in its root node. #[inline(always)] pub fn new(tabs: Vec) -> Self { let root = Node::leaf_with(tabs); @@ -120,77 +168,78 @@ impl Tree { } } - /// Returns the viewport `Rect` and the `Tab` inside the first leaf node, or `None` of no leaf exists in the `Tree`. + /// Returns the viewport [`Rect`] and the `Tab` inside the first leaf node, + /// or `None` if no leaf exists in the [`Tree`]. #[inline] pub fn find_active(&mut self) -> Option<(Rect, &mut Tab)> { - self.tree.iter_mut().find_map(|node| { - if let Node::Leaf { + self.tree.iter_mut().find_map(|node| match node { + Node::Leaf { tabs, active, viewport, .. - } = node - { - tabs.get_mut(active.0).map(|tab| (*viewport, tab)) - } else { - None - } + } => tabs.get_mut(active.0).map(|tab| (viewport.to_owned(), tab)), + _ => None, }) } - /// Returns the viewport `Rect` and the `Tab` inside the focused leaf node or `None` if it does not exist. - #[inline] - pub fn find_active_focused(&mut self) -> Option<(Rect, &mut Tab)> { - if let Some(Node::Leaf { - tabs, - active, - viewport, - .. - }) = self.focused_node.and_then(|idx| self.tree.get_mut(idx.0)) - { - tabs.get_mut(active.0).map(|tab| (*viewport, tab)) - } else { - None - } - } - - /// Returns the number of nodes in the `Tree`. + /// Returns the number of nodes in the [`Tree`]. + /// + /// This includes [`Empty`](Node::Empty) nodes. #[inline(always)] pub fn len(&self) -> usize { self.tree.len() } - /// Returns `true` if the number of nodes in the tree is 0, `false` otherwise. + /// Returns `true` if the number of nodes in the tree is 0, otherwise `false`. #[inline(always)] pub fn is_empty(&self) -> bool { self.tree.is_empty() } - /// Returns `Iter` of the underlying collection of nodes. + /// Returns an [`Iterator`] of the underlying collection of nodes. + /// + /// This includes [`Empty`](Node::Empty) nodes. #[inline(always)] - pub fn iter(&self) -> std::slice::Iter<'_, Node> { + pub fn iter(&self) -> Iter<'_, Node> { self.tree.iter() } - /// Returns `IterMut` of the underlying collection of nodes. + /// Returns [`IterMut`] of the underlying collection of nodes. + /// + /// This includes [`Empty`](Node::Empty) nodes. #[inline(always)] - pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Node> { + pub fn iter_mut(&mut self) -> IterMut<'_, Node> { self.tree.iter_mut() } - /// Returns an `Iterator` of [`NodeIndex`] ordered in a breadth first manner. + /// Returns an [`Iterator`] of [`NodeIndex`] ordered in a breadth first manner. #[inline(always)] pub(crate) fn breadth_first_index_iter(&self) -> impl Iterator { (0..self.tree.len()).map(NodeIndex) } - /// Returns an iterator over all tabs in arbitrary order + /// Returns an iterator over all tabs in arbitrary order. #[inline(always)] pub fn tabs(&self) -> TabIter<'_, Tab> { TabIter::new(self) } - /// Number of tabs + /// Counts and returns the number of tabs in the whole tree. + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::{DockState, NodeIndex, TabIndex}; + /// let mut dock_state = DockState::new(vec!["node 1", "node 2", "node 3"]); + /// assert_eq!(dock_state.main_surface().num_tabs(), 3); + /// + /// let [a, b] = dock_state.main_surface_mut().split_left(NodeIndex::root(), 0.5, vec!["tab 4", "tab 5"]); + /// assert_eq!(dock_state.main_surface().num_tabs(), 5); + /// + /// dock_state.main_surface_mut().remove_leaf(a); + /// assert_eq!(dock_state.main_surface().num_tabs(), 2); + /// ``` #[inline] pub fn num_tabs(&self) -> usize { let mut count = 0; @@ -202,15 +251,71 @@ impl Tree { count } + /// Acquire a immutable borrow to the [`Node`] at the root of the tree. + /// Returns [`None`] if the tree is empty. + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::{DockState}; + /// let mut dock_state = DockState::new(vec!["single tab"]); + /// let root_node = dock_state.main_surface().root_node().unwrap(); + /// + /// assert_eq!(root_node.tabs(), Some(["single tab"].as_slice())); + /// ``` + pub fn root_node(&self) -> Option<&Node> { + self.tree.get(0) + } + + /// Acquire a mutable borrow to the [`Node`] at the root of the tree. + /// Returns [`None`] if the tree is empty. + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::{DockState, Node}; + /// let mut dock_state = DockState::new(vec!["single tab"]); + /// let root_node = dock_state.main_surface_mut().root_node_mut().unwrap(); + /// if let Node::Leaf { tabs, ..} = root_node { + /// tabs.push("partner tab"); + /// } + /// assert_eq!(root_node.tabs(), Some(["single tab", "partner tab"].as_slice())); + /// ``` + pub fn root_node_mut(&mut self) -> Option<&mut Node> { + self.tree.get_mut(0) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) gets the `tabs`. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// /// The new node is placed relatively to the old node, in the direction specified by `split`. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. + /// + /// # Example + /// + /// ```rust + /// # use egui_dock::{DockState, SurfaceIndex, NodeIndex, Split}; + /// let mut dock_state = DockState::new(vec!["tab 1", "tab 2"]); + /// + /// // At this point, the main surface only contains the leaf with tab 1 and 2. + /// assert!(dock_state.main_surface().root_node().unwrap().is_leaf()); + /// + /// // Split the node, giving 50% of the space to the new nodes and 50% to the old ones. + /// let [old, new] = dock_state.main_surface_mut() + /// .split_tabs(NodeIndex::root(), Split::Below, 0.5, vec!["tab 3"]); + /// + /// assert!(dock_state.main_surface().root_node().unwrap().is_parent()); + /// assert!(dock_state[SurfaceIndex::main()][old].is_leaf()); + /// assert!(dock_state[SurfaceIndex::main()][new].is_leaf()); + /// ``` #[inline(always)] pub fn split_tabs( &mut self, @@ -223,14 +328,20 @@ impl Tree { } /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) gets the `tabs`. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// This is a shorthand for using `split_tabs` with [`Split::Above`]. + /// + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// - /// The new node is placed above the old node. + /// The new node is placed *above* the old node. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. #[inline(always)] pub fn split_above( &mut self, @@ -242,14 +353,20 @@ impl Tree { } /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) gets the `tabs`. + /// + /// This is a shorthand for using `split_tabs` with [`Split::Below`]. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// - /// The new node is placed below the old node. + /// The new node is placed *below* the old node. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. #[inline(always)] pub fn split_below( &mut self, @@ -261,14 +378,20 @@ impl Tree { } /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) gets the `tabs`. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// This is a shorthand for using `split_tabs` with [`Split::Left`]. + /// + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// - /// The new node is placed to the left of the old node. + /// The new node is placed to the *left* of the old node. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. #[inline(always)] pub fn split_left( &mut self, @@ -280,14 +403,20 @@ impl Tree { } /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) gets the `tabs`. + /// + /// This is a shorthand for using `split_tabs` with [`Split::Right`]. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// - /// The new node is placed to the right of the old node. + /// The new node is placed to the *right* of the old node. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. #[inline(always)] pub fn split_right( &mut self, @@ -299,14 +428,42 @@ impl Tree { } /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node - /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. + /// inherits content of the `parent` from before the split, and the second (new) uses `new`. /// - /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node is will occupy after the + /// `fraction` (in range 0..=1) specifies how much of the `parent` node's area the old node will occupy after the /// split. /// /// The new node is placed relatively to the old node, in the direction specified by `split`. /// /// Returns the indices of the old node and the new node. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. + /// + /// If `new` is an [`Empty`](Node::Empty), [`Horizontal`](Node::Horizontal) or [`Vertical`](Node::Vertical) node. + /// + /// If `new` is a [`Leaf`](Node::Leaf) node without any tabs. + /// + /// If `parent` points to an [`Empty`](Node::Empty) node. + /// + /// # Example + /// + /// ```rust + /// # use egui_dock::{DockState, SurfaceIndex, NodeIndex, Split, Node}; + /// let mut dock_state = DockState::new(vec!["tab 1", "tab 2"]); + /// + /// // At this point, the main surface only contains the leaf with tab 1 and 2. + /// assert!(dock_state.main_surface().root_node().unwrap().is_leaf()); + /// + /// // Splits the node, giving 50% of the space to the new nodes and 50% to the old ones. + /// let [old, new] = dock_state.main_surface_mut() + /// .split(NodeIndex::root(), Split::Below, 0.5, Node::leaf_with(vec!["tab 3"])); + /// + /// assert!(dock_state.main_surface().root_node().unwrap().is_parent()); + /// assert!(dock_state[SurfaceIndex::main()][old].is_leaf()); + /// assert!(dock_state[SurfaceIndex::main()][new].is_leaf()); + /// ``` pub fn split( &mut self, parent: NodeIndex, @@ -315,12 +472,14 @@ impl Tree { new: Node, ) -> [NodeIndex; 2] { let old = self[parent].split(split, fraction); - assert!(old.is_leaf()); - + assert!(old.is_leaf() || old.is_parent()); + assert_ne!(new.tabs_count(), 0); + // Resize vector to fit the new size of the binary tree. { let index = self.tree.iter().rposition(|n| !n.is_empty()).unwrap_or(0); let level = NodeIndex(index).level(); - self.tree.resize_with(1 << (level + 1), || Node::Empty); + self.tree + .resize_with((1 << (level + 1)) - 1, || Node::Empty); } let index = match split { @@ -328,6 +487,34 @@ impl Tree { Split::Right | Split::Below => [parent.left(), parent.right()], }; + // If the node were splitting is a parent, all it's children need to be moved. + if old.is_parent() { + let levels_to_move = NodeIndex(self.tree.len()).level() - index[0].level(); + + // Level 0 is ourself, which is done when we assign self[index[0]] = old, so start at 1. + for level in (1..levels_to_move).rev() { + // Old child indices for this level + let old_start = parent.children_at(level).start; + // New child indices for this level + let new_start = index[0].children_at(level).start; + + // Children to be moved this level change + let len = 1 << level; + + // Swap self[old_start..(old_start+len)] with self[new_start..(new_start+len)] + // (the new part will only contain empty entries). + let (old_range, new_range) = { + let (first_part, second_part) = self.tree.split_at_mut(new_start); + // Cut to length. + ( + &mut first_part[old_start..old_start + len], + &mut second_part[..len], + ) + }; + old_range.swap_with_slice(new_range); + } + } + self[index[0]] = old; self[index[1]] = new; @@ -336,35 +523,6 @@ impl Tree { index } - /// Moves a tab from a node to another node, you specify how the tab should - /// be moved with [`TabDestination`]. - pub fn move_tab( - &mut self, - (src_node, src_tab): (NodeIndex, TabIndex), - (dst_node, dst_tab): (NodeIndex, TabDestination), - ) { - // Moving a single tab inside its own node is a no-op - if src_node == dst_node && self[src_node].tabs_count() == 1 { - return; - } - - // Call `Node::remove_tab` to avoid auto remove of the node by - // `Tree::remove_tab` from Tree. - let tab = self[src_node].remove_tab(src_tab).unwrap(); - - match dst_tab { - TabDestination::Split(split) => { - self.split(dst_node, split, 0.5, Node::leaf(tab)); - } - TabDestination::Insert(index) => self[dst_node].insert_tab(index, tab), - TabDestination::Append => self[dst_node].append_tab(tab), - }; - - if self[src_node].is_leaf() && self[src_node].tabs_count() == 0 { - self.remove_leaf(src_node); - } - } - fn first_leaf(&self, top: NodeIndex) -> Option { let left = top.left(); let right = top.right(); @@ -375,10 +533,7 @@ impl Tree { ( Some(Node::Horizontal { .. } | Node::Vertical { .. }), Some(Node::Horizontal { .. } | Node::Vertical { .. }), - ) => match self.first_leaf(left) { - ret @ Some(_) => ret, - None => self.first_leaf(right), - }, + ) => self.first_leaf(left).or(self.first_leaf(right)), (Some(Node::Horizontal { .. } | Node::Vertical { .. }), _) => self.first_leaf(left), (_, Some(Node::Horizontal { .. } | Node::Vertical { .. })) => self.first_leaf(right), @@ -389,16 +544,49 @@ impl Tree { } } + /// Returns the viewport [`Rect`] and the `Tab` inside the focused leaf node or [`None`] if it does not exist. + #[inline] + pub fn find_active_focused(&mut self) -> Option<(Rect, &mut Tab)> { + match self.focused_node.and_then(|idx| self.tree.get_mut(idx.0)) { + Some(Node::Leaf { + tabs, + active, + viewport, + .. + }) => tabs.get_mut(active.0).map(|tab| (*viewport, tab)), + _ => None, + } + } + + /// Gets the node index of currently focused leaf node; returns [`None`] when no leaf is focused. + #[inline] + pub fn focused_leaf(&self) -> Option { + self.focused_node + } + + /// Sets the currently focused leaf to `node_index` if the node at `node_index` is a leaf. + /// + /// This method will not never panic and instead removes focus from all nodes when given an invalid index. + #[inline] + pub fn set_focused_node(&mut self, node_index: NodeIndex) { + self.focused_node = self + .tree + .get(node_index.0) + .filter(|node| node.is_leaf()) + .map(|_| node_index); + } + /// Removes the given node from the [`Tree`]. + /// + /// # Panics + /// + /// If the node at index `node` is not a [`Leaf`](Node::Leaf). pub fn remove_leaf(&mut self, node: NodeIndex) { assert!(self[node].is_leaf()); - let parent = match node.parent() { - Some(val) => val, - None => { - self.tree.clear(); - return; - } + let Some(parent) = node.parent() else { + self.tree.clear(); + return; }; if Some(node) == self.focused_node { @@ -410,7 +598,7 @@ impl Tree { } else { parent.left() }; - if let Some(Node::Leaf { .. }) = self.tree.get(next.0) { + if self.tree.get(next.0).is_some_and(|node| node.is_leaf()) { self.focused_node = Some(next); break; } @@ -460,7 +648,7 @@ impl Tree { } } - /// Push a tab to the first leaf it finds or creates a leaf if an empty spot is encountered. + /// Pushes a tab to the first `Leaf` it finds or create a new leaf if an `Empty` node is encountered. pub fn push_to_first_leaf(&mut self, tab: Tab) { for (index, node) in &mut self.tree.iter_mut().enumerate() { match node { @@ -483,22 +671,6 @@ impl Tree { self.focused_node = Some(NodeIndex(0)); } - /// Currently focused leaf. - #[inline] - pub fn focused_leaf(&self) -> Option { - self.focused_node - } - - /// Sets the currently focused leaf to `node_index` if the node at `node_index` is a leaf. - #[inline] - pub fn set_focused_node(&mut self, node_index: NodeIndex) { - if let Some(Node::Leaf { .. }) = self.tree.get(node_index.0) { - self.focused_node = Some(node_index); - } else { - self.focused_node = None; - } - } - /// Sets which is the active tab within a specific node. #[inline] pub fn set_active_tab(&mut self, node_index: NodeIndex, tab_index: TabIndex) { @@ -567,14 +739,14 @@ where { /// Find the given tab. /// - /// Returns which node the tab is in, and where in that node the tab is in. + /// Returns in which node and where in that node the tab is. /// /// The returned [`NodeIndex`] will always point to a [`Node::Leaf`]. /// /// In case there are several hits, only the first is returned. pub fn find_tab(&self, needle_tab: &Tab) -> Option<(NodeIndex, TabIndex)> { for (node_index, node) in self.tree.iter().enumerate() { - if let Node::Leaf { tabs, .. } = node { + if let Some(tabs) = node.tabs() { for (tab_index, tab) in tabs.iter().enumerate() { if tab == needle_tab { return Some((node_index.into(), tab_index.into())); diff --git a/src/tree/node.rs b/src/dock_state/tree/node.rs similarity index 53% rename from src/tree/node.rs rename to src/dock_state/tree/node.rs index 3b41849..afc4521 100644 --- a/src/tree/node.rs +++ b/src/dock_state/tree/node.rs @@ -1,18 +1,19 @@ -use crate::{Split, TabIndex}; +use crate::{Split, TabIndex}; use egui::Rect; /// Represents an abstract node of a [`Tree`](crate::Tree). #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum Node { - /// Empty node + /// Empty node. Empty, - /// Contains the actual tabs + + /// Contains the actual tabs. Leaf { - /// The full rectangle - tab bar plus tab body + /// The full rectangle - tab bar plus tab body. rect: Rect, - /// The tab body rectangle + /// The tab body rectangle. viewport: Rect, /// All the tabs in this node. @@ -24,7 +25,8 @@ pub enum Node { /// Scroll amount of the tab bar. scroll: f32, }, - /// Parent node in the vertical orientation + + /// Parent node in the vertical orientation. Vertical { /// The rectangle in which all children of this node are drawn. rect: Rect, @@ -32,7 +34,8 @@ pub enum Node { /// The fraction taken by the top child of this node. fraction: f32, }, - /// Parent node in the horizontal orientation + + /// Parent node in the horizontal orientation. Horizontal { /// The rectangle in which all children of this node are drawn. rect: Rect, @@ -78,39 +81,59 @@ impl Node { } } - /// Returns `true` if the node is a `Empty`, `false` otherwise. + /// Get a [`Rect`] occupied by the node, could be used e.g. to draw a highlight rect around a node. + /// + /// Returns [`None`] if node is of the [`Empty`](Node::Empty) variant. + #[inline] + pub fn rect(&self) -> Option { + match self { + Node::Empty => None, + Node::Leaf { rect, .. } + | Node::Vertical { rect, .. } + | Node::Horizontal { rect, .. } => Some(*rect), + } + } + + /// Returns `true` if the node is a [`Empty`](Node::Empty), otherwise `false`. #[inline(always)] pub const fn is_empty(&self) -> bool { matches!(self, Self::Empty) } - /// Returns `true` if the node is a `Leaf`, `false` otherwise. + /// Returns `true` if the node is a [`Leaf`](Node::Leaf), otherwise `false`. #[inline(always)] pub const fn is_leaf(&self) -> bool { matches!(self, Self::Leaf { .. }) } - /// Returns `true` if the node is a `Horizontal`, `false` otherwise. + /// Returns `true` if the node is a [`Horizontal`](Node::Horizontal), otherwise `false`. #[inline(always)] pub const fn is_horizontal(&self) -> bool { matches!(self, Self::Horizontal { .. }) } - /// Returns `true` if the node is a `Vertical`, `false` otherwise. + /// Returns `true` if the node is a [`Vertical`](Node::Vertical), otherwise `false`. #[inline(always)] pub const fn is_vertical(&self) -> bool { matches!(self, Self::Vertical { .. }) } - /// Returns `true` if the node is either `Horizontal` or `Vertical`, `false` otherwise. + /// Returns `true` if the node is either [`Horizontal`](Node::Horizontal) or [`Vertical`](Node::Vertical), + /// otherwise `false`. #[inline(always)] pub const fn is_parent(&self) -> bool { self.is_horizontal() || self.is_vertical() } - /// Replaces the node with a `Horizontal` or `Vertical` one (depending on `split`) and assigns it an empty rect. + /// Replaces the node with [`Horizontal`](Node::Horizontal) or [`Vertical`](Node::Vertical) (depending on `split`) + /// and assigns an empty rect to it. + /// + /// # Panics + /// + /// If `fraction` isn't in range 0..=1. #[inline] pub fn split(&mut self, split: Split, fraction: f32) -> Self { + assert!((0.0..=1.0).contains(&fraction)); let rect = Rect::NOTHING; let src = match split { Split::Left | Split::Right => Node::Horizontal { fraction, rect }, @@ -119,10 +142,73 @@ impl Node { std::mem::replace(self, src) } - /// Adds a `tab` to the node. + /// Provides an immutable slice of the tabs inside this node. + /// + /// Returns [`None`] if the node is not a [`Leaf`](Node::Leaf). + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::{DockState, NodeIndex}; + /// let mut dock_state = DockState::new(vec![1, 2, 3, 4, 5, 6]); + /// assert!(dock_state.main_surface().root_node().unwrap().tabs().unwrap().contains(&4)); + /// ``` + #[inline] + pub fn tabs(&self) -> Option<&[Tab]> { + match self { + Node::Leaf { tabs, .. } => Some(tabs), + _ => None, + } + } + + /// Provides an mutable slice of the tabs inside this node. + /// + /// Returns [`None`] if the node is not a [`Leaf`](Node::Leaf). + /// + /// # Examples + /// + /// Modifying tabs inside a node: + /// ```rust + /// # use egui_dock::{DockState, NodeIndex}; + /// let mut dock_state = DockState::new(vec![1, 2, 3, 4, 5, 6]); + /// let mut tabs = dock_state + /// .main_surface_mut() + /// .root_node_mut() + /// .unwrap() + /// .tabs_mut() + /// .unwrap(); + /// + /// tabs[0] = 7; + /// tabs[5] = 8; + /// + /// assert_eq!(&tabs, &[7, 2, 3, 4, 5, 8]); + /// ``` + #[inline] + pub fn tabs_mut(&mut self) -> Option<&mut [Tab]> { + match self { + Node::Leaf { tabs, .. } => Some(tabs), + _ => None, + } + } + + /// Adds `tab` to the node and sets it as the active tab. /// /// # Panics - /// Panics if the new capacity of `tabs` exceeds isize::MAX bytes. + /// + /// If the new capacity of `tabs` exceeds `isize::MAX` bytes. + /// + /// If `self` is not a [`Leaf`](Node::Leaf) node. + /// + /// # Examples + /// + /// ```rust + /// # use egui_dock::{DockState, NodeIndex}; + /// let mut dock_state = DockState::new(vec!["a tab"]); + /// assert_eq!(dock_state.main_surface().root_node().unwrap().tabs_count(), 1); + /// + /// dock_state.main_surface_mut().root_node_mut().unwrap().append_tab("another tab"); + /// assert_eq!(dock_state.main_surface().root_node().unwrap().tabs_count(), 2); + /// ``` #[track_caller] #[inline] pub fn append_tab(&mut self, tab: Tab) { @@ -131,15 +217,15 @@ impl Node { *active = TabIndex(tabs.len()); tabs.push(tab); } - _ => unreachable!(), + _ => panic!("node was not a leaf"), } } /// Adds a `tab` to the node. /// /// # Panics - /// Panics if the new capacity of `tabs` exceeds isize::MAX bytes. - /// index > tabs_count() + /// + /// Panics if the new capacity of `tabs` exceeds `isize::MAX` bytes, or `index > tabs_count()`. #[track_caller] #[inline] pub fn insert_tab(&mut self, index: TabIndex, tab: Tab) { @@ -156,6 +242,7 @@ impl Node { /// Returns the removed tab if the node is a `Leaf`, or `None` otherwise. /// /// # Panics + /// /// Panics if `index` is out of bounds. #[inline] pub fn remove_tab(&mut self, tab_index: TabIndex) -> Option { diff --git a/src/tree/node_index.rs b/src/dock_state/tree/node_index.rs similarity index 62% rename from src/tree/node_index.rs rename to src/dock_state/tree/node_index.rs index 2d815be..32e6a5a 100644 --- a/src/tree/node_index.rs +++ b/src/dock_state/tree/node_index.rs @@ -1,4 +1,6 @@ -/// Wrapper around indices to the collection of nodes inside a [`Tree`](crate::Tree). +use std::ops::Range; + +/// Wrapper around indices to the collection of nodes inside a [`Tree`](crate::Tree). #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct NodeIndex(pub usize); @@ -12,6 +14,17 @@ impl From for NodeIndex { impl NodeIndex { /// Returns the index of the root node. + /// + /// In the context of a [`Tree`](crate::Tree), this will be the node that contains all other nodes. + /// + /// # Examples + /// + /// Splitting the current tree in two. + /// ```rust + /// # use egui_dock::{DockState, NodeIndex}; + /// let mut dock_state = DockState::new(vec!["tab 1", "tab 2"]); + /// let _ = dock_state.main_surface_mut().split_left(NodeIndex::root(), 0.5, vec!["tab 3", "tab 4"]); + /// ``` #[inline(always)] pub const fn root() -> Self { Self(0) @@ -45,20 +58,20 @@ impl NodeIndex { (usize::BITS - (self.0 + 1).leading_zeros()) as usize } - /// Returns true if current node is the left node of its parent, false otherwise. + /// Returns `true` if current node is the left child of its parent, otherwise `false`. #[inline(always)] pub const fn is_left(self) -> bool { self.0 % 2 != 0 } - /// Returns true if current node is the right node of its parent, false otherwise. + /// Returns `true` if current node is the right child of its parent, otherwise `false`. #[inline(always)] pub const fn is_right(self) -> bool { self.0 % 2 == 0 } #[inline] - pub(super) const fn children_at(self, level: usize) -> std::ops::Range { + pub(super) const fn children_at(self, level: usize) -> Range { let base = 1 << level; let s = (self.0 + 1) * base - 1; let e = (self.0 + 2) * base - 1; @@ -66,17 +79,17 @@ impl NodeIndex { } #[inline] - pub(super) const fn children_left(self, level: usize) -> std::ops::Range { + pub(super) const fn children_left(self, level: usize) -> Range { let base = 1 << level; let s = (self.0 + 1) * base - 1; - let e = (self.0 + 1) * base + base / 2 - 1; + let e = (self.0 + 1) * base + (base / 2) - 1; s..e } #[inline] - pub(super) const fn children_right(self, level: usize) -> std::ops::Range { + pub(super) const fn children_right(self, level: usize) -> Range { let base = 1 << level; - let s = (self.0 + 1) * base + base / 2 - 1; + let s = (self.0 + 1) * base + (base / 2) - 1; let e = (self.0 + 2) * base - 1; s..e } diff --git a/src/tree/tab_index.rs b/src/dock_state/tree/tab_index.rs similarity index 100% rename from src/tree/tab_index.rs rename to src/dock_state/tree/tab_index.rs diff --git a/src/tree/tab_iter.rs b/src/dock_state/tree/tab_iter.rs similarity index 89% rename from src/tree/tab_iter.rs rename to src/dock_state/tree/tab_iter.rs index 09f671c..a514db6 100644 --- a/src/tree/tab_iter.rs +++ b/src/dock_state/tree/tab_iter.rs @@ -1,4 +1,4 @@ -use crate::{Node, Tree}; +use crate::Tree; /// Iterates over all tabs in a [`Tree`]. pub struct TabIter<'a, Tab> { @@ -22,8 +22,8 @@ impl<'a, Tab> Iterator for TabIter<'a, Tab> { fn next(&mut self) -> Option { loop { - match self.tree.tree.get(self.node_idx)? { - Node::Leaf { tabs, .. } => match tabs.get(self.tab_idx) { + match self.tree.tree.get(self.node_idx)?.tabs() { + Some(tabs) => match tabs.get(self.tab_idx) { Some(tab) => { self.tab_idx += 1; return Some(tab); @@ -33,7 +33,7 @@ impl<'a, Tab> Iterator for TabIter<'a, Tab> { self.tab_idx = 0; } }, - _ => { + None => { self.node_idx += 1; self.tab_idx = 0; } diff --git a/src/dock_state/window_state.rs b/src/dock_state/window_state.rs new file mode 100644 index 0000000..9e57538 --- /dev/null +++ b/src/dock_state/window_state.rs @@ -0,0 +1,93 @@ +use egui::{Id, Pos2, Rect, Vec2}; + +/// The state of a [`Surface::Window`](crate::Surface::Window). +/// +/// Doubles as a handle for the surface, allowing the user to set its size and position. +#[derive(Debug, Clone)] +pub struct WindowState { + /// The [`Rect`] that this window was last taking up. + screen_rect: Rect, + + /// Was this window dragged in the last frame? + dragged: bool, + + /// The next position this window should be set to next frame. + next_position: Option, + + /// The next size this window should be set to next frame. + next_size: Option, + + /// true the first frame this window is drawn. + /// handles opening collapsing header, etc. + new: bool, +} + +impl Default for WindowState { + fn default() -> Self { + Self { + screen_rect: Rect::NOTHING, + dragged: false, + next_position: None, + next_size: None, + new: true, + } + } +} + +impl WindowState { + /// Create a default window state. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Set the position for this window in screen coordinates. + pub fn set_position(&mut self, position: Pos2) -> &mut Self { + self.next_position = Some(position); + self + } + + /// Set the size of this window in egui points. + pub fn set_size(&mut self, size: Vec2) -> &mut Self { + self.next_size = Some(size); + self + } + + /// Get the [`Rect`] which this window occupies. + /// If this window hasn't been shown before, this will be [`Rect::NOTHING`]. + pub fn rect(&self) -> Rect { + self.screen_rect + } + + /// Returns if this window is currently being dragged or not. + pub fn dragged(&self) -> bool { + self.dragged + } + + #[inline(always)] + pub(crate) fn next_position(&mut self) -> Option { + self.next_position.take() + } + + #[inline(always)] + pub(crate) fn next_size(&mut self) -> Option { + self.next_size.take() + } + + //the 'static in this case means that the `open` field is always `None` + pub(crate) fn create_window(&mut self, id: Id, bounds: Rect) -> (egui::Window<'static>, bool) { + let new = self.new; + let mut window_constructor = egui::Window::new("") + .id(id) + .drag_bounds(bounds) + .title_bar(false); + + if let Some(position) = self.next_position() { + window_constructor = window_constructor.current_pos(position); + } + if let Some(size) = self.next_size() { + window_constructor = window_constructor.fixed_size(size); + } + self.new = false; + (window_constructor, new) + } +} diff --git a/src/lib.rs b/src/lib.rs index 737a645..1fbeff3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,53 +1,68 @@ //! # `egui_dock`: docking support for `egui` //! //! Originally created by [@lain-dono](https://github.com/lain-dono), this library provides docking support for `egui`. -//! It lets you open and close tabs, freely move them around, insert them in selected parts of the [`DockArea`], and resize them. +//! It lets you open and close tabs, freely move them around, resize them, and undock them into new egui windows which +//! can also have other tabs docked in them. //! -//! ## Usage +//! ## Basic usage //! -//! The library is centered around the [`Tree`]. -//! It stores the layout of [`Node`]s which contains tabs. +//! The library is centered around the [`DockState`]. +//! It contains a series of [`Surface`]s which all have their own [`Tree`]. +//! Each [`Tree`] stores a hierarchy of [`Node`]s which contain the splits and tabs. //! -//! [`Tree`] is generic (`Tree`) so you can use any data to represent a tab. -//! You show the tabs using [`DockArea`] and specify how they are shown -//! by implementing [`TabViewer`]. +//! [`DockState`] is generic (`DockState`) so you can use any data to represent a tab. +//! You show the tabs using [`DockArea`] and specify how they are shown by implementing [`TabViewer`]. //! //! ```rust -//! use egui_dock::{NodeIndex, Style, Tree}; +//! use egui_dock::{DockArea, DockState, NodeIndex, Style, TabViewer}; +//! use egui::{Ui, WidgetText}; //! -//! struct MyTabs { -//! tree: Tree -//! } +//! // First, let's pick a type that we'll use to attach some data to each tab. +//! // It can be any type. +//! type Tab = String; //! -//! impl MyTabs { -//! pub fn new() -> Self { -//! let tab1 = "tab1".to_string(); -//! let tab2 = "tab2".to_string(); +//! // To define the contents and properties of individual tabs, we implement the `TabViewer` +//! // trait. Only three things are mandatory: the `Tab` associated type, and the `ui` and +//! // `title` methods. There are more methods in `TabViewer` which you can also override. +//! struct MyTabViewer; //! -//! let mut tree = Tree::new(vec![tab1]); -//! tree.split_left(NodeIndex::root(), 0.20, vec![tab2]); +//! impl TabViewer for MyTabViewer { +//! // This associated type is used to attach some data to each tab. +//! type Tab = Tab; //! -//! Self { tree } +//! // Defines the contents of a given `tab`. +//! fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) { +//! ui.label(format!("Content of {tab}")); //! } //! -//! fn ui(&mut self, ui: &mut egui::Ui) { -//! egui_dock::DockArea::new(&mut self.tree) -//! .style(Style::from_egui(ui.style().as_ref())) -//! .show_inside(ui, &mut TabViewer {}); +//! // Returns the current `tab`'s title. +//! fn title(&mut self, tab: &mut Self::Tab) -> WidgetText { +//! tab.as_str().into() //! } //! } //! -//! struct TabViewer; -//! -//! impl egui_dock::TabViewer for TabViewer { -//! type Tab = String; +//! // Here is a simple example of how you can manage a `DockState` of your application. +//! struct MyTabs { +//! dock_state: DockState +//! } //! -//! fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { -//! ui.label(format!("Content of {tab}")); +//! impl MyTabs { +//! pub fn new() -> Self { +//! // Create a `DockState` with an initial tab "tab1" in the main `Surface`'s root node. +//! let tabs = ["tab1", "tab2", "tab3"].map(str::to_string).into_iter().collect(); +//! let dock_state = DockState::new(tabs); +//! Self { dock_state } //! } //! -//! fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { -//! (&*tab).into() +//! fn ui(&mut self, ui: &mut Ui) { +//! // Here we just display the `DockState` using a `DockArea`. +//! // This is where egui handles rendering and all the integrations. +//! // +//! // We can specify a custom `Style` for the `DockArea`, or just inherit +//! // all of it from egui. +//! DockArea::new(&mut self.dock_state) +//! .style(Style::from_egui(ui.style().as_ref())) +//! .show_inside(ui, &mut MyTabViewer); //! } //! } //! @@ -56,20 +71,134 @@ //! # egui::CentralPanel::default().show(ctx, |ui| my_tabs.ui(ui)); //! # }); //! ``` +//! +//! ## Look and feel customization +//! +//! `egui_dock` exposes the [`Style`] struct that lets you change how tabs and the [`DockArea`] +//! should look and feel. [`Style`] is divided into several, more specialized structs that handle +//! individual elements of the UI. +//! +//! Your [`Style`] can inherit all its properties from an [`egui::Style`] through the +//! [`Style::from_egui`] function. +//! +//! Example: +//! +//! ```rust +//! # use egui_dock::{DockArea, DockState, OverlayType, Style, TabAddAlign, TabViewer}; +//! # use egui::{Ui, WidgetText}; +//! # struct MyTabViewer; +//! # impl TabViewer for MyTabViewer { +//! # type Tab = (); +//! # fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) {} +//! # fn title(&mut self, tab: &mut Self::Tab) -> WidgetText { WidgetText::default() } +//! # } +//! # egui::__run_test_ctx(|ctx| { +//! # egui::CentralPanel::default().show(ctx, |ui| { +//! # let mut dock_state = DockState::new(vec![]); +//! // Inherit the look and feel from egui. +//! let mut style = Style::from_egui(ui.style()); +//! +//! // Modify a few fields. +//! style.overlay.overlay_type = OverlayType::HighlightedAreas; +//! style.buttons.add_tab_align = TabAddAlign::Left; +//! +//! // Use the style with the `DockArea`. +//! DockArea::new(&mut dock_state) +//! .style(style) +//! .show_inside(ui, &mut MyTabViewer); +//! # }); +//! # }); +//! # +//! ``` +//! +//! ## Surfaces +//! +//! A [`Surface`] is an abstraction for any tab hierarchy. There are two kinds of +//! non-empty surfaces: `Main` and `Window`. +//! +//! There can only be one `Main` surface. It's the one surface that is rendered inside the +//! [`Ui`](egui::Ui) you've passed to [`DockArea::show_inside`], or inside the +//! [`egui::CentralPanel`] created by [`DockArea::show`]. +//! +//! On the other hand, there can be multiple `Window` surfaces. Those represent surfaces that were +//! created by undocking tabs from the `Main` surface, and each of them is rendered inside +//! a [`egui::Window`] - hence their name. +//! +//! While most of surface management will be done by the user of your application, you can also do it +//! programatically using the [`DockState`] API. +//! +//! Example: +//! +//! ```rust +//! # use egui_dock::DockState; +//! # use egui::{Pos2, Vec2}; +//! # let mut dock_state = DockState::new(vec![]); +//! // Create a new window `Surface` with one tab inside it. +//! let mut surface_index = dock_state.add_window(vec!["Window Tab".to_string()]); +//! +//! // Access the window state by its surface index and then move and resize it. +//! let window_state = dock_state.get_window_state_mut(surface_index).unwrap(); +//! window_state.set_position(Pos2::ZERO); +//! window_state.set_size(Vec2::splat(100.0)); +//! ``` +//! +//! For more details, see: [`DockState`]. +//! +//! ## Trees +//! +//! In each [`Surface`] there is a [`Tree`] which actually stores the tabs. As the name suggests, +//! tabs and splits are represented with a binary tree. +//! +//! The [`Tree`] API allows you to programatically manipulate the dock layout. +//! +//! Example: +//! +//! ```rust +//! # use egui_dock::{DockState, NodeIndex}; +//! // Create a `DockState` with an initial tab "tab1" in the main `Surface`'s root node. +//! let mut dock_state = DockState::new(vec!["tab1".to_string()]); +//! +//! // Currently, the `DockState` only has one `Surface`: the main one. +//! // Let's get mutable access to add more nodes in it. +//! let surface = dock_state.main_surface_mut(); +//! +//! // Insert "tab2" to the left of "tab1", where the width of "tab2" +//! // is 20% of root node's width. +//! let [_old_node, new_node] = +//! surface.split_left(NodeIndex::root(), 0.20, vec!["tab2".to_string()]); +//! +//! // Insert "tab3" below "tab2" with both tabs having equal size. +//! surface.split_below(new_node, 0.5, vec!["tab3".to_string()]); +//! +//! // The layout will look similar to this: +//! // +--------+--------------------------------+ +//! // | | | +//! // | tab2 | | +//! // | | | +//! // +--------+ tab1 | +//! // | | | +//! // | tab3 | | +//! // | | | +//! // +--------+--------------------------------+ +//! ``` #![warn(missing_docs)] #![forbid(unsafe_code)] -pub use egui; #[allow(deprecated)] +pub use dock_state::*; +pub use egui; pub use style::*; pub use tree::*; pub use widgets::*; -/// egui_dock theme (color, sizes...). +/// The main Structure of the library. +pub mod dock_state; + +/// Look and feel. pub mod style; -pub mod tree; -mod utils; /// Widgets provided by the library. pub mod widgets; + +mod utils; diff --git a/src/style.rs b/src/style.rs index 10f580f..e410f1e 100644 --- a/src/style.rs +++ b/src/style.rs @@ -9,24 +9,56 @@ pub enum TabAddAlign { Right, } -/// Specifies the look and feel of egui_dock. +/// Lets you change how tabs and the [`DockArea`](crate::DockArea) should look and feel. +/// [`Style`] is divided into several, more specialized structs that handle individual +/// elements of the UI. +/// +/// Your [`Style`] can inherit all its properties from an [`egui::Style`] through the +/// [`Style::from_egui`] function. +/// +/// Example: +/// +/// ```rust +/// # use egui_dock::{DockArea, DockState, OverlayType, Style, TabAddAlign, TabViewer}; +/// # use egui::{Ui, WidgetText}; +/// # struct MyTabViewer; +/// # impl TabViewer for MyTabViewer { +/// # type Tab = (); +/// # fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) {} +/// # fn title(&mut self, tab: &mut Self::Tab) -> WidgetText { WidgetText::default() } +/// # } +/// # egui::__run_test_ctx(|ctx| { +/// # egui::CentralPanel::default().show(ctx, |ui| { +/// # let mut dock_state = DockState::new(vec![]); +/// // Inherit the look and feel from egui. +/// let mut style = Style::from_egui(ui.style()); +/// +/// // Modify a few fields. +/// style.overlay.overlay_type = OverlayType::HighlightedAreas; +/// style.buttons.add_tab_align = TabAddAlign::Left; +/// +/// // Use the style with the `DockArea`. +/// DockArea::new(&mut dock_state) +/// .style(style) +/// .show_inside(ui, &mut MyTabViewer); +/// # }); +/// # }); +/// # +/// ``` #[derive(Clone, Debug)] #[allow(missing_docs)] pub struct Style { /// Sets padding to indent from the edges of the window. By `Default` it's `None`. pub dock_area_padding: Option, - /// Sets selection color for the placing area of the tab where this tab targeted on it. - /// By `Default` it's `(0, 191, 255)` (light blue) with `0.5` capacity. - pub selection_color: Color32, - - pub border: Stroke, - pub rounding: Rounding, + pub main_surface_border_stroke: Stroke, + pub main_surface_border_rounding: Rounding, pub buttons: ButtonsStyle, pub separator: SeparatorStyle, pub tab_bar: TabBarStyle, pub tab: TabStyle, + pub overlay: OverlayStyle, } /// Specifies the look and feel of buttons. @@ -93,7 +125,7 @@ pub struct TabBarStyle { /// Show a scroll bar when tab bar overflows. By `Default` it's `true`. pub show_scroll_bar_on_overflow: bool, - /// Tab rounding. By `Default` it's [`Rounding::default`] + /// Tab rounding. By `Default` it's [`Rounding::default`]. pub rounding: Rounding, /// Color of th line separating the tab name area from the tab content area. @@ -140,10 +172,10 @@ pub struct TabInteractionStyle { /// Color of the outline around tabs. By `Default` it's [`Color32::BLACK`]. pub outline_color: Color32, - /// Tab rounding. By `Default` it's [`Rounding::default`] + /// Tab rounding. By `Default` it's [`Rounding::default`]. pub rounding: Rounding, - /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`] + /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`]. pub bg_fill: Color32, /// Color of the title text. @@ -153,30 +185,116 @@ pub struct TabInteractionStyle { /// Specifies the look and feel of the tab body. #[derive(Clone, Debug)] pub struct TabBodyStyle { - /// Inner margin of tab body. By `Default` it's `Margin::same(4.0)` + /// Inner margin of tab body. By `Default` it's `Margin::same(4.0)`. pub inner_margin: Margin, - /// The stroke of the tabs border. By `Default` it's ['Stroke::default'] + /// The stroke of the tabs border. By `Default` it's ['Stroke::default']. pub stroke: Stroke, - /// Tab rounding. By `Default` it's [`Rounding::default`] + /// Tab rounding. By `Default` it's [`Rounding::default`]. pub rounding: Rounding, - /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`] + /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`]. pub bg_fill: Color32, } +/// Specifies the look and feel of the tab drop overlay. +#[derive(Clone, Debug)] +pub struct OverlayStyle { + /// Sets selection color for the placing area of the tab where this tab targeted on it. + /// By `Default` it's `(0, 191, 255)` (light blue) with `0.5` capacity. + pub selection_color: Color32, + + /// Width of stroke when a selection uses an outline instead of filled rectangle. + pub selection_storke_width: f32, + + /// Units of padding between each button. + pub button_spacing: f32, + + /// Max side length of a button on the overlay. + pub max_button_size: f32, + + /// Style of the additional highlighting rectangle drawn on the surface which you're attempting to drop a tab in. + /// + /// By default this value shows no highlighting. + pub hovered_leaf_highlight: LeafHighlighting, + + /// Opacity which surfaces will fade to in a range of `0.0..=1.0`. + pub surface_fade_opacity: f32, + + /// The color of the overlay buttons. + pub button_color: Color32, + + /// The stroke of the button border. + pub button_border_stroke: Stroke, + + /// The type of overlay used. + pub overlay_type: OverlayType, + + /// The feel of the overlay, timings, detection, etc. + pub feel: OverlayFeel, +} + +/// Specifies the feel of the tab drop overlay, i.e anything non visual about the overlay. +#[derive(Clone, Debug)] +pub struct OverlayFeel { + /// range is `0.0..=1.0`. + pub window_drop_coverage: f32, + + /// range is `0.0..=1.0`. + pub center_drop_coverage: f32, + + /// The amount of time windows should stay faded despite not needing to, prevents quick mouse movements from causing flashing. + pub fade_hold_time: f32, + + /// Amount of time the overlay waits before dropping a preference it may have for a node. + pub max_preference_time: f32, + + /// Units which the buttons interact area will be expanded by. + pub interact_expansion: f32, +} + +/// Specifies the type of overlay used. +#[derive(Clone, Debug, PartialEq)] +pub enum OverlayType { + /// Shows highlighted areas predicting where a dropped tab would land were it to be dropped this frame. + /// + /// Always used when hovering over tabs and tab head. + HighlightedAreas, + + /// Shows icons indicating the possible drop positions which the user may hover over to drop a tab at that given location. + /// + /// This is the default type of overlay for leaves. + Widgets, +} + +/// Highlighting on the currently hovered leaf. +#[derive(Clone, Debug)] +pub struct LeafHighlighting { + /// Fill color. + pub color: Color32, + + /// Rounding of the resulting rectangle. + pub rounding: Rounding, + + /// Stroke. + pub stroke: Stroke, + + /// Amount of egui units which each side should expand. + pub expansion: f32, +} + impl Default for Style { fn default() -> Self { Self { dock_area_padding: None, - border: Stroke::new(f32::default(), Color32::BLACK), - rounding: Rounding::default(), - selection_color: Color32::from_rgb(0, 191, 255).linear_multiply(0.5), + main_surface_border_stroke: Stroke::new(f32::default(), Color32::BLACK), + main_surface_border_rounding: Rounding::default(), buttons: ButtonsStyle::default(), separator: SeparatorStyle::default(), tab_bar: TabBarStyle::default(), tab: TabStyle::default(), + overlay: OverlayStyle::default(), } } } @@ -268,6 +386,48 @@ impl Default for TabBodyStyle { } } +impl Default for OverlayStyle { + fn default() -> Self { + Self { + selection_color: Color32::from_rgb(0, 191, 255).linear_multiply(0.5), + selection_storke_width: 1.0, + button_spacing: 10.0, + max_button_size: 100.0, + + surface_fade_opacity: 0.1, + + hovered_leaf_highlight: Default::default(), + button_color: Color32::from_gray(140), + button_border_stroke: Stroke::new(1.0, Color32::from_gray(60)), + overlay_type: OverlayType::Widgets, + feel: Default::default(), + } + } +} + +impl Default for OverlayFeel { + fn default() -> Self { + Self { + max_preference_time: 0.3, + window_drop_coverage: 0.5, + center_drop_coverage: 0.25, + fade_hold_time: 0.2, + interact_expansion: 20.0, + } + } +} + +impl Default for LeafHighlighting { + fn default() -> Self { + Self { + color: Color32::TRANSPARENT, + rounding: Rounding::same(0.0), + stroke: Stroke::NONE, + expansion: 0.0, + } + } +} + impl Style { pub(crate) const TAB_ADD_BUTTON_SIZE: f32 = 24.0; pub(crate) const TAB_ADD_PLUS_SIZE: f32 = 12.0; @@ -279,20 +439,19 @@ impl Style { /// Derives relevant fields from `egui::Style` and sets the remaining fields to their default values. /// /// Fields overwritten by [`egui::Style`] are: - /// - [`Style::border`] - /// - [`Style::selection_color`] + /// - [`Style::main_surface_border_stroke`] /// /// See also: [`ButtonsStyle::from_egui`], [`SeparatorStyle::from_egui`], [`TabBarStyle::from_egui`], /// [`TabStyle::from_egui`] pub fn from_egui(style: &egui::Style) -> Self { Self { - border: Stroke::NONE, - rounding: Rounding::none(), - selection_color: style.visuals.selection.bg_fill.linear_multiply(0.5), + main_surface_border_stroke: Stroke::NONE, + main_surface_border_rounding: Rounding::none(), buttons: ButtonsStyle::from_egui(style), separator: SeparatorStyle::from_egui(style), tab_bar: TabBarStyle::from_egui(style), tab: TabStyle::from_egui(style), + overlay: OverlayStyle::from_egui(style), ..Self::default() } } @@ -399,6 +558,7 @@ impl TabInteractionStyle { }, } } + /// Derives relevant fields from `egui::Style` for an inactive tab and sets the remaining fields to their default values. /// /// Fields overwritten by [`egui::Style`] are: @@ -468,3 +628,22 @@ impl TabBodyStyle { } } } + +impl OverlayStyle { + /// Derives relevant fields from `egui::Style` and sets the remaining fields to their default values. + /// + /// Fields overwritten by [`egui::Style`] are: + /// - [`OverlayStyle::selection_color`] + /// - [`OverlayStyle::button_spacing] + /// - [`OverlayStyle::button_color`] + /// - [`OverlayStyle::button_border_stroke`] + pub fn from_egui(style: &egui::Style) -> Self { + Self { + selection_color: style.visuals.selection.bg_fill.linear_multiply(0.5), + button_spacing: style.spacing.icon_spacing, + button_color: style.visuals.widgets.noninteractive.fg_stroke.color, + button_border_stroke: style.visuals.widgets.noninteractive.bg_stroke, + ..Default::default() + } + } +} diff --git a/src/utils.rs b/src/utils.rs index fac6447..a7099ac 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,10 @@ use egui::emath::*; +use crate::{ + ButtonsStyle, SeparatorStyle, Style, TabBarStyle, TabBodyStyle, TabInteractionStyle, TabStyle, +}; +use egui::style::{Visuals, WidgetVisuals, Widgets}; + #[inline(always)] pub fn expand_to_pixel(mut rect: Rect, ppi: f32) -> Rect { rect.min = map_to_pixel_pos(rect.min, ppi, f32::floor); @@ -31,3 +36,86 @@ pub fn rect_set_size_centered(rect: &mut Rect, size: Vec2) { pub fn rect_stroke_box(rect: Rect, width: f32) -> Rect { rect.expand(-f32::ceil(width / 2.0)) } + +/// Fade a `egui_dock::Style` to a certain opacity +pub(super) fn fade_dock_style(style: &mut Style, factor: f32) { + style.main_surface_border_stroke.color = style + .main_surface_border_stroke + .color + .linear_multiply(factor); + fade_tab_style(&mut style.tab, factor); + fade_button_style(&mut style.buttons, factor); + fade_seperator_style(&mut style.separator, factor); + fade_tab_bar_style(&mut style.tab_bar, factor); +} + +fn fade_tab_bar_style(style: &mut TabBarStyle, factor: f32) { + style.hline_color = style.hline_color.linear_multiply(factor); + style.bg_fill = style.bg_fill.linear_multiply(factor); +} + +fn fade_seperator_style(style: &mut SeparatorStyle, factor: f32) { + style.color_idle = style.color_idle.linear_multiply(factor); + style.color_hovered = style.color_hovered.linear_multiply(factor); + style.color_dragged = style.color_dragged.linear_multiply(factor); +} + +fn fade_button_style(style: &mut ButtonsStyle, factor: f32) { + style.close_tab_color = style.close_tab_color.linear_multiply(factor); + style.close_tab_active_color = style.close_tab_active_color.linear_multiply(factor); + style.close_tab_bg_fill = style.close_tab_bg_fill.linear_multiply(factor); + style.add_tab_color = style.add_tab_color.linear_multiply(factor); + style.add_tab_active_color = style.add_tab_active_color.linear_multiply(factor); + style.add_tab_bg_fill = style.add_tab_bg_fill.linear_multiply(factor); + style.add_tab_border_color = style.add_tab_border_color.linear_multiply(factor); +} + +fn fade_tab_style(style: &mut TabStyle, factor: f32) { + fade_tab_interaction_style(&mut style.active, factor); + fade_tab_interaction_style(&mut style.inactive, factor); + fade_tab_interaction_style(&mut style.focused, factor); + fade_tab_interaction_style(&mut style.hovered, factor); + fade_tab_body_style(&mut style.tab_body, factor); +} + +fn fade_tab_interaction_style(style: &mut TabInteractionStyle, factor: f32) { + style.outline_color = style.outline_color.linear_multiply(factor); + style.bg_fill = style.bg_fill.linear_multiply(factor); + style.text_color = style.text_color.linear_multiply(factor); +} + +fn fade_tab_body_style(style: &mut TabBodyStyle, factor: f32) { + style.stroke.color = style.stroke.color.linear_multiply(factor); + style.bg_fill = style.bg_fill.linear_multiply(factor); +} + +/// Fade a `egui::style::Visuals` to a certain opacity +pub(super) fn fade_visuals(visuals: &mut Visuals, factor: f32) { + if let Some(override_text_color) = &mut visuals.override_text_color { + *override_text_color = override_text_color.linear_multiply(factor); + } + visuals.hyperlink_color = visuals.hyperlink_color.linear_multiply(factor); + visuals.faint_bg_color = visuals.faint_bg_color.linear_multiply(factor); + visuals.extreme_bg_color = visuals.extreme_bg_color.linear_multiply(factor); + visuals.code_bg_color = visuals.code_bg_color.linear_multiply(factor); + visuals.warn_fg_color = visuals.warn_fg_color.linear_multiply(factor); + visuals.error_fg_color = visuals.error_fg_color.linear_multiply(factor); + visuals.window_fill = visuals.window_fill.linear_multiply(factor); + visuals.panel_fill = visuals.window_fill.linear_multiply(factor); + fade_widgets(&mut visuals.widgets, factor); +} + +fn fade_widgets(widgets: &mut Widgets, factor: f32) { + fade_widget_visuals(&mut widgets.noninteractive, factor); + fade_widget_visuals(&mut widgets.inactive, factor); + fade_widget_visuals(&mut widgets.hovered, factor); + fade_widget_visuals(&mut widgets.active, factor); + fade_widget_visuals(&mut widgets.open, factor); +} + +fn fade_widget_visuals(visuals: &mut WidgetVisuals, factor: f32) { + visuals.bg_fill = visuals.bg_fill.linear_multiply(factor); + visuals.weak_bg_fill = visuals.weak_bg_fill.linear_multiply(factor); + visuals.bg_stroke.color = visuals.bg_stroke.color.linear_multiply(factor); + visuals.fg_stroke.color = visuals.fg_stroke.color.linear_multiply(factor); +} diff --git a/src/widgets/dock_area/allowed_splits.rs b/src/widgets/dock_area/allowed_splits.rs new file mode 100644 index 0000000..4f72198 --- /dev/null +++ b/src/widgets/dock_area/allowed_splits.rs @@ -0,0 +1,38 @@ +/// What directions can this dock be split in? +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum AllowedSplits { + #[default] + /// Allow splits in any direction (horizontal and vertical). + All = 0b11, + + /// Only allow split in a horizontal directions. + LeftRightOnly = 0b10, + + /// Only allow splits in a vertical directions. + TopBottomOnly = 0b01, + + /// Don't allow splits at all. + None = 0b00, +} + +impl std::ops::BitAnd for AllowedSplits { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self::from_u8(self as u8 & rhs as u8) + } +} + +impl AllowedSplits { + /// Create allowed splits from a u8, panics if an invalid value is given. + #[inline(always)] + fn from_u8(u8: u8) -> Self { + match u8 { + 0b11 => AllowedSplits::All, + 0b10 => AllowedSplits::LeftRightOnly, + 0b01 => AllowedSplits::TopBottomOnly, + 0b00 => AllowedSplits::None, + _ => panic!("Provided an invalid value for allowed splits: {u8:0x}"), + } + } +} diff --git a/src/widgets/dock_area/drag_and_drop.rs b/src/widgets/dock_area/drag_and_drop.rs new file mode 100644 index 0000000..fe80fed --- /dev/null +++ b/src/widgets/dock_area/drag_and_drop.rs @@ -0,0 +1,470 @@ +use std::{ + ops::BitOrAssign, + time::{Duration, Instant}, +}; + +use crate::{ + AllowedSplits, NodeIndex, Split, Style, SurfaceIndex, TabDestination, TabIndex, TabInsert, +}; +use egui::{ + emath::inverse_lerp, vec2, Context, Id, LayerId, NumExt, Order, Pos2, Rect, Stroke, Ui, Vec2, +}; + +#[derive(Debug, Clone)] +pub(super) struct HoverData { + /// Rect of the hovered element. + pub rect: Rect, + + /// The "address" of the tab/node being hovered over. + pub dst: TreeComponent, + + /// If a tab title or the tab head is hovered, this is the rect of it. + pub tab: Option, +} + +/// Specifies the location of a tab on the tree, used when moving tabs. +#[derive(Debug, Clone)] +pub(super) struct DragData { + pub src: TreeComponent, + pub rect: Rect, +} + +#[derive(Debug, Clone)] +pub(super) enum TreeComponent { + Surface(SurfaceIndex), + Node(SurfaceIndex, NodeIndex), + Tab(SurfaceIndex, NodeIndex, TabIndex), +} + +impl TreeComponent { + pub(super) fn as_tab_destination(&self) -> TabDestination { + match *self { + TreeComponent::Surface(surface) => TabDestination::EmptySurface(surface), + TreeComponent::Node(dst_surf, dst_node) => { + TabDestination::Node(dst_surf, dst_node, TabInsert::Append) + } + TreeComponent::Tab(dst_surf, dst_node, tab_index) => { + TabDestination::Node(dst_surf, dst_node, TabInsert::Insert(tab_index)) + } + } + } + + pub(super) fn node_address(&self) -> (SurfaceIndex, Option) { + match *self { + TreeComponent::Surface(surface) => (surface, None), + TreeComponent::Node(dst_surf, dst_node) => (dst_surf, Some(dst_node)), + TreeComponent::Tab(dst_surf, dst_node, _) => (dst_surf, Some(dst_node)), + } + } + + pub(super) fn surface_address(&self) -> SurfaceIndex { + match *self { + TreeComponent::Surface(surface) + | TreeComponent::Node(surface, _) + | TreeComponent::Tab(surface, _, _) => surface, + } + } + + pub(super) fn is_surface(&self) -> bool { + matches!(self, TreeComponent::Surface(_)) + } +} + +fn draw_highlight_rect(rect: Rect, ui: &Ui, style: &Style) { + ui.painter().rect( + rect.expand(style.overlay.hovered_leaf_highlight.expansion), + style.overlay.hovered_leaf_highlight.rounding, + style.overlay.hovered_leaf_highlight.color, + style.overlay.hovered_leaf_highlight.stroke, + ) +} + +// Draws one of the Tab drop destination icons inside `rect`, which one you get is specified by `is_top_bottom`. +fn button_ui( + rect: Rect, + ui: &Ui, + lock: &mut bool, + mouse_pos: Pos2, + style: &Style, + split: Option, +) -> bool { + let visuals = &style.overlay; + let button_stroke = Stroke::new(1.0, visuals.button_color); + let painter = ui.painter(); + painter.rect_stroke(rect, 0.0, visuals.button_border_stroke); + let rect = rect.shrink(rect.width() * 0.1); + painter.rect_stroke(rect, 0.0, button_stroke); + let rim = { Rect::from_two_pos(rect.min, rect.lerp_inside(vec2(1.0, 0.1))) }; + painter.rect(rim, 0.0, visuals.button_color, Stroke::NONE); + + if let Some(split) = split { + for line in DASHED_LINE_ALPHAS.chunks(2) { + let start = rect.lerp_inside(lerp_vec(split, line[0])); + let end = rect.lerp_inside(lerp_vec(split, line[1])); + painter.line_segment([start, end], button_stroke); + } + } + let is_mouse_over = rect + .expand(style.overlay.feel.interact_expansion) + .contains(mouse_pos); + if is_mouse_over && !*lock { + let vertical_alphas = vec2(1.0, 0.5); + let horizontal_alphas = vec2(0.5, 1.0); + let rect = match split { + Some(Split::Above) => Rect::from_min_size(rect.min, rect.size() * vertical_alphas), + Some(Split::Left) => Rect::from_min_size(rect.min, rect.size() * horizontal_alphas), + Some(Split::Below) => { + let min = rect.lerp_inside(lerp_vec(Split::Below, 0.0)); + Rect::from_min_size(min, rect.size() * vertical_alphas) + } + Some(Split::Right) => { + let min = rect.lerp_inside(lerp_vec(Split::Right, 0.0)); + Rect::from_min_size(min, rect.size() * horizontal_alphas) + } + _ => rect, + }; + painter.rect_filled(rect, 0.0, style.overlay.selection_color); + } + lock.bitor_assign(is_mouse_over); + is_mouse_over +} + +const DASHED_LINE_ALPHAS: [f32; 8] = [ + 0.0625, 0.1875, 0.3125, 0.4375, 0.5625, 0.6875, 0.8125, 0.9375, +]; + +#[derive(PartialEq, Eq)] +enum LockState { + /// Lock is unlocked. + Unlocked, + + /// Lock remains locked, but can be unlocked. + SoftLock, + + /// Lock is locked forever. + HardLock, +} + +#[derive(Debug, Clone)] +pub(super) struct DragDropState { + pub hover: HoverData, + pub drag: DragData, + pub pointer: Pos2, + /// Is some when the pointer is over rect, instant holds when the lock was last active. + pub locked: Option, +} + +impl DragDropState { + // Determines if the hover data implies we're hovering over a tab or the tab title bar. + pub(super) fn is_on_title_bar(&self) -> bool { + self.hover.tab.is_some() + } + + pub(super) fn resolve_icon_based( + &mut self, + ui: &Ui, + style: &Style, + allowed_splits: AllowedSplits, + windows_allowed: bool, + window_bounds: Rect, + ) -> Option { + assert!(!self.is_on_title_bar()); + + draw_highlight_rect(self.hover.rect, ui, style); + let mut hovering_buttons = false; + let total_button_spacing = style.overlay.button_spacing * 2.0; + let (rect, pointer) = (self.hover.rect, self.pointer); + let rect = rect.shrink(style.overlay.button_spacing); + let shortest_side = ((rect.width() - total_button_spacing) / 3.0) + .min((rect.height() - total_button_spacing) / 3.0) + .min(style.overlay.max_button_size); + let mut offset_vector = vec2(0.0, shortest_side + style.overlay.button_spacing); + + let mut destination: Option = match windows_allowed { + true => Some(TabDestination::Window(Rect::from_min_size( + pointer, + self.drag.rect.size(), + ))), + false => None, + }; + + let center = rect.center(); + let rect = Rect::from_center_size(center, Vec2::splat(shortest_side)); + + if button_ui(rect, ui, &mut hovering_buttons, pointer, style, None) { + match self.hover.dst { + TreeComponent::Node(surface, node) => { + destination = Some(TabDestination::Node(surface, node, TabInsert::Append)) + } + TreeComponent::Surface(surface) => { + destination = Some(TabDestination::EmptySurface(surface)) + } + _ => (), + } + } + + for split in [Split::Below, Split::Right, Split::Above, Split::Left] { + match allowed_splits { + AllowedSplits::TopBottomOnly if split.is_top_bottom() => continue, + AllowedSplits::LeftRightOnly if split.is_left_right() => continue, + AllowedSplits::None => continue, + _ => { + if button_ui( + Rect::from_center_size(center + offset_vector, Vec2::splat(shortest_side)), + ui, + &mut hovering_buttons, + pointer, + style, + Some(split), + ) { + if let TreeComponent::Node(surface, node) = self.hover.dst { + destination = + Some(TabDestination::Node(surface, node, TabInsert::Split(split))) + } + } + offset_vector = offset_vector.rot90(); + } + } + } + let hovering_rect = self.hover.rect.contains(pointer); + let target_lock_state = match (hovering_rect, hovering_buttons) { + (false, false) => LockState::Unlocked, + (_, true) => LockState::HardLock, + (true, _) => LockState::SoftLock, + }; + self.update_lock(target_lock_state, style, ui.ctx()); + if let Some(TabDestination::Window(rect)) = destination { + let rect = self.window_preview_rect(rect); + let rect_bounded = constrain_rect_to_area(ui, rect, window_bounds); + draw_window_rect(rect_bounded, ui, style); + } + destination + } + + pub(super) fn resolve_traditional( + &mut self, + ui: &Ui, + style: &Style, + allowed_splits: AllowedSplits, + windows_allowed: bool, + window_bounds: Rect, + ) -> Option { + // If windows are not allowed, any hover over a window is immediately disallowed. + if !windows_allowed && self.hover.dst.surface_address() != SurfaceIndex::main() { + return None; + } + draw_highlight_rect(self.hover.rect, ui, style); + + // Deals with hovers over tab bar and tab titles. + if let Some(rect) = self.hover.tab { + draw_drop_rect(rect, ui, style); + let target_lock_state = if rect.contains(self.pointer) { + LockState::SoftLock + } else { + LockState::Unlocked + }; + self.update_lock(target_lock_state, style, ui.ctx()); + return Some(self.hover.dst.as_tab_destination()); + } + + // Main cases, splits, window creations, etc. + let (hover_rect, pointer) = (self.hover.rect, self.pointer); + let center = hover_rect.center(); + + let (tab_insertion, overlay_rect) = { + // A reverse lerp of the pointers position relative to the hovered leaf rect. + // Range is (-0.5, -0.5) to (0.5, 0.5) + let a_pos = (Pos2::new( + inverse_lerp(hover_rect.x_range(), pointer.x).unwrap(), + inverse_lerp(hover_rect.y_range(), pointer.y).unwrap(), + ) - Pos2::new(0.5, 0.5)) + .to_pos2(); + + let center_drop_rect = Rect::from_center_size( + Pos2::ZERO, + Vec2::splat(style.overlay.feel.center_drop_coverage), + ); + let window_drop_rect = Rect::from_center_size( + Pos2::ZERO, + Vec2::splat(style.overlay.feel.window_drop_coverage), + ); + + // Find out what kind of tab insertion (if any) should be used to move this widget. + if center_drop_rect.contains(a_pos) { + (Some(TabInsert::Append), Rect::EVERYTHING) + } else if window_drop_rect.contains(a_pos) { + match windows_allowed { + true => (None, Rect::NOTHING), + false => (Some(TabInsert::Append), Rect::EVERYTHING), + } + } else { + // Assessing if were above/below the two linear functions x-y=0 and -x-y=0 determines + // what "diagonal" quadrant were in. + let a_pos = match allowed_splits { + AllowedSplits::All => a_pos, + AllowedSplits::LeftRightOnly => Pos2::new(a_pos.x, 0.0), + AllowedSplits::TopBottomOnly => Pos2::new(0.0, a_pos.y), + AllowedSplits::None => Pos2::ZERO, + }; + if a_pos == Pos2::ZERO { + match windows_allowed { + true => (None, Rect::NOTHING), + false => (Some(TabInsert::Append), Rect::EVERYTHING), + } + } else { + match (a_pos.x - a_pos.y > 0., -a_pos.x - a_pos.y > 0.) { + (true, true) => ( + Some(TabInsert::Split(Split::Above)), + Rect::everything_above(center.y), + ), + (false, true) => ( + Some(TabInsert::Split(Split::Left)), + Rect::everything_left_of(center.x), + ), + (true, false) => ( + Some(TabInsert::Split(Split::Right)), + Rect::everything_right_of(center.x), + ), + (false, false) => ( + Some(TabInsert::Split(Split::Below)), + Rect::everything_below(center.y), + ), + } + } + } + }; + + let default_value = windows_allowed + .then(|| TabDestination::Window(Rect::from_min_size(pointer, self.drag.rect.size()))); + let final_result = tab_insertion.map_or(default_value, |tab| match self.hover.dst { + TreeComponent::Surface(surface) => Some(TabDestination::EmptySurface(surface)), + TreeComponent::Node(surface, node) => Some(TabDestination::Node(surface, node, tab)), + _ => None, + }); + + self.update_lock(LockState::SoftLock, style, ui.ctx()); + + //Draw the overlay + match final_result { + Some(TabDestination::Window(rect)) => { + let rect = self.window_preview_rect(rect); + let rect_bounded = constrain_rect_to_area(ui, rect, window_bounds); + draw_window_rect(rect_bounded, ui, style); + } + Some(_) => { + draw_drop_rect(hover_rect.intersect(overlay_rect), ui, style); + } + None => (), + } + + final_result + } + + fn update_lock(&mut self, target_state: LockState, style: &Style, ctx: &Context) { + match self.locked.as_mut() { + Some(lock_time) => { + if target_state == LockState::HardLock { + *lock_time = Instant::now(); + } + let window_hold = if !self.hover.dst.surface_address().is_main() { + ctx.request_repaint(); + self.is_locked(style, ctx) + } else { + false + }; + if target_state == LockState::Unlocked && !window_hold { + self.locked = None; + } + } + None => { + if target_state != LockState::Unlocked { + self.locked = Some(Instant::now()); + } + } + } + } + + pub(super) fn is_locked(&self, style: &Style, ctx: &Context) -> bool { + match self.locked.as_ref() { + Some(lock_time) => { + let elapsed = lock_time.elapsed().as_secs_f32(); + ctx.request_repaint_after(Duration::from_secs_f32( + (style.overlay.feel.max_preference_time - elapsed).max(0.0), + )); + elapsed < style.overlay.feel.max_preference_time + } + None => false, + } + } + + fn window_preview_rect(&self, rect: Rect) -> Rect { + if self.drag.src.surface_address() == SurfaceIndex::main() { + Rect::from_min_size(rect.min, rect.size() * 0.8) + } else { + rect + } + } +} + +#[inline(always)] +const fn lerp_vec(split: Split, alpha: f32) -> Vec2 { + if split.is_top_bottom() { + vec2(alpha, 0.5) + } else { + vec2(0.5, alpha) + } +} + +// Draws a filled rect describing where a tab will be dropped. +#[inline(always)] +fn draw_drop_rect(rect: Rect, ui: &Ui, style: &Style) { + let id = Id::new("overlay"); + let layer_id = LayerId::new(Order::Foreground, id); + let painter = ui.ctx().layer_painter(layer_id); + painter.rect_filled(rect, 0.0, style.overlay.selection_color); +} + +// Draws a stroked rect describing where a tab will be dropped. +#[inline(always)] +fn draw_window_rect(rect: Rect, ui: &Ui, style: &Style) { + let id = Id::new("overlay"); + let layer_id = LayerId::new(Order::Foreground, id); + let painter = ui.ctx().layer_painter(layer_id); + painter.rect_stroke( + rect, + 0.0, + Stroke::new( + style.overlay.selection_storke_width, + style.overlay.selection_color, + ), + ); +} + +/// An adapted version of the [`egui::Area`]s code for restricting an area rect to a bound. +fn constrain_rect_to_area(ui: &Ui, rect: Rect, mut bounds: Rect) -> Rect { + if rect.width() > bounds.width() { + // Allow overlapping side bars. + let screen_rect = ui.ctx().screen_rect(); + (bounds.min.x, bounds.max.x) = (screen_rect.min.x, screen_rect.max.x); + } + if rect.height() > bounds.height() { + // Allow overlapping top/bottom bars: + let screen_rect = ui.ctx().screen_rect(); + (bounds.min.y, bounds.max.y) = (screen_rect.min.y, screen_rect.max.y); + } + + let mut pos = rect.min; + + // Constrain to screen, unless window is too large to fit: + let margin_x = (rect.width() - bounds.width()).at_least(0.0); + let margin_y = (rect.height() - bounds.height()).at_least(0.0); + + pos.x = pos.x.at_most(bounds.right() + margin_x - rect.width()); // move left if needed + pos.x = pos.x.at_least(bounds.left() - margin_x); // move right if needed + pos.y = pos.y.at_most(bounds.bottom() + margin_y - rect.height()); // move right if needed + pos.y = pos.y.at_least(bounds.top() - margin_y); // move down if needed + + pos = ui.painter().round_pos_to_pixels(pos); + + Rect::from_min_size(pos, rect.size()) +} diff --git a/src/widgets/dock_area/hover_data.rs b/src/widgets/dock_area/hover_data.rs deleted file mode 100644 index 38a8013..0000000 --- a/src/widgets/dock_area/hover_data.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::{AllowedSplits, NodeIndex, Split, TabDestination, TabIndex}; -use egui::{Pos2, Rect}; - -#[derive(Debug)] -pub(super) struct HoverData { - pub rect: Rect, - pub tabs: Option, - pub tab: Option<(Rect, TabIndex)>, - pub dst: NodeIndex, - pub pointer: Pos2, -} - -impl HoverData { - pub(super) fn resolve(&self, allowed_splits: &AllowedSplits) -> (Rect, TabDestination) { - if let Some(tab) = self.tab { - return (tab.0, TabDestination::Insert(tab.1)); - } - if let Some(tabs) = self.tabs { - return (tabs, TabDestination::Append); - } - - let (rect, pointer) = (self.rect, self.pointer); - - let center = rect.center(); - - let pts = match allowed_splits { - AllowedSplits::All => vec![ - ( - center.distance(pointer), - TabDestination::Append, - Rect::EVERYTHING, - ), - ( - rect.left_center().distance(pointer), - TabDestination::Split(Split::Left), - Rect::everything_left_of(center.x), - ), - ( - rect.right_center().distance(pointer), - TabDestination::Split(Split::Right), - Rect::everything_right_of(center.x), - ), - ( - rect.center_top().distance(pointer), - TabDestination::Split(Split::Above), - Rect::everything_above(center.y), - ), - ( - rect.center_bottom().distance(pointer), - TabDestination::Split(Split::Below), - Rect::everything_below(center.y), - ), - ], - AllowedSplits::LeftRightOnly => vec![ - ( - center.distance(pointer), - TabDestination::Append, - Rect::EVERYTHING, - ), - ( - rect.left_center().distance(pointer), - TabDestination::Split(Split::Left), - Rect::everything_left_of(center.x), - ), - ( - rect.right_center().distance(pointer), - TabDestination::Split(Split::Right), - Rect::everything_right_of(center.x), - ), - ], - AllowedSplits::TopBottomOnly => vec![ - ( - rect.center_top().distance(pointer), - TabDestination::Split(Split::Above), - Rect::everything_above(center.y), - ), - ( - rect.center_bottom().distance(pointer), - TabDestination::Split(Split::Below), - Rect::everything_below(center.y), - ), - ], - AllowedSplits::None => vec![( - center.distance(pointer), - TabDestination::Append, - Rect::EVERYTHING, - )], - }; - - let (_, tab_dst, overlay) = pts - .into_iter() - .min_by(|(lhs, ..), (rhs, ..)| lhs.total_cmp(rhs)) - .unwrap(); - - (rect.intersect(overlay), tab_dst) - } -} diff --git a/src/widgets/dock_area/mod.rs b/src/widgets/dock_area/mod.rs index 9560d14..799fa69 100644 --- a/src/widgets/dock_area/mod.rs +++ b/src/widgets/dock_area/mod.rs @@ -1,41 +1,25 @@ -mod hover_data; +/// Due to there being a lot of code to show a dock in a ui every complementing +/// method to ``show`` and ``show_inside`` is put in ``show_extra``. +/// Otherwise ``mod.rs`` would be humongous. +mod show; + +// Various components of the `DockArea` which is used when rendering +mod allowed_splits; +mod drag_and_drop; mod state; +mod tab_removal; -use std::ops::RangeInclusive; +use crate::{dock_state::DockState, NodeIndex, Style, SurfaceIndex, TabIndex}; +pub use allowed_splits::AllowedSplits; +use drag_and_drop::{DragData, HoverData}; +use tab_removal::TabRemoval; -use crate::{ - utils::{expand_to_pixel, map_to_pixel, rect_set_size_centered, rect_stroke_box}, - widgets::popup::popup_under_widget, - Node, NodeIndex, Style, TabAddAlign, TabIndex, TabStyle, TabViewer, Tree, -}; +use egui::{emath::*, Id}; -use duplicate::duplicate; -use egui::{ - containers::*, emath::*, epaint::*, layers::*, Context, CursorIcon, Id, Layout, Response, - Sense, TextStyle, Ui, WidgetText, -}; -use hover_data::HoverData; -use paste::paste; -use state::State; - -/// What directions can this dock split in? -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum AllowedSplits { - #[default] - /// Allow splits in any direction (horizontal and vertical). - All, - /// Only allow split in a horizontal direction. - LeftRightOnly, - /// Only allow splits in a vertical direction. - TopBottomOnly, - /// Don't allow splits at all. - None, -} - -/// Displays a [`Tree`] in `egui`. +/// Displays a [`DockState`] in `egui`. pub struct DockArea<'tree, Tab> { id: Id, - tree: &'tree mut Tree, + dock_state: &'tree mut DockState, style: Option