From fc77f4cc7f4dac2637c5ed13ebb75a66c8f85e30 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sat, 10 Oct 2026 09:40:28 -0600 Subject: [PATCH] UI.7: preserve split resizing beside panel scrollbars --- crates/ui/src/docking/surface.rs | 14 ++++---- crates/ui/src/docking/tests.rs | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index cc4e7b05..d993ffa5 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -218,6 +218,14 @@ impl Renderer<'_, P> { let (lo, hi) = split_bounds(wanted.available(), *axis, self.node_limits(first), self.node_limits(second)); let first_size = wanted.first_size().clamp(lo, hi); let Some(split) = split_rect(rect, *axis, SplitSize::FixedFirst(first_size), self.style.gap, 0.0) else { return }; + path.push(false); + self.node(ui, first, split.first, path); + path.pop(); + path.push(true); + self.node(ui, second, split.second, path); + path.pop(); + // The divider's inner-edge hit margin must win over child scrollbars, just + // as accordion dividers win over the adjacent panel's content. let response = ResizeHandle::new( self.id.with(("split", &*path)), *axis, @@ -232,12 +240,6 @@ impl Renderer<'_, P> { { self.output.actions.push(Action::ResizeSplit { path: path.clone(), size }); } - path.push(false); - self.node(ui, first, split.first, path); - path.pop(); - path.push(true); - self.node(ui, second, split.second, path); - path.pop(); } Node::Tabs { panels, active } => self.group(ui, panels, *active, rect, false), Node::Stack { entries } => { diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs index efc02b7f..91e374d5 100644 --- a/crates/ui/src/docking/tests.rs +++ b/crates/ui/src/docking/tests.rs @@ -509,6 +509,65 @@ fn stack_dividers_resize_adjacent_open_bodies_by_pointer_and_keyboard() { assert!((entries[0].height.unwrap() + entries[2].height.unwrap() - 300.0).abs() < 0.001); } +#[test] +fn split_inner_edge_remains_draggable_beside_a_scrollbar() { + let ctx = Context::default(); + let mut layout = Layout { + root: Some(Node::Split { + axis: SplitAxis::Horizontal, + size: SplitSize::FixedFirst(220.0), + first: Box::new(tabs(&[1], 0)), + second: Box::new(tabs(&[2], 0)), + }), + floating: Vec::new(), + }; + let frame = |layout: &mut Layout, events| { + let mut output = DockOutput::default(); + let mut sidebar = Rect::NOTHING; + ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), events, ..Default::default() }, |ui| { + output = DockArea::new(Id::new("scrolling-dock")).show( + ui, + layout, + &DockStyle::from_ui(ui), + |p| format!("Panel {p}"), + |_| Permissions::default(), + |ui, panel| { + if *panel == 1 { + sidebar = ui.max_rect(); + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) + .show(ui, |ui| { + for row in 0..100 { + ui.label(format!("Library item {row}")); + } + }); + } else { + ui.label("Document"); + } + }, + ); + }) + .textures_delta + .clear(); + assert!(output.error.is_none()); + for action in output.actions { + layout.apply(action).unwrap(); + } + sidebar + }; + for _ in 0..3 { + frame(&mut layout, Vec::new()); + } + let sidebar = frame(&mut layout, Vec::new()); + let start = pos2(sidebar.right() - 2.0, sidebar.center().y); + frame(&mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]); + frame(&mut layout, vec![Event::PointerMoved(start + vec2(40.0, 0.0))]); + frame(&mut layout, vec![pointer_button(start + vec2(40.0, 0.0), false)]); + let Node::Split { size: SplitSize::FixedFirst(width), .. } = layout.root.unwrap() else { panic!() }; + assert!((width - 260.0).abs() < 0.001, "sidebar width after inner-edge drag: {width}"); +} + #[test] fn restoring_a_panel_beside_a_nested_subtree_preserves_the_complete_workspace() { let mut layout = Layout {