273 lines
9.9 KiB
Rust
273 lines
9.9 KiB
Rust
use egui::{Rect, Sense, Ui, vec2};
|
|
|
|
use crate::{
|
|
ids::resize_handle_id,
|
|
style::PanelStyle,
|
|
tree::{Node, PanelTree, SplitDir},
|
|
};
|
|
|
|
/// Run the layout pass over the entire tree.
|
|
///
|
|
/// Assigns a rectangle to every node starting from the root, renders resize
|
|
/// handles for split nodes, and handles ratio updates from handle drags.
|
|
///
|
|
/// Returns a list of `(leaf_index, content_rect)` pairs for consumption by
|
|
/// the header and content passes.
|
|
pub(crate) fn layout_pass<T: Clone + 'static>(
|
|
ui: &mut Ui,
|
|
tree: &mut PanelTree<T>,
|
|
style: &PanelStyle,
|
|
) -> Vec<(usize, Rect)> {
|
|
let root_rect = ui.available_rect_before_wrap();
|
|
let mut leaf_rects: Vec<(usize, Rect)> = Vec::new();
|
|
layout_node(ui, tree, 0, root_rect, style, &mut leaf_rects);
|
|
leaf_rects
|
|
}
|
|
|
|
/// Compute whether a node has vanished from the layout. A leaf with no tabs
|
|
/// is considered vanished UNLESS its `persist_when_empty` option is set, which
|
|
/// keeps the pane alive so that split ratios and sibling panes are preserved.
|
|
fn is_node_vanished<T: Clone + 'static>(tree: &PanelTree<T>, idx: usize) -> bool {
|
|
match tree.node(idx) {
|
|
Node::Leaf { tabs, options, .. } => tabs.is_empty() && !options.persist_when_empty,
|
|
Node::Split { .. } => {
|
|
let left = PanelTree::<T>::left_child(idx);
|
|
let right = PanelTree::<T>::right_child(idx);
|
|
is_node_vanished(tree, left) && is_node_vanished(tree, right)
|
|
}
|
|
Node::Empty => true,
|
|
}
|
|
}
|
|
|
|
/// Compute the effective collapsed thickness for a child node. Collapsed
|
|
/// leaves that have no tabs (i.e. floated panels) are assigned zero space so
|
|
/// they vanish from the layout entirely.
|
|
fn effective_collapsed_thickness<T: Clone + 'static>(
|
|
tree: &PanelTree<T>,
|
|
child_idx: usize,
|
|
collapsed: bool,
|
|
default: f32,
|
|
) -> f32 {
|
|
if !collapsed {
|
|
return 0.0;
|
|
}
|
|
if is_node_vanished(tree, child_idx) {
|
|
0.0
|
|
} else {
|
|
default
|
|
}
|
|
}
|
|
|
|
fn layout_node<T: Clone + 'static>(
|
|
ui: &mut Ui,
|
|
tree: &mut PanelTree<T>,
|
|
idx: usize,
|
|
rect: Rect,
|
|
style: &PanelStyle,
|
|
leaf_rects: &mut Vec<(usize, Rect)>,
|
|
) {
|
|
// Store the rect in the node for DnD hit-testing later.
|
|
match tree.node_mut(idx) {
|
|
Node::Split { rect: r, .. } => *r = rect,
|
|
Node::Leaf { rect: r, .. } => *r = rect,
|
|
Node::Empty => return,
|
|
}
|
|
|
|
match tree.node(idx).clone() {
|
|
Node::Split { dir, mut ratio, .. } => {
|
|
let handle_w = style.handle_width;
|
|
let first_idx = PanelTree::<T>::left_child(idx);
|
|
let second_idx = PanelTree::<T>::right_child(idx);
|
|
let first_collapsed = tree.is_collapsed(first_idx) || is_node_vanished(tree, first_idx);
|
|
let second_collapsed = tree.is_collapsed(second_idx) || is_node_vanished(tree, second_idx);
|
|
let resize_locked = tree.split_resize_locked(idx);
|
|
|
|
let first_ct = effective_collapsed_thickness(
|
|
tree, first_idx, first_collapsed, style.collapsed_pane_thickness,
|
|
);
|
|
let second_ct = effective_collapsed_thickness(
|
|
tree, second_idx, second_collapsed, style.collapsed_pane_thickness,
|
|
);
|
|
|
|
let (_first_rect, handle_rect, _second_rect) = split_rect(
|
|
rect, dir, ratio, handle_w,
|
|
first_collapsed, second_collapsed,
|
|
first_ct, second_ct,
|
|
style.min_pane_size, !resize_locked,
|
|
);
|
|
|
|
// Draw and interact with the resize handle.
|
|
let handle_id = resize_handle_id(idx);
|
|
let handle_response = ui.interact(
|
|
handle_rect,
|
|
handle_id,
|
|
if resize_locked {
|
|
Sense::hover()
|
|
} else {
|
|
Sense::drag()
|
|
},
|
|
);
|
|
|
|
let is_active = handle_response.dragged() || handle_response.hovered();
|
|
let color = if resize_locked {
|
|
style.handle.locked_color
|
|
} else if is_active {
|
|
style.handle.hover_color
|
|
} else {
|
|
style.handle.color
|
|
};
|
|
ui.painter().rect_filled(handle_rect, 0.0, color);
|
|
|
|
if !resize_locked && handle_response.hovered() {
|
|
let cursor = match dir {
|
|
SplitDir::Horizontal => egui::CursorIcon::ResizeHorizontal,
|
|
SplitDir::Vertical => egui::CursorIcon::ResizeVertical,
|
|
};
|
|
ui.ctx().set_cursor_icon(cursor);
|
|
}
|
|
|
|
if handle_response.dragged()
|
|
&& !resize_locked
|
|
&& !first_collapsed
|
|
&& !second_collapsed
|
|
{
|
|
let delta = handle_response.drag_delta();
|
|
let delta_ratio = match dir {
|
|
SplitDir::Horizontal => delta.x / rect.width().max(1.0),
|
|
SplitDir::Vertical => delta.y / rect.height().max(1.0),
|
|
};
|
|
|
|
let min_ratio = style.min_pane_size
|
|
/ match dir {
|
|
SplitDir::Horizontal => rect.width().max(1.0),
|
|
SplitDir::Vertical => rect.height().max(1.0),
|
|
};
|
|
let max_ratio = 1.0 - min_ratio;
|
|
ratio = (ratio + delta_ratio).clamp(min_ratio.max(0.0), max_ratio.min(1.0));
|
|
|
|
if let Node::Split { ratio: r, .. } = tree.node_mut(idx) {
|
|
*r = ratio;
|
|
}
|
|
}
|
|
|
|
let (first_rect, _, second_rect) = split_rect(
|
|
rect, dir, ratio, handle_w,
|
|
first_collapsed, second_collapsed,
|
|
first_ct, second_ct,
|
|
style.min_pane_size, !resize_locked,
|
|
);
|
|
|
|
layout_node(ui, tree, first_idx, first_rect, style, leaf_rects);
|
|
layout_node(ui, tree, second_idx, second_rect, style, leaf_rects);
|
|
}
|
|
Node::Leaf { tabs, collapsed, .. } => {
|
|
let header_height = if tree.header_visible(idx) {
|
|
if collapsed && tabs.is_empty() { 0.0 } else { style.header_height }
|
|
} else {
|
|
0.0
|
|
};
|
|
let content_rect = Rect::from_min_size(
|
|
rect.min + vec2(0.0, header_height),
|
|
vec2(rect.width(), (rect.height() - header_height).max(0.0)),
|
|
);
|
|
leaf_rects.push((idx, content_rect));
|
|
}
|
|
Node::Empty => {}
|
|
}
|
|
}
|
|
|
|
/// Split `rect` into two sub-rects with a handle strip between them.
|
|
///
|
|
/// `first_ct` / `second_ct` are the collapsed thickness for each side when
|
|
/// that side is collapsed. A value of `0.0` makes the collapsed side vanish.
|
|
///
|
|
/// When `enforce_min` is `true`, each non-collapsed side is guaranteed at least
|
|
/// `min_pane_size` pixels of extent. Splits whose resize handle is locked
|
|
/// (e.g. the fixed-width Tools column) pass `enforce_min = false` so their
|
|
/// ratio-driven (possibly narrow) width is preserved.
|
|
fn split_rect(
|
|
rect: Rect,
|
|
dir: SplitDir,
|
|
ratio: f32,
|
|
handle_w: f32,
|
|
first_collapsed: bool,
|
|
second_collapsed: bool,
|
|
first_ct: f32,
|
|
second_ct: f32,
|
|
min_pane_size: f32,
|
|
enforce_min: bool,
|
|
) -> (Rect, Rect, Rect) {
|
|
let total_extent = match dir {
|
|
SplitDir::Horizontal => rect.width(),
|
|
SplitDir::Vertical => rect.height(),
|
|
}
|
|
.max(0.0);
|
|
|
|
let first_vanished = first_collapsed && first_ct == 0.0;
|
|
let second_vanished = second_collapsed && second_ct == 0.0;
|
|
|
|
if first_vanished && second_vanished {
|
|
return (Rect::NOTHING, Rect::NOTHING, Rect::NOTHING);
|
|
} else if first_vanished {
|
|
match dir {
|
|
SplitDir::Horizontal => {
|
|
let empty = Rect::from_min_max(rect.min, rect.min);
|
|
return (empty, empty, rect);
|
|
}
|
|
SplitDir::Vertical => {
|
|
let empty = Rect::from_min_max(rect.min, rect.min);
|
|
return (empty, empty, rect);
|
|
}
|
|
}
|
|
} else if second_vanished {
|
|
match dir {
|
|
SplitDir::Horizontal => {
|
|
let empty = Rect::from_min_max(rect.max, rect.max);
|
|
return (rect, empty, empty);
|
|
}
|
|
SplitDir::Vertical => {
|
|
let empty = Rect::from_min_max(rect.max, rect.max);
|
|
return (rect, empty, empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
let first_extent = if first_collapsed && !second_collapsed {
|
|
first_ct.min((total_extent - handle_w).max(0.0))
|
|
} else if second_collapsed && !first_collapsed {
|
|
(total_extent - handle_w - second_ct).max(0.0)
|
|
} else {
|
|
let raw = total_extent * ratio;
|
|
if enforce_min && min_pane_size > 0.0 {
|
|
let usable = (total_extent - handle_w).max(0.0);
|
|
let min = min_pane_size.min(usable * 0.5);
|
|
raw.clamp(min, (usable - min).max(min))
|
|
} else {
|
|
raw
|
|
}
|
|
};
|
|
|
|
match dir {
|
|
SplitDir::Horizontal => {
|
|
let split_x = rect.min.x + first_extent;
|
|
let first = Rect::from_min_max(rect.min, egui::pos2(split_x - handle_w / 2.0, rect.max.y));
|
|
let handle = Rect::from_min_max(
|
|
egui::pos2(split_x - handle_w / 2.0, rect.min.y),
|
|
egui::pos2(split_x + handle_w / 2.0, rect.max.y),
|
|
);
|
|
let second = Rect::from_min_max(egui::pos2(split_x + handle_w / 2.0, rect.min.y), rect.max);
|
|
(first, handle, second)
|
|
}
|
|
SplitDir::Vertical => {
|
|
let split_y = rect.min.y + first_extent;
|
|
let first = Rect::from_min_max(rect.min, egui::pos2(rect.max.x, split_y - handle_w / 2.0));
|
|
let handle = Rect::from_min_max(
|
|
egui::pos2(rect.min.x, split_y - handle_w / 2.0),
|
|
egui::pos2(rect.max.x, split_y + handle_w / 2.0),
|
|
);
|
|
let second = Rect::from_min_max(egui::pos2(rect.min.x, split_y + handle_w / 2.0), rect.max);
|
|
(first, handle, second)
|
|
}
|
|
}
|
|
}
|