Files
hcie-rust-v3.05/patches/grimdock/src/layout.rs
T
2026-07-09 02:59:53 +03:00

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)
}
}
}