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( ui: &mut Ui, tree: &mut PanelTree, 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(tree: &PanelTree, idx: usize) -> bool { match tree.node(idx) { Node::Leaf { tabs, options, .. } => tabs.is_empty() && !options.persist_when_empty, Node::Split { .. } => { let left = PanelTree::::left_child(idx); let right = PanelTree::::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( tree: &PanelTree, 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( ui: &mut Ui, tree: &mut PanelTree, 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::::left_child(idx); let second_idx = PanelTree::::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) } } }