1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Remove dock stuff

This commit is contained in:
Emil Ernerfeldt
2023-05-08 12:03:27 +02:00
parent 1799713fb0
commit abd8cefb78
14 changed files with 0 additions and 2699 deletions

16
Cargo.lock generated
View File

@@ -1128,17 +1128,6 @@ dependencies = [
"libloading 0.7.4",
]
[[package]]
name = "dock"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"log",
"serde",
]
[[package]]
name = "document-features"
version = "0.2.7"
@@ -1330,11 +1319,8 @@ dependencies = [
"chrono",
"document-features",
"egui",
"getrandom",
"image",
"itertools",
"log",
"rand",
"resvg",
"serde",
"tiny-skia",
@@ -1706,10 +1692,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]

View File

@@ -36,13 +36,7 @@ svg = ["resvg", "tiny-skia", "usvg"]
[dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false }
# For dock:
# required to make rand work on wasm, see https://github.com/rust-random/rand#wasm-support
getrandom = { version = "0.2", features = ["js"] }
itertools = "0.10"
log = { version = "0.4", features = ["std"] }
rand = { version = "0.8.5", features = ["getrandom", "small_rng"] }
serde = { version = "1", features = ["derive"] }
#! ### Optional dependencies

View File

@@ -1,209 +0,0 @@
use egui::{
vec2, Color32, Id, Rect, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText,
};
use super::{Node, NodeId, Nodes, ResizeState, SimplificationOptions, UiResponse};
/// Trait defining how the [`Dock`] and its leaf should be shown.
pub trait Behavior<Leaf> {
/// Show this leaf node in the given [`egui::Ui`].
///
/// If this is an unknown node, return [`NodeAction::Remove`] and the node will be removed.
///
/// You can make the leaf draggable by returning [`UiResponse::DragStarted`]
/// when the user drags some handle.
fn leaf_ui(&mut self, _ui: &mut Ui, _node_id: NodeId, _leaf: &mut Leaf) -> UiResponse;
/// The title of a leaf tab.
fn tab_title_for_leaf(&mut self, leaf: &Leaf) -> WidgetText;
/// The title of a general tab.
///
/// The default implementation uses the name of the layout for branches, and
/// calls [`Self::tab_text_for_leaf`] for leaves.
fn tab_title_for_node(&mut self, nodes: &Nodes<Leaf>, node_id: NodeId) -> WidgetText {
if let Some(node) = nodes.nodes.get(&node_id) {
match node {
Node::Leaf(leaf) => self.tab_title_for_leaf(leaf),
Node::Branch(branch) => format!("{:?}", branch.layout()).into(),
}
} else {
"MISSING NODE".into()
}
}
/// Show the title of a tab as a button.
///
/// You can override the default implementation to add e.g. a close button.
fn tab_ui(
&mut self,
nodes: &Nodes<Leaf>,
ui: &mut Ui,
id: Id,
node_id: NodeId,
active: bool,
is_being_dragged: bool,
) -> Response {
let text = self.tab_title_for_node(nodes, node_id);
let font_id = TextStyle::Button.resolve(ui.style());
let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
let x_margin = self.tab_title_spacing(ui.visuals());
let (_, rect) = ui.allocate_space(vec2(
galley.size().x + 2.0 * x_margin,
ui.available_height(),
));
let response = ui.interact(rect, id, Sense::click_and_drag());
// Show a gap when dragged
if ui.is_rect_visible(rect) && !is_being_dragged {
let bg_color = self.tab_bg_color(ui.visuals(), node_id, active);
let stroke = self.tab_outline_stroke(ui.visuals(), node_id, active);
ui.painter().rect(rect.shrink(0.5), 0.0, bg_color, stroke);
if active {
// Make the tab name area connect with the tab ui area:
ui.painter().hline(
rect.x_range(),
rect.bottom(),
Stroke::new(stroke.width + 1.0, bg_color),
);
}
let text_color = self.tab_text_color(ui.visuals(), node_id, active);
ui.painter().galley_with_color(
egui::Align2::CENTER_CENTER
.align_size_within_rect(galley.size(), rect)
.min,
galley.galley,
text_color,
);
}
response
}
/// Returns `false` if this leaf should be removed from its parent.
fn retain_leaf(&mut self, _leaf: &Leaf) -> bool {
true
}
/// Adds some UI to the top right of each tab bar.
///
/// You can use this to, for instance, add a button for adding new tabs.
///
/// The widgets will be added right-to-left.
fn top_bar_rtl_ui(&mut self, _ui: &mut Ui, _node_id: NodeId) {
// if ui.button("").clicked() {
// }
}
// --------
// Settings:
/// The height of the bar holding tab titles.
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
24.0
}
/// Width of the gap between nodes in a horizontal or vertical layout,
/// and between rows/columns in a grid layout.
fn gap_width(&self, _style: &egui::Style) -> f32 {
1.0
}
/// No child should shrink below this width nor height.
fn min_size(&self) -> f32 {
32.0
}
/// What are the rules for simplifying the tree?
fn simplification_options(&self) -> SimplificationOptions {
SimplificationOptions::default()
}
/// The stroke used for the lines in horizontal, vertical, and grid layouts.
fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> egui::Stroke {
match resize_state {
ResizeState::Idle => egui::Stroke::NONE, // Let the gap speak for itself
ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke,
ResizeState::Dragging => style.visuals.widgets.active.fg_stroke,
}
}
/// Extra spacing to left and right of tab titles.
fn tab_title_spacing(&self, _visuals: &Visuals) -> f32 {
8.0
}
/// The background color of the tab bar.
fn tab_bar_color(&self, visuals: &Visuals) -> Color32 {
if visuals.dark_mode {
Color32::BLACK
} else {
(Rgba::from(visuals.window_fill()) * Rgba::from_gray(0.8)).into()
}
}
/// The background color of a tab.
fn tab_bg_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 {
if active {
visuals.window_fill() // same as the tab contents
} else {
Color32::TRANSPARENT // fade into background
}
}
/// Stroke of the outline around a tab title.
fn tab_outline_stroke(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Stroke {
if active {
Stroke::new(1.0, visuals.widgets.active.bg_fill)
} else {
Stroke::NONE
}
}
/// Stroke of the line separating the tab title bar and the content of the active tab.
fn tab_bar_hline_stroke(&self, visuals: &Visuals) -> Stroke {
Stroke::new(1.0, visuals.widgets.noninteractive.bg_stroke.color)
}
/// The color of the title text of the tab.
fn tab_text_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 {
if active {
visuals.widgets.active.text_color()
} else {
visuals.widgets.noninteractive.text_color()
}
}
/// When drag-and-dropping a node, how do we preview what is about to happen?
fn paint_drag_preview(
&self,
visuals: &Visuals,
painter: &egui::Painter,
parent_rect: Option<Rect>,
preview_rect: Rect,
) {
let preview_stroke = visuals.selection.stroke;
let preview_color = preview_stroke.color;
if let Some(parent_rect) = parent_rect {
// Show which parent we will be dropped into
painter.rect_stroke(parent_rect, 1.0, preview_stroke);
}
painter.rect(
preview_rect,
1.0,
preview_color.gamma_multiply(0.5),
preview_stroke,
);
}
/// Show we preview leaves that are being dragged,
/// i.e. show their ui in the region where they will end up?
fn preview_dragged_leaves(&self) -> bool {
false
}
}

View File

