UI.7: resize accordion bodies with pointer and keyboard handles

This commit is contained in:
skylar taylor-barrick committed 2026-10-09 22:45:31 -06:00
1 parent efc2fc6702
commit c4fbe220bc
3 files changed
+79 -2

No files matched your search

+38 -1
View File
@@ -246,7 +246,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let total_wanted: f64 = entries.iter().filter(|e| e.open).map(|e| f64::from(e.height.unwrap_or(self.style.min_pane).max(1.0))).sum();
let header = self.style.tab_height.min(rect.height() / entries.len().max(1) as f32);
let mut top = rect.top();
for entry in entries {
let mut dividers = Vec::new();
for (index, entry) in entries.iter().enumerate() {
let share = if entry.open && total_wanted > 0.0 {
(f64::from(remaining) * f64::from(entry.height.unwrap_or(self.style.min_pane).max(1.0)) / total_wanted) as f32
} else {
@@ -283,6 +284,42 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
accepts_tabs: (self.permissions)(&entry.panel).accepts_tabs,
});
top = panel_rect.bottom();
if entry.open && remaining > 0.0 {
dividers.push((index, Rect::from_min_max(pos2(rect.left(), top - 1.0), pos2(rect.right(), top + 1.0)).intersect(rect)));
}
}
// Register dividers after the headers so their narrow hit regions win at the
// shared boundary. Closed entries retain their saved sizes and are skipped.
for (index, divider) in dividers {
let Some(first) = entries.get(index) else { continue };
let Some(second) = entries.iter().skip(index.saturating_add(1)).find(|entry| entry.open) else { continue };
let a = f64::from(first.height.unwrap_or(self.style.min_pane).max(1.0));
let b = f64::from(second.height.unwrap_or(self.style.min_pane).max(1.0));
let scale = f64::from(remaining) / total_wanted;
let first_size = (a * scale) as f32;
let pair_size = ((a + b) * scale) as f32;
let body_limits = |panel: &P| {
let mut limits = (self.limits)(panel);
limits.min.y = limits.min.y.max(self.style.min_pane);
limits.max = limits.max.max(limits.min);
limits
};
let (lo, hi) = split_bounds(pair_size, SplitAxis::Vertical, body_limits(&first.panel), body_limits(&second.panel));
let response = ResizeHandle::new(
self.id.with(("stack-resize", &first.panel)),
SplitAxis::Vertical,
divider,
&self.style.resize_label,
ResizeHandleStyle { hovered: self.style.accent, dragged: self.style.accent, focused: self.style.accent },
)
.value(first_size, lo..=hi)
.show(ui);
if response.delta != 0.0 && scale > 0.0 {
let delta = f64::from((first_size + response.delta).clamp(lo, hi) - first_size) / scale;
let next_a = (a + delta).clamp((a + b - 1_000_000.0).max(1.0), (a + b - 1.0).min(1_000_000.0));
self.output.actions.push(Action::ResizeStack { panel: first.panel.clone(), height: Some(next_a as f32) });
self.output.actions.push(Action::ResizeStack { panel: second.panel.clone(), height: Some((a + b - next_a) as f32) });
}
}
}
}
+40
View File
@@ -469,6 +469,46 @@ fn focused_floating_resize_grip_accepts_both_arrow_axes_and_releases_tab() {
assert_ne!(ctx.memory(|memory| memory.focused()), Some(id));
}
#[test]
fn stack_dividers_resize_adjacent_open_bodies_by_pointer_and_keyboard() {
let ctx = Context::default();
let mut layout = Layout {
root: Some(Node::Stack {
entries: vec![
StackEntry { panel: 1, open: true, height: Some(150.0) },
StackEntry { panel: 2, open: false, height: Some(91.0) },
StackEntry { panel: 3, open: true, height: Some(150.0) },
StackEntry { panel: 4, open: true, height: Some(120.0) },
],
}),
floating: Vec::new(),
};
for _ in 0..3 {
step(&ctx, &mut layout, Vec::new());
}
let id = Id::new("test-area").with(("stack-resize", &1u32));
let start = ctx.read_response(id).unwrap().rect.center();
step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
step(&ctx, &mut layout, vec![Event::PointerMoved(start + vec2(0.0, 24.0))]);
step(&ctx, &mut layout, vec![pointer_button(start + vec2(0.0, 24.0), false)]);
let Node::Stack { entries } = layout.root.as_ref().unwrap() else { panic!() };
let after_pointer = entries[0].height.unwrap();
assert!(after_pointer > 150.0);
assert!((entries[0].height.unwrap() + entries[2].height.unwrap() - 300.0).abs() < 0.001);
assert_eq!(entries[1].height, Some(91.0));
assert!(!entries[1].open);
assert_eq!(entries[3].height, Some(120.0));
ctx.memory_mut(|memory| memory.request_focus(id));
step(
&ctx,
&mut layout,
vec![Event::Key { key: egui::Key::ArrowDown, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }],
);
let Node::Stack { entries } = layout.root.as_ref().unwrap() else { panic!() };
assert!(entries[0].height.unwrap() > after_pointer);
assert!((entries[0].height.unwrap() + entries[2].height.unwrap() - 300.0).abs() < 0.001);
}
#[test]
fn restoring_a_panel_beside_a_nested_subtree_preserves_the_complete_workspace() {
let mut layout = Layout {
+1 -1
View File
@@ -57,7 +57,7 @@ accepts tabs or splits. Supply targets back-to-front, including floating-window
The frontmost containing group blocks targets underneath even when it rejects a drop.
The optional `DockArea` renderer handles tab fitting and overflow, drag/drop previews,
Escape/focus-loss cancellation, split dividers and floating-group resize grips. It renders a
Escape/focus-loss cancellation, split/accordion dividers and floating-group resize grips. It renders a
snapshot and returns `DockOutput.actions`; apply them through the application's command path.
With `serde` enabled, `Action` also serializes so pointer, keyboard and control-channel requests
can share that command path, including splitter changes, tab reorder and floating bounds.