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 { pub fn new(axis: Axis, _: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|_| ResizableState::default()); let state = cx.new(|_| ResizableState::default());
// Bubble up the resize event. let _subscriptions = vec![
let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| { // Bubble up the resize event.
cx.emit(PanelEvent::LayoutChanged) cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| {
})]; cx.emit(PanelEvent::LayoutChanged)
}),
];
Self { Self {
axis, axis,
@ -253,7 +255,7 @@ impl StackPanel {
Some(size) => size, Some(size) => size,
None => { None => {
let state = self.state.read(cx); 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 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; use crate::PixelsExt;
@ -66,23 +68,56 @@ impl ResizableState {
..Default::default() ..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 { if let Some(ix) = ix {
self.panels.insert(ix, panel_state); self.panels.insert(ix, panel_state);
self.sizes.insert(ix, size.unwrap_or(PANEL_MIN_SIZE)); self.sizes.insert(ix, size);
} else { } else {
self.panels.push(panel_state); self.panels.push(panel_state);
self.sizes.push(size.unwrap_or(PANEL_MIN_SIZE)); self.sizes.push(size);
}; };
cx.notify(); 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; self.axis = axis;
if panels_count > self.panels.len() { if panels_count > self.panels.len() {
let diff = panels_count - self.panels.len(); let diff = panels_count - self.panels.len();
self.panels self.panels
.extend(vec![ResizablePanelState::default(); diff]); .extend(vec![ResizablePanelState::default(); diff]);
self.sizes.extend(vec![PANEL_MIN_SIZE; 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); self.resizing_panel_ix = Some(resizing_panel_ix - 1);
} }
} }
cx.notify(); self.adjust_to_container_size(cx);
} }
pub(crate) fn replace_panel( pub(crate) fn replace_panel(
@ -127,7 +162,7 @@ impl ResizableState {
self.panels[panel_ix] = panel; self.panels[panel_ix] = panel;
self.sizes[panel_ix] = old_size; self.sizes[panel_ix] = old_size;
cx.notify(); self.adjust_to_container_size(cx);
} }
pub(crate) fn clear(&mut self) { pub(crate) fn clear(&mut self) {
@ -135,8 +170,9 @@ impl ResizableState {
self.sizes.clear(); self.sizes.clear();
} }
pub(crate) fn total_size(&self) -> Pixels { #[inline]
self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into() pub(crate) fn container_size(&self) -> Pixels {
self.bounds.size.along(self.axis)
} }
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) { pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
@ -168,7 +204,7 @@ impl ResizableState {
if ix >= old_sizes.len() - 1 { if ix >= old_sizes.len() - 1 {
return; return;
} }
let container_size = self.bounds.size.along(self.axis); let container_size = self.container_size();
self.sync_real_panel_sizes(cx); self.sync_real_panel_sizes(cx);
let move_changed = size - old_sizes[ix]; let move_changed = size - old_sizes[ix];
@ -226,21 +262,25 @@ impl ResizableState {
cx.notify(); 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. /// 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>) { fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
// At least 1px so we don't divide by zero. if self.container_size().is_zero() {
let container_size = self.bounds.size.along(self.axis).max(px(1.)); return;
let total_size = self.total_size(); }
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() { for i in 0..self.panels.len() {
let size = self.sizes[i]; let size = self.sizes[i];
let fractional_size = size / total_size; let ratio = size / total_size;
let new_size = container_size * fractional_size; let new_size = container_size * ratio;
self.sizes[i] = new_size; self.sizes[i] = new_size;
self.panels[i].size = Some(new_size); self.panels[i].size = Some(new_size);
} }
cx.notify(); cx.notify();
} }
} }

View file

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