@@ -1,442 +0,0 @@
use std::collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet};
use egui::{emath::Rangef, pos2, vec2, NumExt as _, Rect};
use itertools::Itertools as _;
use crate::dock::{
Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes, ResizeState,
};
/// Where in a grid?
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
)]
pub struct GridLoc {
// Row first for sorting
pub row: usize,
pub col: usize,
}
impl GridLoc {
#[inline]
pub fn from_col_row(col: usize, row: usize) -> Self {
Self { col, row }
}
}
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
)]
pub enum GridLayout {
/// Place children in a grid, with a dynamic number of columns and rows.
/// Resizing the window may change the number of columns and rows.
#[default]
Auto,
/// Place children in a grid with this many columns,
/// and as many rows as needed.
Columns(usize),
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Grid {
pub children: Vec<NodeId>,
pub layout: GridLayout,
/// Where each child is located.
///
/// If the chils is missing from this set, it will be assgined a location during layout.
pub locations: HashMap<NodeId, GridLoc>,
/// Share of the available width assigned to each column.
pub col_shares: Vec<f32>,
/// Share of the available height assigned to each row.
pub row_shares: Vec<f32>,
/// ui point x ranges for each column, recomputed during layout
#[serde(skip)]
col_ranges: Vec<Rangef>,
/// ui point y ranges for each row, recomputed during layout
#[serde(skip)]
row_ranges: Vec<Rangef>,
}
impl Grid {
pub fn new(children: Vec<NodeId>) -> Self {
Self {
children,
..Default::default()
}
}
pub fn add_child(&mut self, child: NodeId) {
self.children.push(child);
}
pub fn layout<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
let gap = behavior.gap_width(style);
let child_ids: HashSet<NodeId> = self.children.iter().copied().collect();
let num_cols = match self.layout {
GridLayout::Auto => num_columns_heuristic(self.children.len(), rect, gap),
GridLayout::Columns(num_columns) => num_columns.at_least(1),
};
let num_rows = (self.children.len() + num_cols - 1) / num_cols;
// Where to place each node?
let mut node_id_from_location: BTreeMap<GridLoc, NodeId> = Default::default();
self.locations.retain(|&child_id, &mut loc| {
if child_ids.contains(&child_id) {
match node_id_from_location.entry(loc) {
btree_map::Entry::Occupied(_) => {
false // two nodes assigned to the same position - forget this one for now
}
btree_map::Entry::Vacant(entry) => {
if num_cols <= loc.col || num_rows <= loc.row {
false // out of bounds
} else {
entry.insert(child_id);
true
}
}
}
} else {
false // child no longer exists
}
});
// Find location for nodes that don't have one yet
let mut next_pos = 0;
for &child_id in &self.children {
if let hash_map::Entry::Vacant(entry) = self.locations.entry(child_id) {
// find a position:
loop {
let loc = GridLoc::from_col_row(next_pos % num_cols, next_pos / num_cols);
if node_id_from_location.contains_key(&loc) {
next_pos += 1;
continue;
}
entry.insert(loc);
node_id_from_location.insert(loc, child_id);
break;
}
}
}
// Everything has a location - now we know how many rows we have:
let num_rows = node_id_from_location.keys().last().unwrap().row + 1;
// Figure out where each column and row goes:
self.col_shares.resize(num_cols, 1.0);
self.row_shares.resize(num_rows, 1.0);
let col_widths = sizes_from_shares(&self.col_shares, rect.width(), gap);
let row_heights = sizes_from_shares(&self.row_shares, rect.height(), gap);
{
let mut x = rect.left();
self.col_ranges.clear();
for &width in &col_widths {
self.col_ranges.push(Rangef::new(x, x + width));
x += width + gap;
}
}
{
let mut y = rect.top();
self.row_ranges.clear();
for &height in &row_heights {
self.row_ranges.push(Rangef::new(y, y + height));
y += height + gap;
}
}
// Each child now has a location. Use this to order them, in case we will ater do auto-layouts:
self.children.sort_by_key(|&child| self.locations[&child]);
// Place each child:
for &child in &self.children {
let loc = self.locations[&child];
let child_rect =
Rect::from_x_y_ranges(self.col_ranges[loc.col], self.row_ranges[loc.row]);
nodes.layout_node(style, behavior, child_rect, child);
}
}
pub(super) fn ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
node_id: NodeId,
) {
for &child in &self.children {
nodes.node_ui(behavior, drop_context, ui, child);
}
// Register drop-zones:
for (col, &x_range) in self.col_ranges.iter().enumerate() {
for (row, &y_range) in self.row_ranges.iter().enumerate() {
let cell_rect = Rect::from_x_y_ranges(x_range, y_range);
drop_context.suggest_rect(
InsertionPoint::new(
node_id,
LayoutInsertion::Grid(GridLoc::from_col_row(col, row)),
),
cell_rect,
);
}
}
self.resize_columns(nodes, behavior, ui, node_id);
self.resize_rows(nodes, behavior, ui, node_id);
}
fn resize_columns<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
ui: &mut egui::Ui,
parent_id: NodeId,
) {
let parent_rect = nodes.rect(parent_id);
for (i, (left, right)) in self.col_ranges.iter().copied().tuple_windows().enumerate() {
let resize_id = egui::Id::new((parent_id, "resize_col", i));
let x = egui::lerp(left.max..=right.min, 0.5);
let mut resize_state = ResizeState::Idle;
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let line_rect = Rect::from_center_size(
pos2(x, parent_rect.center().y),
vec2(
2.0 * ui.style().interaction.resize_grab_radius_side,
parent_rect.height(),
),
);
let response = ui.interact(line_rect, resize_id, egui::Sense::click_and_drag());
resize_state = resize_interaction(
behavior,
&self.col_ranges,
&mut self.col_shares,
&response,
ui.painter().round_to_pixel(pointer.x) - x,
i,
);
if resize_state != ResizeState::Idle {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
}
}
let stroke = behavior.resize_stroke(ui.style(), resize_state);
ui.painter().vline(x, parent_rect.y_range(), stroke);
}
}
fn resize_rows<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
ui: &mut egui::Ui,
parent_id: NodeId,
) {
let parent_rect = nodes.rect(parent_id);
for (i, (top, bottom)) in self.row_ranges.iter().copied().tuple_windows().enumerate() {
let resize_id = egui::Id::new((parent_id, "resize_row", i));
let y = egui::lerp(top.max..=bottom.min, 0.5);
let mut resize_state = ResizeState::Idle;
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let line_rect = Rect::from_center_size(
pos2(parent_rect.center().x, y),
vec2(
parent_rect.width(),
2.0 * ui.style().interaction.resize_grab_radius_side,
),
);
let response = ui.interact(line_rect, resize_id, egui::Sense::click_and_drag());
resize_state = resize_interaction(
behavior,
&self.row_ranges,
&mut self.row_shares,
&response,
ui.painter().round_to_pixel(pointer.y) - y,
i,
);
if resize_state != ResizeState::Idle {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
}
}
let stroke = behavior.resize_stroke(ui.style(), resize_state);
ui.painter().hline(parent_rect.x_range(), y, stroke);
}
}
}
/// How many columns should we use to fit `n` children in a grid?
fn num_columns_heuristic(n: usize, rect: Rect, gap: f32) -> usize {
let desired_aspect = 4.0 / 3.0;
let mut best_loss = f32::INFINITY;
let mut best_num_columns = 1;
for ncols in 1..=n {
let nrows = (n + ncols - 1) / ncols;
let cell_width = (rect.width() - gap * (ncols as f32 - 1.0)) / (ncols as f32);
let cell_height = (rect.height() - gap * (nrows as f32 - 1.0)) / (nrows as f32);
let cell_aspect = cell_width / cell_height;
let aspect_diff = (desired_aspect - cell_aspect).abs();
let num_empty_cells = ncols * nrows - n;
let loss = aspect_diff + 0.1 * num_empty_cells as f32; // TODO(emilk): weight differently?
if loss < best_loss {
best_loss = loss;
best_num_columns = ncols;
}
}
best_num_columns
}
fn resize_interaction<Leaf>(
behavior: &mut dyn Behavior<Leaf>,
ranges: &[Rangef],
shares: &mut [f32],
splitter_response: &egui::Response,
dx: f32,
i: usize,
) -> ResizeState {
assert_eq!(ranges.len(), shares.len());
let num = ranges.len();
let node_width = |i: usize| ranges[i].span();
let left = i;
let right = i + 1;
if splitter_response.double_clicked() {
// double-click to center the split between left and right:
let mean = 0.5 * (shares[left] + shares[right]);
shares[left] = mean;
shares[right] = mean;
ResizeState::Hovering
} else if splitter_response.dragged() {
if dx < 0.0 {
// Expand right, shrink stuff to the left:
shares[right] += shrink_shares(
behavior,
shares,
&(0..=i).rev().collect_vec(),
dx.abs(),
node_width,
);
} else {
// Expand the left, shrink stuff to the right:
shares[left] += shrink_shares(
behavior,
shares,
&(i + 1..num).collect_vec(),
dx.abs(),
node_width,
);
}
ResizeState::Dragging
} else if splitter_response.hovered() {
ResizeState::Hovering
} else {
ResizeState::Idle
}
}
/// Try shrink the children by a total of `target_in_points`,
/// making sure no child gets smaller than its minimum size.
fn shrink_shares<Leaf>(
behavior: &dyn Behavior<Leaf>,
shares: &mut [f32],
children: &[usize],
target_in_points: f32,
size_in_point: impl Fn(usize) -> f32,
) -> f32 {
if children.is_empty() {
return 0.0;
}
let mut total_shares = 0.0;
let mut total_points = 0.0;
for &child in children {
total_shares += shares[child];
total_points += size_in_point(child);
}
let shares_per_point = total_shares / total_points;
let min_size_in_points = shares_per_point * behavior.min_size();
let target_in_shares = shares_per_point * target_in_points;
let mut total_shares_lost = 0.0;
for &child in children {
let share = &mut shares[child];
let shrink_by = (target_in_shares - total_shares_lost)
.min(*share - min_size_in_points)
.max(0.0);
*share -= shrink_by;
total_shares_lost += shrink_by;
}
total_shares_lost
}
fn sizes_from_shares(shares: &[f32], available_size: f32, gap_width: f32) -> Vec<f32> {
if shares.is_empty() {
return vec![];
}
let available_size = available_size - gap_width * (shares.len() - 1) as f32;
let available_size = available_size.at_least(0.0);
let total_share: f32 = shares.iter().sum();
if total_share <= 0.0 {
vec![available_size / shares.len() as f32; shares.len()]
} else {
shares
.iter()
.map(|&share| share / total_share * available_size)
.collect()
}
}

