mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Remove dock stuff
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -1128,17 +1128,6 @@ dependencies = [
|
|||||||
"libloading 0.7.4",
|
"libloading 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dock"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui_extras",
|
|
||||||
"env_logger",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -1330,11 +1319,8 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"document-features",
|
"document-features",
|
||||||
"egui",
|
"egui",
|
||||||
"getrandom",
|
|
||||||
"image",
|
"image",
|
||||||
"itertools",
|
|
||||||
"log",
|
"log",
|
||||||
"rand",
|
|
||||||
"resvg",
|
"resvg",
|
||||||
"serde",
|
"serde",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
@@ -1706,10 +1692,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -36,13 +36,7 @@ svg = ["resvg", "tiny-skia", "usvg"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.21.0", path = "../egui", default-features = false }
|
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"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
rand = { version = "0.8.5", features = ["getrandom", "small_rng"] }
|
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
mod datepicker;
|
mod datepicker;
|
||||||
|
|
||||||
pub mod dock;
|
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod sizing;
|
mod sizing;
|
||||||
|
|||||||
@@ -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"] }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Example how to showcase `egui_extras::dock`.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run -p dock
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user