resizable, dock: Improve Panel adjust size logic on layout changed. (#1588)

This is a follow up to #1560 I noticed a couple small issues with
resizable panels and their use in docks after experimenting a lot:
- Sometimes the resizable group would take about ~500ms to update after
a resize. This was caused by the `StackPanel` not getting notified and
therefore not redrawn when the `ResizableState` was notified, as well as
the notify in `adjust_to_container_size` not actually firing properly.
The latter was fixed by defering the notify, though I'm still not quite
sure why that is necessary.
- In a couple of places, the total size all the panel missmatches the
container size, causing small glitches the first time a panel is
resized. Update the code in those places to properly adjust all the
panel sizes so they match the container size.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
Nico Gründel 2025-11-14 09:12:59 +01:00 committed by GitHub
parent 2a98628b5e
commit 78efbfdd7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 64 additions and 22 deletions

View file

@ -56,10 +56,12 @@ impl StackPanel {
pub fn new(axis: Axis, _: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|_| ResizableState::default());
// Bubble up the resize event.
let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| {
cx.emit(PanelEvent::LayoutChanged)
})];
let _subscriptions = vec![
// Bubble up the resize event.
cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| {
cx.emit(PanelEvent::LayoutChanged)
}),
];
Self {
axis,
@ -253,7 +255,7 @@ impl StackPanel {
Some(size) => size,
None => {
let state = self.state.read(cx);
(state.total_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
(state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
}
};

View file

@ -1,6 +1,8 @@
use std::ops::Range;
use gpui::{px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, Pixels, Window};
use gpui::{
px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window,
};
use crate::PixelsExt;
@ -66,23 +68,56 @@ impl ResizableState {
..Default::default()
};
let size = size.unwrap_or(PANEL_MIN_SIZE);
// We make sure that the size always sums up to the container size
// by reducing the size of all other panels first.
let container_size = self.container_size().max(px(1.));
let total_leftover_size = (container_size - size).max(px(1.));
for (i, panel) in self.panels.iter_mut().enumerate() {
let ratio = self.sizes[i] / container_size;
self.sizes[i] = total_leftover_size * ratio;
panel.size = Some(self.sizes[i]);
}
if let Some(ix) = ix {
self.panels.insert(ix, panel_state);
self.sizes.insert(ix, size.unwrap_or(PANEL_MIN_SIZE));
self.sizes.insert(ix, size);
} else {
self.panels.push(panel_state);
self.sizes.push(size.unwrap_or(PANEL_MIN_SIZE));
self.sizes.push(size);
};
cx.notify();
}
pub(crate) fn sync_panels_count(&mut self, axis: Axis, panels_count: usize) {
pub(crate) fn sync_panels_count(
&mut self,
axis: Axis,
panels_count: usize,
cx: &mut Context<Self>,
) {
let mut changed = self.axis != axis;
self.axis = axis;
if panels_count > self.panels.len() {
let diff = panels_count - self.panels.len();
self.panels
.extend(vec![ResizablePanelState::default(); diff]);
self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
changed = true;
}
if panels_count < self.panels.len() {
self.panels.truncate(panels_count);
self.sizes.truncate(panels_count);
changed = true;
}
if changed {
// We need to make sure the total size is in line with the container size.
self.adjust_to_container_size(cx);
}
}
@ -114,7 +149,7 @@ impl ResizableState {
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
}
}
cx.notify();
self.adjust_to_container_size(cx);
}
pub(crate) fn replace_panel(
@ -127,7 +162,7 @@ impl ResizableState {
self.panels[panel_ix] = panel;
self.sizes[panel_ix] = old_size;
cx.notify();
self.adjust_to_container_size(cx);
}
pub(crate) fn clear(&mut self) {
@ -135,8 +170,9 @@ impl ResizableState {
self.sizes.clear();
}
pub(crate) fn total_size(&self) -> Pixels {
self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into()
#[inline]
pub(crate) fn container_size(&self) -> Pixels {
self.bounds.size.along(self.axis)
}
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
@ -168,7 +204,7 @@ impl ResizableState {
if ix >= old_sizes.len() - 1 {
return;
}
let container_size = self.bounds.size.along(self.axis);
let container_size = self.container_size();
self.sync_real_panel_sizes(cx);
let move_changed = size - old_sizes[ix];
@ -226,21 +262,25 @@ impl ResizableState {
cx.notify();
}
/// Adjust panel sizes according to the container size.
///
/// When the container size changes, the panels should take up the same percentage as they did before.
fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
// At least 1px so we don't divide by zero.
let container_size = self.bounds.size.along(self.axis).max(px(1.));
let total_size = self.total_size();
if self.container_size().is_zero() {
return;
}
let container_size = self.container_size();
let total_size = px(self.sizes.iter().map(|s| s.as_f32()).sum::<f32>());
for i in 0..self.panels.len() {
let size = self.sizes[i];
let fractional_size = size / total_size;
let new_size = container_size * fractional_size;
let ratio = size / total_size;
let new_size = container_size * ratio;
self.sizes[i] = new_size;
self.panels[i].size = Some(new_size);
}
cx.notify();
}
}

View file

@ -136,8 +136,8 @@ impl RenderOnce for ResizablePanelGroup {
// Sync panels to the state
let panels_count = self.children.len();
state.update(cx, |state, _| {
state.sync_panels_count(self.axis, panels_count);
state.update(cx, |state, cx| {
state.sync_panels_count(self.axis, panels_count, cx);
});
container