View File

@@ -1,471 +0,0 @@
use std::collections::HashMap;
use egui::{pos2, vec2, NumExt, Rect};
use itertools::Itertools as _;
use crate::dock::{
is_being_dragged, Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes,
ResizeState,
};
// ----------------------------------------------------------------------------
/// How large of a share of space each child has, on a 1D axis.
///
/// Used for [`Linear`] layouts (horizontal and vertical).
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Shares {
/// How large of a share each child has.
///
/// For instance, the shares `[1, 2, 3]` means that the first child gets 1/6 of the space,
/// the second gets 2/6 and the third gets 3/6.
shares: HashMap<NodeId, f32>,
}
impl Shares {
pub fn replace_with(&mut self, remove: NodeId, new: NodeId) {
if let Some(share) = self.shares.remove(&remove) {
self.shares.insert(new, share);
}
}
/// Split the given width based on the share of the children.
pub fn split(&self, children: &[NodeId], available_width: f32) -> Vec<f32> {
let mut num_shares = 0.0;
for &child in children {
num_shares += self[child];
}
if num_shares == 0.0 {
num_shares = 1.0;
}
children
.iter()
.map(|&child| available_width * self[child] / num_shares)
.collect()
}
}
impl std::ops::Index<NodeId> for Shares {
type Output = f32;
#[inline]
fn index(&self, id: NodeId) -> &Self::Output {
self.shares.get(&id).unwrap_or(&1.0)
}
}
impl std::ops::IndexMut<NodeId> for Shares {
#[inline]
fn index_mut(&mut self, id: NodeId) -> &mut Self::Output {
self.shares.entry(id).or_insert(1.0)
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum LinearDir {
#[default]
Horizontal,
Vertical,
}
/// Horizontal or vertical layout.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Linear {
pub children: Vec<NodeId>,
pub dir: LinearDir,
pub shares: Shares,
}
impl Linear {
pub fn new(dir: LinearDir, children: Vec<NodeId>) -> Self {
Self {
children,
dir,
..Default::default()
}
}
pub fn add_child(&mut self, child: NodeId) {
self.children.push(child);
}
pub fn layout<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
match self.dir {
LinearDir::Horizontal => {
self.layout_horizontal(nodes, style, behavior, rect);
}
LinearDir::Vertical => self.layout_vertical(nodes, style, behavior, rect),
}
}
fn layout_horizontal<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
let num_gaps = self.children.len().saturating_sub(1);
let gap_width = behavior.gap_width(style);
let total_gap_width = gap_width * num_gaps as f32;
let available_width = (rect.width() - total_gap_width).at_least(0.0);
let widths = self.shares.split(&self.children, available_width);
let mut x = rect.min.x;
for (child, width) in self.children.iter().zip(widths) {
let child_rect = Rect::from_min_size(pos2(x, rect.min.y), vec2(width, rect.height()));
nodes.layout_node(style, behavior, child_rect, *child);
x += width + gap_width;
}
}
fn layout_vertical<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
let num_gaps = self.children.len().saturating_sub(1);
let gap_height = behavior.gap_width(style);
let total_gap_height = gap_height * num_gaps as f32;
let available_height = (rect.height() - total_gap_height).at_least(0.0);
let heights = self.shares.split(&self.children, available_height);
let mut y = rect.min.y;
for (child, height) in self.children.iter().zip(heights) {
let child_rect = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), height));
nodes.layout_node(style, behavior, child_rect, *child);
y += height + gap_height;
}
}
pub(super) fn ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
node_id: NodeId,
) {
match self.dir {
LinearDir::Horizontal => self.horizontal_ui(nodes, behavior, drop_context, ui, node_id),
LinearDir::Vertical => self.vertical_ui(nodes, behavior, drop_context, ui, node_id),
}
}
fn horizontal_ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
parent_id: NodeId,
) {
for &child in &self.children {
if !is_being_dragged(ui.ctx(), child) {
nodes.node_ui(behavior, drop_context, ui, child);
}
}
linear_drop_zones(ui.ctx(), nodes, &self.children, self.dir, |rect, i| {
drop_context.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(i)),
rect,
);
});
// ------------------------
// resizing:
let parent_rect = nodes.rect(parent_id);
for (i, (left, right)) in self.children.iter().copied().tuple_windows().enumerate() {
let resize_id = egui::Id::new((parent_id, "resize", i));
let left_rect = nodes.rect(left);
let right_rect = nodes.rect(right);
let x = egui::lerp(left_rect.right()..=right_rect.left(), 0.5);
let mut resize_state = ResizeState::Idle;
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let line_rect = Rect::from_center_size(
pos2(x, parent_rect.center().y),
vec2(
2.0 * ui.style().interaction.resize_grab_radius_side,
parent_rect.height(),
),
);
let response = ui.interact(line_rect, resize_id, egui::Sense::click_and_drag());
resize_state = resize_interaction(
behavior,
&mut self.shares,
&self.children,
&response,
[left, right],
ui.painter().round_to_pixel(pointer.x) - x,
i,
|node_id: NodeId| nodes.rect(node_id).width(),
);
if resize_state != ResizeState::Idle {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
}
}
let stroke = behavior.resize_stroke(ui.style(), resize_state);
ui.painter().vline(x, parent_rect.y_range(), stroke);
}
}
fn vertical_ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
parent_id: NodeId,
) {
for &child in &self.children {
if !is_being_dragged(ui.ctx(), child) {
nodes.node_ui(behavior, drop_context, ui, child);
}
}
linear_drop_zones(ui.ctx(), nodes, &self.children, self.dir, |rect, i| {
drop_context.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Vertical(i)),
rect,
);
});
// ------------------------
// resizing:
let parent_rect = nodes.rect(parent_id);
for (i, (top, bottom)) in self.children.iter().copied().tuple_windows().enumerate() {
let resize_id = egui::Id::new((parent_id, "resize", i));
let top_rect = nodes.rect(top);
let bottom_rect = nodes.rect(bottom);
let y = egui::lerp(top_rect.bottom()..=bottom_rect.top(), 0.5);
let mut resize_state = ResizeState::Idle;
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let line_rect = Rect::from_center_size(
pos2(parent_rect.center().x, y),
vec2(
parent_rect.width(),
2.0 * ui.style().interaction.resize_grab_radius_side,
),
);
let response = ui.interact(line_rect, resize_id, egui::Sense::click_and_drag());
resize_state = resize_interaction(
behavior,
&mut self.shares,
&self.children,
&response,
[top, bottom],
ui.painter().round_to_pixel(pointer.y) - y,
i,
|node_id: NodeId| nodes.rect(node_id).height(),
);
if resize_state != ResizeState::Idle {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
}
}
let stroke = behavior.resize_stroke(ui.style(), resize_state);
ui.painter().hline(parent_rect.x_range(), y, stroke);
}
}
}
#[allow(clippy::too_many_arguments)]
fn resize_interaction<Leaf>(
behavior: &mut dyn Behavior<Leaf>,
shares: &mut Shares,
children: &[NodeId],
splitter_response: &egui::Response,
[left, right]: [NodeId; 2],
dx: f32,
i: usize,
node_width: impl Fn(NodeId) -> f32,
) -> ResizeState {
if splitter_response.double_clicked() {
// double-click to center the split between left and right:
let mean = 0.5 * (shares[left] + shares[right]);
shares[left] = mean;
shares[right] = mean;
ResizeState::Hovering
} else if splitter_response.dragged() {
if dx < 0.0 {
// Expand right, shrink stuff to the left:
shares[right] += shrink_shares(
behavior,
shares,
&children[0..=i].iter().copied().rev().collect_vec(),
dx.abs(),
node_width,
);
} else {
// Expand the left, shrink stuff to the right:
shares[left] +=
shrink_shares(behavior, shares, &children[i + 1..], dx.abs(), node_width);
}
ResizeState::Dragging
} else if splitter_response.hovered() {
ResizeState::Hovering
} else {
ResizeState::Idle
}
}
/// Try shrink the children by a total of `target_in_points`,
/// making sure no child gets smaller than its minimum size.
fn shrink_shares<Leaf>(
behavior: &dyn Behavior<Leaf>,
shares: &mut Shares,
children: &[NodeId],
target_in_points: f32,
size_in_point: impl Fn(NodeId) -> f32,
) -> f32 {
if children.is_empty() {
return 0.0;
}
let mut total_shares = 0.0;
let mut total_points = 0.0;
for &child in children {
total_shares += shares[child];
total_points += size_in_point(child);
}
let shares_per_point = total_shares / total_points;
let min_size_in_points = shares_per_point * behavior.min_size();
let target_in_shares = shares_per_point * target_in_points;
let mut total_shares_lost = 0.0;
for &child in children {
let share = &mut shares[child];
let shrink_by = (target_in_shares - total_shares_lost)
.min(*share - min_size_in_points)
.max(0.0);
*share -= shrink_by;
total_shares_lost += shrink_by;
}
total_shares_lost
}
fn linear_drop_zones<Leaf>(
egui_ctx: &egui::Context,
nodes: &Nodes<Leaf>,
children: &[NodeId],
dir: LinearDir,
add_drop_drect: impl FnMut(Rect, usize),
) {
let preview_thickness = 12.0;
let dragged_index = children
.iter()
.position(|&child| is_being_dragged(egui_ctx, child));
let after_rect = |rect: Rect| match dir {
LinearDir::Horizontal => Rect::from_min_max(
rect.right_top() - vec2(preview_thickness, 0.0),
rect.right_bottom(),
),
LinearDir::Vertical => Rect::from_min_max(
rect.left_bottom() - vec2(0.0, preview_thickness),
rect.right_bottom(),
),
};
drop_zones(
preview_thickness,
children,
dragged_index,
dir,
|node_id| nodes.rect(node_id),
add_drop_drect,
after_rect,
);
}
/// Register drop-zones for a linear layout.
pub(super) fn drop_zones(
preview_thickness: f32,
children: &[NodeId],
dragged_index: Option<usize>,
dir: LinearDir,
get_rect: impl Fn(NodeId) -> Rect,
mut add_drop_drect: impl FnMut(Rect, usize),
after_rect: impl Fn(Rect) -> Rect,
) {
let before_rect = |rect: Rect| match dir {
LinearDir::Horizontal => Rect::from_min_max(
rect.left_top(),
rect.left_bottom() + vec2(preview_thickness, 0.0),
),
LinearDir::Vertical => Rect::from_min_max(
rect.left_top(),
rect.right_top() + vec2(0.0, preview_thickness),
),
};
let between_rects = |a: Rect, b: Rect| match dir {
LinearDir::Horizontal => Rect::from_center_size(
a.right_center().lerp(b.left_center(), 0.5),
vec2(preview_thickness, a.height()),
),
LinearDir::Vertical => Rect::from_center_size(
a.center_bottom().lerp(b.center_top(), 0.5),
vec2(a.width(), preview_thickness),
),
};
let mut prev_rect: Option<Rect> = None;
let mut insertion_index = 0; // skips over drag-source, if any, because it will be removed before its re-inserted
for (i, &child) in children.iter().enumerate() {
let rect = get_rect(child);
if Some(i) == dragged_index {
// Suggest hole as a drop-target:
add_drop_drect(rect, i);
} else {
if let Some(prev_rect) = prev_rect {
if Some(i - 1) != dragged_index {
// Suggest dropping between the rects:
add_drop_drect(between_rects(prev_rect, rect), insertion_index);
}
} else {
// Suggest dropping before the first child:
add_drop_drect(before_rect(rect), 0);
}
insertion_index += 1;
}
prev_rect = Some(rect);
}
if let Some(last_rect) = prev_rect {
// Suggest dropping after the last child (unless that's the one being dragged):
if dragged_index != Some(children.len() - 1) {
add_drop_drect(after_rect(last_rect), insertion_index + 1);
}
}
}

View File

@@ -1,188 +0,0 @@
use egui::Rect;
use super::{Behavior, DropContext, NodeId, Nodes, SimplifyAction};
mod grid;
mod linear;
mod tabs;
pub use grid::{Grid, GridLoc};
pub use linear::{Linear, LinearDir, Shares};
pub use tabs::Tabs;
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Layout {
#[default]
Tabs,
Horizontal,
Vertical,
Grid,
}
impl Layout {
pub const ALL: [Self; 4] = [Self::Tabs, Self::Horizontal, Self::Vertical, Self::Grid];
}
// ----------------------------------------------------------------------------
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum Branch {
Tabs(Tabs),
Linear(Linear),
Grid(Grid),
}
impl Branch {
pub fn new_linear(dir: LinearDir, children: Vec<NodeId>) -> Self {
Self::Linear(Linear::new(dir, children))
}
pub fn new_tabs(children: Vec<NodeId>) -> Self {
Self::Tabs(Tabs::new(children))
}
pub fn new_grid(children: Vec<NodeId>) -> Self {
Self::Grid(Grid::new(children))
}
pub fn is_empty(&self) -> bool {
self.children().is_empty()
}
pub fn children(&self) -> &[NodeId] {
match self {
Self::Tabs(tabs) => &tabs.children,
Self::Linear(linear) => &linear.children,
Self::Grid(grid) => &grid.children,
}
}
pub fn add_child(&mut self, child: NodeId) {
match self {
Self::Tabs(tabs) => tabs.add_child(child),
Self::Linear(linear) => linear.add_child(child),
Self::Grid(grid) => grid.add_child(child),
}
}
pub fn layout(&self) -> Layout {
match self {
Self::Tabs(_) => Layout::Tabs,
Self::Linear(linear) => match linear.dir {
LinearDir::Horizontal => Layout::Horizontal,
LinearDir::Vertical => Layout::Vertical,
},
Self::Grid(_) => Layout::Grid,
}
}
pub fn set_layout(&mut self, layout: Layout) {
if layout == self.layout() {
return;
}
*self = match layout {
Layout::Tabs => Self::Tabs(Tabs::new(self.children().to_vec())),
Layout::Horizontal => {
Self::Linear(Linear::new(LinearDir::Horizontal, self.children().to_vec()))
}
Layout::Vertical => {
Self::Linear(Linear::new(LinearDir::Vertical, self.children().to_vec()))
}
Layout::Grid => Self::Grid(Grid::new(self.children().to_vec())),
};
}
pub(super) fn retain(&mut self, mut retain: impl FnMut(NodeId) -> bool) {
let retain = |node_id: &NodeId| retain(*node_id);
match self {
Self::Tabs(tabs) => tabs.children.retain(retain),
Self::Linear(linear) => linear.children.retain(retain),
Self::Grid(grid) => grid.children.retain(retain),
}
}
pub(super) fn simplify_children(&mut self, mut simplify: impl FnMut(NodeId) -> SimplifyAction) {
match self {
Self::Tabs(tabs) => tabs.children.retain_mut(|child| match simplify(*child) {
SimplifyAction::Remove => false,
SimplifyAction::Keep => true,
SimplifyAction::Replace(new) => {
if tabs.active == *child {
tabs.active = new;
}
*child = new;
true
}
}),
Self::Linear(linear) => linear.children.retain_mut(|child| match simplify(*child) {
SimplifyAction::Remove => false,
SimplifyAction::Keep => true,
SimplifyAction::Replace(new) => {
linear.shares.replace_with(*child, new);
*child = new;
true
}
}),
Self::Grid(grid) => grid.children.retain_mut(|child| match simplify(*child) {
SimplifyAction::Remove => false,
SimplifyAction::Keep => true,
SimplifyAction::Replace(new) => {
if let Some(loc) = grid.locations.remove(child) {
grid.locations.insert(new, loc);
}
*child = new;
true
}
}),
}
}
}
impl Branch {
pub(super) fn layout_recursive<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
if self.is_empty() {
return;
}
match self {
Branch::Tabs(tabs) => tabs.layout(nodes, style, behavior, rect),
Branch::Linear(linear) => {
linear.layout(nodes, style, behavior, rect);
}
Branch::Grid(grid) => grid.layout(nodes, style, behavior, rect),
}
}
}
impl Branch {
pub(super) fn ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
rect: Rect,
node_id: NodeId,
) {
match self {
Branch::Tabs(tabs) => {
tabs.ui(nodes, behavior, drop_context, ui, rect, node_id);
}
Branch::Linear(linear) => {
linear.ui(nodes, behavior, drop_context, ui, node_id);
}
Branch::Grid(grid) => {
grid.ui(nodes, behavior, drop_context, ui, node_id);
}
}
}
}

View File

@@ -1,164 +0,0 @@
use std::collections::HashMap;
use egui::{vec2, Rect};
use crate::dock::{
is_being_dragged, Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes,
};
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Tabs {
/// The tabs, in order.
pub children: Vec<NodeId>,
/// The currenlty open tab.
pub active: NodeId,
}
impl Tabs {
pub fn new(children: Vec<NodeId>) -> Self {
let active = children.first().copied().unwrap_or_default();
Self { children, active }
}
pub fn add_child(&mut self, child: NodeId) {
self.children.push(child);
}
pub fn set_active(&mut self, child: NodeId) {
self.active = child;
}
pub(super) fn layout<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
) {
if !self.children.iter().any(|&child| child == self.active) {
// Make sure something is active:
self.active = self.children.first().copied().unwrap_or_default();
}
let mut active_rect = rect;
active_rect.min.y += behavior.tab_bar_height(style);
// Only lay out the active tab (saves CPU):
nodes.layout_node(style, behavior, active_rect, self.active);
}
pub(super) fn ui<Leaf>(
&mut self,
nodes: &mut Nodes<Leaf>,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
rect: Rect,
node_id: NodeId,
) {
let next_active = self.tab_bar_ui(behavior, ui, rect, nodes, drop_context, node_id);
// When dragged, don't show it (it is "being held")
let is_active_being_dragged = is_being_dragged(ui.ctx(), self.active);
if !is_active_being_dragged {
nodes.node_ui(behavior, drop_context, ui, self.active);
}
// We have only laid out the active tab, so we need to switch active tab _after_ the ui pass above:
self.active = next_active;
}
/// Returns the next active tab (e.g. the one clicked, or the current).
fn tab_bar_ui<Leaf>(
&self,
behavior: &mut dyn Behavior<Leaf>,
ui: &mut egui::Ui,
rect: Rect,
nodes: &mut Nodes<Leaf>,
drop_context: &mut DropContext,
node_id: NodeId,
) -> NodeId {
let mut next_active = self.active;
let tab_bar_height = behavior.tab_bar_height(ui.style());
let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0;
let mut ui = ui.child_ui(tab_bar_rect, *ui.layout());
let mut button_rects = HashMap::new();
let mut dragged_index = None;
ui.painter()
.rect_filled(ui.max_rect(), 0.0, behavior.tab_bar_color(ui.visuals()));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// Add buttons such as "add new tab"
behavior.top_bar_rtl_ui(ui, node_id);
ui.spacing_mut().item_spacing.x = 0.0; // Tabs have spacing built-in
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_clip_rect(ui.max_rect()); // Don't cover the `rtl_ui` buttons.
for (i, &child_id) in self.children.iter().enumerate() {
let is_being_dragged = is_being_dragged(ui.ctx(), child_id);
let selected = child_id == self.active;
let id = child_id.id();
let response =
behavior.tab_ui(nodes, ui, id, child_id, selected, is_being_dragged);
let response = response.on_hover_cursor(egui::CursorIcon::Grab);
if response.clicked() {
next_active = child_id;
}
if let Some(mouse_pos) = drop_context.mouse_pos {
if drop_context.dragged_node_id.is_some()
&& response.rect.contains(mouse_pos)
{
// Expand this tab - maybe the user wants to drop something into it!
next_active = child_id;
}
}
button_rects.insert(child_id, response.rect);
if is_being_dragged {
dragged_index = Some(i);
}
}
});
});
// -----------
// Drop zones:
let preview_thickness = 6.0;
let after_rect = |rect: Rect| {
let dragged_size = if let Some(dragged_index) = dragged_index {
// We actually know the size of this thing
button_rects[&self.children[dragged_index]].size()
} else {
rect.size() // guess that the size is the same as the last button
};
Rect::from_min_size(
rect.right_top() + vec2(ui.spacing().item_spacing.x, 0.0),
dragged_size,
)
};
super::linear::drop_zones(
preview_thickness,
&self.children,
dragged_index,
super::LinearDir::Horizontal,
|node_id| button_rects[&node_id],
|rect, i| {
drop_context
.suggest_rect(InsertionPoint::new(node_id, LayoutInsertion::Tabs(i)), rect);
},
after_rect,
);
next_active
}
}

View File

@@ -1,271 +0,0 @@
use egui::{Id, NumExt as _, Rect, Ui};
use super::{
is_possible_drag, Behavior, Branch, DropContext, InsertionPoint, Node, NodeId, Nodes,
SimplificationOptions, SimplifyAction,
};
/// The top level type. Contains all persistent state, including layouts and sizes.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Dock<Leaf> {
pub root: NodeId,
pub nodes: Nodes<Leaf>,
/// Smoothed avaerage of preview
#[serde(skip)]
pub smoothed_preview_rect: Option<Rect>,
}
impl<Leaf> Default for Dock<Leaf> {
fn default() -> Self {
Self {
root: Default::default(),
nodes: Default::default(),
smoothed_preview_rect: None,
}
}
}
impl<Leaf: std::fmt::Debug> std::fmt::Debug for Dock<Leaf> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Print a hiearchical view of the tree:
fn format_node<Leaf: std::fmt::Debug>(
f: &mut std::fmt::Formatter<'_>,
nodes: &Nodes<Leaf>,
indent: usize,
node_id: NodeId,
) -> std::fmt::Result {
write!(f, "{} {node_id:?} ", " ".repeat(indent))?;
if let Some(node) = nodes.get(node_id) {
match node {
Node::Leaf(leaf) => writeln!(f, "Leaf {leaf:?}"),
Node::Branch(branch) => {
writeln!(
f,
"{}",
match branch {
Branch::Tabs(_) => "Tabs",
Branch::Linear(_) => "Linear",
Branch::Grid(_) => "Grid",
}
)?;
for &child in branch.children() {
format_node(f, nodes, indent + 1, child)?;
}
Ok(())
}
}
} else {
write!(f, "DANGLING {node_id:?}")
}
}
writeln!(f, "Dock {{")?;
format_node(f, &self.nodes, 1, self.root)?;
write!(f, "\n}}")
}
}
// ----------------------------------------------------------------------------
impl<Leaf> Dock<Leaf> {
pub fn new(root: NodeId, nodes: Nodes<Leaf>) -> Self {
Self {
root,
nodes,
smoothed_preview_rect: None,
}
}
pub fn root(&self) -> NodeId {
self.root
}
pub fn parent_of(&self, node_id: NodeId) -> Option<NodeId> {
self.nodes
.nodes
.iter()
.find(|(_, node)| {
if let Node::Branch(branch) = node {
branch.children().contains(&node_id)
} else {
false
}
})
.map(|(id, _)| *id)
}
/// Show the dock in the given [`Ui`].
///
/// The dock will use upp all the avilable space - nothing more, nothing less.
pub fn ui(&mut self, behavior: &mut dyn Behavior<Leaf>, ui: &mut Ui) {
let options = behavior.simplification_options();
self.simplify(&options);
if options.all_leaves_must_have_tabs {
self.nodes
.make_all_leaves_children_of_tabs(false, self.root);
}
self.nodes.gc_root(behavior, self.root);
self.nodes.rects.clear();
// Check if anything is being dragged:
let mut drop_context = DropContext {
enabled: true,
dragged_node_id: self.dragged_id(ui.ctx()),
mouse_pos: ui.input(|i| i.pointer.hover_pos()),
best_dist_sq: f32::INFINITY,
best_insertion: None,
preview_rect: None,
};
self.nodes.layout_node(
ui.style(),
behavior,
ui.available_rect_before_wrap(),
self.root,
);
self.nodes
.node_ui(behavior, &mut drop_context, ui, self.root);
self.preview_dragged_node(behavior, &drop_context, ui);
}
fn preview_dragged_node(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &DropContext,
ui: &mut Ui,
) {
if let (Some(mouse_pos), Some(dragged_node_id)) =
(drop_context.mouse_pos, drop_context.dragged_node_id)
{
ui.output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing);
// Preview what is being dragged:
egui::Area::new(Id::new((dragged_node_id, "preview")))
.pivot(egui::Align2::CENTER_CENTER)
.current_pos(mouse_pos)
.interactable(false)
.show(ui.ctx(), |ui| {
let mut frame = egui::Frame::popup(ui.style());
frame.fill = frame.fill.gamma_multiply(0.5); // Make see-through
frame.show(ui, |ui| {
// TODO(emilk): preview contents?
let text = behavior.tab_title_for_node(&self.nodes, dragged_node_id);
ui.label(text);
});
});
if let Some(preview_rect) = drop_context.preview_rect {
let preview_rect = self.smooth_preview_rect(ui.ctx(), preview_rect);
let parent_rect = drop_context
.best_insertion
.and_then(|insertion_point| self.nodes.try_rect(insertion_point.parent_id));
behavior.paint_drag_preview(ui.visuals(), ui.painter(), parent_rect, preview_rect);
if behavior.preview_dragged_leaves() {
// TODO(emilk): add support for previewing branches too.
if preview_rect.width() > 32.0 && preview_rect.height() > 32.0 {
if let Some(Node::Leaf(leaf)) = self.nodes.get_mut(dragged_node_id) {
let _ = behavior.leaf_ui(
&mut ui.child_ui(preview_rect, *ui.layout()),
dragged_node_id,
leaf,
);
}
}
}
}
if ui.input(|i| i.pointer.any_released()) {
ui.memory_mut(|mem| mem.stop_dragging());
if let Some(insertion_point) = drop_context.best_insertion {
self.move_node(dragged_node_id, insertion_point);
}
self.smoothed_preview_rect = None;
}
} else {
self.smoothed_preview_rect = None;
}
}
/// Take the preview rectangle and smooth it over time.
fn smooth_preview_rect(&mut self, ctx: &egui::Context, new_rect: Rect) -> Rect {
let dt = ctx.input(|input| input.stable_dt).at_most(0.1);
let t = egui::emath::exponential_smooth_factor(0.9, 0.05, dt);
let smoothed = self.smoothed_preview_rect.get_or_insert(new_rect);
*smoothed = smoothed.lerp_towards(&new_rect, t);
let diff = smoothed.min.distance(new_rect.min) + smoothed.max.distance(new_rect.max);
if diff < 0.5 {
*smoothed = new_rect;
} else {
ctx.request_repaint();
}
*smoothed
}
fn simplify(&mut self, options: &SimplificationOptions) {
match self.nodes.simplify(options, self.root) {
SimplifyAction::Remove => {
log::warn!("Tried to simplify root node!"); // TODO: handle this
}
SimplifyAction::Keep => {}
SimplifyAction::Replace(new_root) => {
self.root = new_root;
}
}
}
/// Move the given node to the given insertion point.
pub fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) {
log::debug!(
"Moving {moved_node_id:?} into {:?}",
insertion_point.insertion
);
self.remove_node_id_from_parent(moved_node_id);
self.nodes.insert(insertion_point, moved_node_id);
}
/// Find the currently dragged node, if any.
pub fn dragged_id(&self, ctx: &egui::Context) -> Option<NodeId> {
if !is_possible_drag(ctx) {
// We're not sure we're dragging _at all_ yet.
return None;
}
for &node_id in self.nodes.nodes.keys() {
if node_id == self.root {
continue; // not allowed to drag root
}
let id = node_id.id();
let is_node_being_dragged = ctx.memory(|mem| mem.is_being_dragged(id));
if is_node_being_dragged {
// Abort drags on escape:
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
ctx.memory_mut(|mem| mem.stop_dragging());
return None;
}
return Some(node_id);
}
}
None
}
/// Performs no simplifcations, nor does it remove the actual [`Node`].
pub fn remove_node_id_from_parent(&mut self, remove_me: NodeId) {
for parent in self.nodes.nodes.values_mut() {
if let Node::Branch(branch) = parent {
branch.retain(|child| child != remove_me);
}
}
}
}

View File

@@ -1,261 +0,0 @@
//! # Dock
//! Tabs that can be dragged around and split up in horizontal, vertical, and grid-layouts.
//!
//! ## Overview
//! The user add leaves to a [`Dock`], arranged using [`Branch`]es.
//! This forms a layout tree.
//! Everything is generic over the type of leaves, leaving up to the user what to store in the tree.
//!
//! Each [`Node]` is either a `Leaf` or a [`Branch`].
//! Each [`Node`] is identified by a (random) [`NodeId`].
//! The nodes are stored in [`Nodes`].
//!
//! The entire state is stored in a single [`Dock`] struct which consists of a [`Nodes`] and a root [`NodeId`].
//!
//! The behavior and the look of the dock is controlled by the [`Behavior`] `trait`.
//! The user needs to implement this in order to specify the `ui` of each `Leaf` and
//! the tab name of leaves (if there are tab nodes).
//!
//! ## Shares
//! The relative sizes of linear layout (horizontal or vertical) and grid columns and rows are spcified by _shares_.
//! If the shares are `1,2,3` it means the first element gets `1/6` of the space, the second `2/6`, and the third `3/6`.
//! The default share size is `1`, and when resizing the shares are restributed so that
//! the total shares are always aproximately the same as the number of rows/columns.
//! This makes it easy to add new rows/columns.
//!
//! ## Shortcomings
//! The implementation is recursive, so if your trees get too deep you will get a stack overflow.
//!
//! ## Future improvements
//! * Easy per-tab close-buttons
//! * Scrolling of tab-bar
//! * Vertical tab bar
//! * Auto-join nested horizontal/vertical layouts in the simplify step
// ## Implementation notes
// In many places we want to recursively visit all noted, while also mutating them.
// In order to not get into trouble with the borrow checker a trick is used:
// each [`Node`] is removed, mutated, recursed, and then re-added.
// You'll see this pattern many times reading the following code.
//
// Each frame consists of two passes: layout, and ui.
// The layout pass figures out where each node should be placed.
// The ui pass does all the painting.
// These two passes could be combined into one pass if we wanted to,
// but having them split up makes the code slightly simpler, and
// leaves the door open for more complex layout (e.g. min/max sizes per node).
//
// Everything is quite dynamic, so we have a bunch of defensive coding that call `warn!` on failure.
// These situations should not happen in normal use, but could happen if the user messes with
// the internals of the tree, putting it in an invalid state.
use egui::{Id, Pos2, Rect};
mod behavior;
mod branch;
mod dock_struct;
mod nodes;
pub use behavior::Behavior;
pub use branch::{Branch, Grid, GridLoc, Layout, Linear, LinearDir, Tabs};
pub use dock_struct::Dock;
pub use nodes::Nodes;
// ----------------------------------------------------------------------------
/// An identifier for a [`Node`] in the dock tree, be it a branch or a leaf.
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct NodeId(u128);
impl NodeId {
/// Generate a new random [`NodeId`].
pub fn random() -> Self {
use rand::Rng as _;
Self(rand::thread_rng().gen())
}
/// Corresponding [`egui::Id`], used for dragging.
pub fn id(&self) -> Id {
Id::new(self)
}
}
impl std::fmt::Debug for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:08X}", self.0 as u32)
}
}
// ----------------------------------------------------------------------------
/// A node in the tree. Either a leaf or a [`Branch`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum Node<Leaf> {
Leaf(Leaf),
Branch(Branch),
}
impl<Leaf> Node<Leaf> {
fn layout(&self) -> Option<Layout> {
match self {
Node::Leaf(_) => None,
Node::Branch(branch) => Some(branch.layout()),
}
}
}
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UiResponse {
None,
/// The viewer is being dragged via some element in the Leaf
DragStarted,
}
/// What are the rules for simplifying the tree?
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SimplificationOptions {
pub prune_empty_tabs: bool,
pub prune_single_child_tabs: bool,
pub prune_empty_layouts: bool,
pub prune_single_child_layouts: bool,
pub all_leaves_must_have_tabs: bool,
}
impl Default for SimplificationOptions {
fn default() -> Self {
Self {
prune_empty_tabs: true,
prune_single_child_tabs: true,
prune_empty_layouts: true,
prune_single_child_layouts: true,
all_leaves_must_have_tabs: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ResizeState {
Idle,
Hovering,
Dragging,
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LayoutInsertion {
Tabs(usize),
Horizontal(usize),
Vertical(usize),
Grid(GridLoc),
}
#[derive(Clone, Copy, Debug)]
pub struct InsertionPoint {
pub parent_id: NodeId,
/// Where in the parent?
pub insertion: LayoutInsertion,
}
impl InsertionPoint {
pub fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self {
Self {
parent_id,
insertion,
}
}
}
#[derive(PartialEq, Eq)]
enum GcAction {
Keep,
Remove,
}
#[must_use]
enum SimplifyAction {
Remove,
Keep,
Replace(NodeId),
}
fn is_possible_drag(ctx: &egui::Context) -> bool {
ctx.input(|input| input.pointer.is_decidedly_dragging())
}
fn is_being_dragged(ctx: &egui::Context, node_id: NodeId) -> bool {
ctx.memory(|mem| mem.is_being_dragged(node_id.id())) && is_possible_drag(ctx)
}
// ----------------------------------------------------------------------------
struct DropContext {
enabled: bool,
dragged_node_id: Option<NodeId>,
mouse_pos: Option<Pos2>,
best_insertion: Option<InsertionPoint>,
best_dist_sq: f32,
preview_rect: Option<Rect>,
}
impl DropContext {
fn on_node<Leaf>(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
style: &egui::Style,
parent_id: NodeId,
rect: Rect,
node: &Node<Leaf>,
) {
if !self.enabled {
return;
}
if node.layout() != Some(Layout::Horizontal) {
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(0)),
rect.split_left_right_at_fraction(0.5).0,
);
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(usize::MAX)),
rect.split_left_right_at_fraction(0.5).1,
);
}
if node.layout() != Some(Layout::Vertical) {
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Vertical(0)),
rect.split_top_bottom_at_fraction(0.5).0,
);
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Vertical(usize::MAX)),
rect.split_top_bottom_at_fraction(0.5).1,
);
}
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Tabs(usize::MAX)),
rect.split_top_bottom_at_y(rect.top() + behavior.tab_bar_height(style))
.1,
);
}
fn suggest_rect(&mut self, insertion: InsertionPoint, preview_rect: Rect) {
if !self.enabled {
return;
}
let target_point = preview_rect.center();
if let Some(mouse_pos) = self.mouse_pos {
let dist_sq = mouse_pos.distance_sq(target_point);
if dist_sq < self.best_dist_sq {
self.best_dist_sq = dist_sq;
self.best_insertion = Some(insertion);
self.preview_rect = Some(preview_rect);
}
}
}
}

View File

@@ -1,358 +0,0 @@
use std::collections::{HashMap, HashSet};
use egui::{Pos2, Rect, Ui};
use super::{
Behavior, Branch, DropContext, GcAction, Grid, InsertionPoint, Layout, LayoutInsertion, Linear,
LinearDir, Node, NodeId, SimplificationOptions, SimplifyAction, Tabs, UiResponse,
};
/// Contains all node state, but no root.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Nodes<Leaf> {
pub nodes: HashMap<NodeId, Node<Leaf>>,
/// Filled in by the layout step at the start of each frame.
#[serde(default, skip)]
pub(super) rects: HashMap<NodeId, Rect>,
}
impl<Leaf> Default for Nodes<Leaf> {
fn default() -> Self {
Self {
nodes: Default::default(),
rects: Default::default(),
}
}
}
// ----------------------------------------------------------------------------
impl<Leaf> Nodes<Leaf> {
pub(super) fn try_rect(&self, node_id: NodeId) -> Option<Rect> {
self.rects.get(&node_id).copied()
}
pub(super) fn rect(&self, node_id: NodeId) -> Rect {
let rect = self.try_rect(node_id);
debug_assert!(rect.is_some(), "Failed to find rect for {node_id:?}");
rect.unwrap_or(egui::Rect::from_min_max(Pos2::ZERO, Pos2::ZERO))
}
pub fn get(&self, node_id: NodeId) -> Option<&Node<Leaf>> {
self.nodes.get(&node_id)
}
pub fn get_mut(&mut self, node_id: NodeId) -> Option<&mut Node<Leaf>> {
self.nodes.get_mut(&node_id)
}
#[must_use]
pub fn insert_node(&mut self, node: Node<Leaf>) -> NodeId {
let id = NodeId::random();
self.nodes.insert(id, node);
id
}
#[must_use]
pub fn insert_leaf(&mut self, leaf: Leaf) -> NodeId {
self.insert_node(Node::Leaf(leaf))
}
#[must_use]
pub fn insert_branch(&mut self, branch: Branch) -> NodeId {
self.insert_node(Node::Branch(branch))
}
#[must_use]
pub fn insert_tab_node(&mut self, children: Vec<NodeId>) -> NodeId {
self.insert_node(Node::Branch(Branch::new_tabs(children)))
}
#[must_use]
pub fn insert_horizontal_node(&mut self, children: Vec<NodeId>) -> NodeId {
self.insert_node(Node::Branch(Branch::new_linear(
LinearDir::Horizontal,
children,
)))
}
#[must_use]
pub fn insert_vertical_node(&mut self, children: Vec<NodeId>) -> NodeId {
self.insert_node(Node::Branch(Branch::new_linear(
LinearDir::Vertical,
children,
)))
}
#[must_use]
pub fn insert_grid_node(&mut self, children: Vec<NodeId>) -> NodeId {
self.insert_node(Node::Branch(Branch::new_grid(children)))
}
pub fn insert(&mut self, insertion_point: InsertionPoint, child_id: NodeId) {
let InsertionPoint {
parent_id,
insertion,
} = insertion_point;
let Some(mut node) = self.nodes.remove(&parent_id) else {
log::warn!("Failed to insert: could not find parent {parent_id:?}");
return;
};
match insertion {
LayoutInsertion::Tabs(index) => {
if let Node::Branch(Branch::Tabs(tabs)) = &mut node {
let index = index.min(tabs.children.len());
tabs.children.insert(index, child_id);
tabs.active = child_id;
self.nodes.insert(parent_id, node);
} else {
let new_node_id = self.insert_node(node);
let mut tabs = Tabs::new(vec![new_node_id]);
tabs.children.insert(index.min(1), child_id);
tabs.active = child_id;
self.nodes
.insert(parent_id, Node::Branch(Branch::Tabs(tabs)));
}
}
LayoutInsertion::Horizontal(index) => {
if let Node::Branch(Branch::Linear(Linear {
dir: LinearDir::Horizontal,
children,
..
})) = &mut node
{
let index = index.min(children.len());
children.insert(index, child_id);
self.nodes.insert(parent_id, node);
} else {
let new_node_id = self.insert_node(node);
let mut linear = Linear::new(LinearDir::Horizontal, vec![new_node_id]);
linear.children.insert(index.min(1), child_id);
self.nodes
.insert(parent_id, Node::Branch(Branch::Linear(linear)));
}
}
LayoutInsertion::Vertical(index) => {
if let Node::Branch(Branch::Linear(Linear {
dir: LinearDir::Vertical,
children,
..
})) = &mut node
{
let index = index.min(children.len());
children.insert(index, child_id);
self.nodes.insert(parent_id, node);
} else {
let new_node_id = self.insert_node(node);
let mut linear = Linear::new(LinearDir::Vertical, vec![new_node_id]);
linear.children.insert(index.min(1), child_id);
self.nodes
.insert(parent_id, Node::Branch(Branch::Linear(linear)));
}
}
LayoutInsertion::Grid(insert_location) => {
if let Node::Branch(Branch::Grid(grid)) = &mut node {
grid.locations.retain(|_, pos| *pos != insert_location);
grid.locations.insert(child_id, insert_location);
grid.children.push(child_id);
self.nodes.insert(parent_id, node);
} else {
let new_node_id = self.insert_node(node);
let mut grid = Grid::new(vec![new_node_id, child_id]);
grid.locations.insert(child_id, insert_location);
self.nodes
.insert(parent_id, Node::Branch(Branch::Grid(grid)));
}
}
}
}
pub(super) fn gc_root(&mut self, behavior: &mut dyn Behavior<Leaf>, root_id: NodeId) {
let mut visited = HashSet::default();
self.gc_node_id(behavior, &mut visited, root_id);
if visited.len() < self.nodes.len() {
log::warn!(
"GC collecting nodes: {:?}",
self.nodes
.keys()
.filter(|id| !visited.contains(id))
.collect::<Vec<_>>()
);
}
self.nodes.retain(|node_id, _| visited.contains(node_id));
}
fn gc_node_id(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
visited: &mut HashSet<NodeId>,
node_id: NodeId,
) -> GcAction {
let Some(mut node) = self.nodes.remove(&node_id) else { return GcAction::Remove; };
if !visited.insert(node_id) {
log::warn!("Cycle or duplication detected");
return GcAction::Remove;
}
match &mut node {
Node::Leaf(leaf) => {
if !behavior.retain_leaf(leaf) {
return GcAction::Remove;
}
}
Node::Branch(branch) => {
branch.retain(|child| self.gc_node_id(behavior, visited, child) == GcAction::Keep);
}
}
self.nodes.insert(node_id, node);
GcAction::Keep
}
pub(super) fn layout_node(
&mut self,
style: &egui::Style,
behavior: &mut dyn Behavior<Leaf>,
rect: Rect,
node_id: NodeId,
) {
let Some(mut node) = self.nodes.remove(&node_id) else {
log::warn!("Failed to find node {node_id:?} during layout");
return;
};
self.rects.insert(node_id, rect);
if let Node::Branch(branch) = &mut node {
branch.layout_recursive(self, style, behavior, rect);
}
self.nodes.insert(node_id, node);
}
pub(super) fn node_ui(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &mut DropContext,
ui: &mut Ui,
node_id: NodeId,
) {
// NOTE: important that we get the rect and node in two steps,
// otherwise we could loose the node when there is no rect.
let Some(rect) = self.try_rect(node_id) else {
log::warn!("Failed to find rect for node {node_id:?} during ui");
return
};
let Some(mut node) = self.nodes.remove(&node_id) else {
log::warn!("Failed to find node {node_id:?} during ui");
return
};
let drop_context_was_enabled = drop_context.enabled;
if Some(node_id) == drop_context.dragged_node_id {
// Can't drag a node onto self or any children
drop_context.enabled = false;
}
drop_context.on_node(behavior, ui.style(), node_id, rect, &node);
// Each node gets its own `Ui`, nested inside each other, with proper clip rectangles.
let mut ui = egui::Ui::new(
ui.ctx().clone(),
ui.layer_id(),
ui.id().with(node_id),
rect,
rect,
);
match &mut node {
Node::Leaf(leaf) => {
if behavior.leaf_ui(&mut ui, node_id, leaf) == UiResponse::DragStarted {
ui.memory_mut(|mem| mem.set_dragged_id(node_id.id()));
}
}
Node::Branch(branch) => {
branch.ui(self, behavior, drop_context, &mut ui, rect, node_id);
}
};
self.nodes.insert(node_id, node);
drop_context.enabled = drop_context_was_enabled;
}
pub(super) fn simplify(
&mut self,
options: &SimplificationOptions,
it: NodeId,
) -> SimplifyAction {
let Some(mut node) = self.nodes.remove(&it) else {
log::warn!("Failed to find node {it:?} during simplify");
return SimplifyAction::Remove;
};
if let Node::Branch(branch) = &mut node {
// TODO(emilk): join nested versions of the same horizontal/vertical layouts
branch.simplify_children(|child| self.simplify(options, child));
if branch.layout() == Layout::Tabs {
if options.prune_empty_tabs && branch.is_empty() {
log::debug!("Simplify: removing empty tabs node");
return SimplifyAction::Remove;
}
if options.prune_single_child_tabs && branch.children().len() == 1 {
if options.all_leaves_must_have_tabs
&& matches!(self.get(branch.children()[0]), Some(Node::Leaf(_)))
{
// Keep it
} else {
log::debug!("Simplify: collapsing single-child tabs node");
return SimplifyAction::Replace(branch.children()[0]);
}
}
} else {
if options.prune_empty_layouts && branch.is_empty() {
log::debug!("Simplify: removing empty layout node");
return SimplifyAction::Remove;
}
if options.prune_single_child_layouts && branch.children().len() == 1 {
log::debug!("Simplify: collapsing single-child layout node");
return SimplifyAction::Replace(branch.children()[0]);
}
}
}
self.nodes.insert(it, node);
SimplifyAction::Keep
}
pub(super) fn make_all_leaves_children_of_tabs(&mut self, parent_is_tabs: bool, it: NodeId) {
let Some(mut node) = self.nodes.remove(&it) else {
log::warn!("Failed to find node {it:?} during make_all_leaves_children_of_tabs");
return;
};
match &mut node {
Node::Leaf(_) => {
if !parent_is_tabs {
// Add tabs to this leaf:
log::debug!("Auto-adding Tabs-parent to leaf {it:?}");
let new_id = NodeId::random();
self.nodes.insert(new_id, node);
self.nodes
.insert(it, Node::Branch(Branch::new_tabs(vec![new_id])));
return;
}
}
Node::Branch(branch) => {
let is_tabs = branch.layout() == Layout::Tabs;
for &child in branch.children() {
self.make_all_leaves_children_of_tabs(is_tabs, child);
}
}
}
self.nodes.insert(it, node);
}
}

View File

@@ -13,7 +13,6 @@
#[cfg(feature = "chrono")]
mod datepicker;
pub mod dock;
pub mod image;
mod layout;
mod sizing;

View File

@@ -1,19 +0,0 @@
[package]
name = "dock"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.65"
publish = false
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"persistence",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { path = "../../crates/egui_extras" }
env_logger = "0.10"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,7 +0,0 @@
Example how to showcase `egui_extras::dock`.
```sh
cargo run -p dock
```
![](screenshot.png)

View File

@@ -1,286 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
use egui_extras::dock;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(800.0, 600.0)),
..Default::default()
};
eframe::run_native(
"Dock",
options,
Box::new(|cc| {
let mut app = MyApp::default();
if let Some(storage) = cc.storage {
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
app = state;
}
}
Box::new(app)
}),
)
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct View {
nr: usize,
}
impl std::fmt::Debug for View {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("View").field("nr", &self.nr).finish()
}
}
impl View {
pub fn with_nr(nr: usize) -> Self {
Self { nr }
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> dock::UiResponse {
let color = egui::epaint::Hsva::new(0.1 * self.nr as f32, 0.5, 0.5, 1.0);
ui.painter().rect_filled(ui.max_rect(), 0.0, color);
let dragged = ui
.allocate_rect(ui.max_rect(), egui::Sense::drag())
.on_hover_cursor(egui::CursorIcon::Grab)
.dragged();
if dragged {
dock::UiResponse::DragStarted
} else {
dock::UiResponse::None
}
}
}
struct DockBehavior {
simplification_options: dock::SimplificationOptions,
tab_bar_height: f32,
gap_width: f32,
add_child_to: Option<dock::NodeId>,
}
impl Default for DockBehavior {
fn default() -> Self {
Self {
simplification_options: Default::default(),
tab_bar_height: 24.0,
gap_width: 2.0,
add_child_to: None,
}
}
}
impl DockBehavior {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
simplification_options,
tab_bar_height,
gap_width,
add_child_to: _,
} = self;
egui::Grid::new("behavior_ui")
.num_columns(2)
.show(ui, |ui| {
ui.label("All leaves must have tabs:");
ui.checkbox(&mut simplification_options.all_leaves_must_have_tabs, "");
ui.end_row();
ui.label("Tab bar height:");
ui.add(
egui::DragValue::new(tab_bar_height)
.clamp_range(0.0..=100.0)
.speed(1.0),
);
ui.end_row();
ui.label("Gap width:");
ui.add(
egui::DragValue::new(gap_width)
.clamp_range(0.0..=20.0)
.speed(1.0),
);
ui.end_row();
});
}
}
impl dock::Behavior<View> for DockBehavior {
fn leaf_ui(
&mut self,
ui: &mut egui::Ui,
_node_id: dock::NodeId,
view: &mut View,
) -> dock::UiResponse {
view.ui(ui)
}
fn tab_title_for_leaf(&mut self, view: &View) -> egui::WidgetText {
format!("View {}", view.nr).into()
}
fn top_bar_rtl_ui(&mut self, ui: &mut egui::Ui, node_id: dock::NodeId) {
if ui.button("").clicked() {
self.add_child_to = Some(node_id);
}
}
// ---
// Settings:
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
self.tab_bar_height
}
fn gap_width(&self, _style: &egui::Style) -> f32 {
self.gap_width
}
fn simplification_options(&self) -> dock::SimplificationOptions {
self.simplification_options
}
}
#[derive(serde::Deserialize, serde::Serialize)]
struct MyApp {
dock: dock::Dock<View>,
#[serde(skip)]
behavior: DockBehavior,
#[serde(skip)]
last_dock_debug: String,
}
impl Default for MyApp {
fn default() -> Self {
let mut next_view_nr = 0;
let mut gen_view = || {
let view = View::with_nr(next_view_nr);
next_view_nr += 1;
view
};
let mut nodes = dock::Nodes::default();
let mut tabs = vec![];
let tab_node = {
let children = (0..7).map(|_| nodes.insert_leaf(gen_view())).collect();
nodes.insert_tab_node(children)
};
tabs.push(tab_node);
tabs.push({
let children = (0..7).map(|_| nodes.insert_leaf(gen_view())).collect();
nodes.insert_horizontal_node(children)
});
tabs.push({
let children = (0..7).map(|_| nodes.insert_leaf(gen_view())).collect();
nodes.insert_vertical_node(children)
});
tabs.push({
let cells = (0..11).map(|_| nodes.insert_leaf(gen_view())).collect();
nodes.insert_grid_node(cells)
});
tabs.push(nodes.insert_leaf(gen_view()));
let root = nodes.insert_tab_node(tabs);
let dock = dock::Dock::new(root, nodes);
Self {
dock,
behavior: Default::default(),
last_dock_debug: Default::default(),
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::SidePanel::left("tree").show(ctx, |ui| {
if ui.button("Reset").clicked() {
*self = Default::default();
}
self.behavior.ui(ui);
ui.separator();
tree_ui(ui, &mut self.behavior, &mut self.dock.nodes, self.dock.root);
if let Some(parent) = self.behavior.add_child_to.take() {
let new_child = self.dock.nodes.insert_leaf(View::with_nr(100));
if let Some(dock::Node::Branch(dock::Branch::Tabs(tabs))) =
self.dock.nodes.get_mut(parent)
{
tabs.add_child(new_child);
tabs.set_active(new_child);
}
}
ui.separator();
ui.style_mut().wrap = Some(false);
let dock_debug = format!("{:#?}", self.dock);
ui.monospace(&dock_debug);
if self.last_dock_debug != dock_debug {
self.last_dock_debug = dock_debug;
log::debug!("{}", self.last_dock_debug);
}
});
egui::CentralPanel::default().show(ctx, |ui| {
self.dock.ui(&mut self.behavior, ui);
});
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, &self);
}
}
fn tree_ui(
ui: &mut egui::Ui,
behavior: &mut dyn dock::Behavior<View>,
nodes: &mut dock::Nodes<View>,
node_id: dock::NodeId,
) {
// Get the name BEFORE we remove the node below!
let text = format!(
"{} - {node_id:?}",
behavior.tab_title_for_node(nodes, node_id).text()
);
let Some(mut node) = nodes.nodes.remove(&node_id) else {
log::warn!("Missing node {node_id:?}");
return;
};
egui::CollapsingHeader::new(text)
.id_source((node_id, "tree"))
.default_open(true)
.show(ui, |ui| match &mut node {
dock::Node::Leaf(_) => {}
dock::Node::Branch(branch) => {
let mut layout = branch.layout();
egui::ComboBox::from_label("Layout")
.selected_text(format!("{:?}", layout))
.show_ui(ui, |ui| {
for typ in dock::Layout::ALL {
ui.selectable_value(&mut layout, typ, format!("{:?}", typ))
.clicked();
}
});
if layout != branch.layout() {
branch.set_layout(layout);
}
for &child in branch.children() {
tree_ui(ui, behavior, nodes, child);
}
}
});
nodes.nodes.insert(node_id, node);
}