resizable: Improve to avoid panels from improperly resizing on container resizing. (#1560)

This pull request changes the internal behavior of the resizable panel
to get rid of some weird behavior with many panels and resizing.

Previously, when having multiple panels and dragging them into each
other, panels that shouldn't be affected got improperly resized:


https://github.com/user-attachments/assets/20d280c0-8a51-4e96-9c4a-3ade3deaca02

This pull request completely fixes this behavior:


https://github.com/user-attachments/assets/7c207dd8-d0a0-47d3-8d9e-c7127f43fa73

In addition, dragging the container smaller and larger again wasn't
idempotent:


https://github.com/user-attachments/assets/ca54b039-fd79-4fa8-87cf-590c06dd2c18

This is also fixed by this pull request, replacing the behavior with
more intuitive behavior, keeping the fractional size of all panels
constant:


https://github.com/user-attachments/assets/365d0a99-fd4b-4948-bc1b-bafdd826394a


https://github.com/user-attachments/assets/b208b34f-18e7-42a4-825a-9dad421b7ad3

If desired, it would also be easy to only change the size of the panels
that don't have a default size set when the container changes size.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
Nico Gründel 2025-11-12 14:18:05 +01:00 committed by GitHub
parent 1074f78fb0
commit 5de9a24654
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 11 deletions

View file

@ -94,8 +94,13 @@ impl ResizableState {
cx: &mut Context<Self>,
) {
let size = bounds.size.along(self.axis);
self.sizes[panel_ix] = size;
self.panels[panel_ix].size = Some(size);
// This check is only necessary to stop the very first panel from resizing on its own
// it needs to be passed when the panel is freshly created so we get the initial size,
// but its also fine when it sometimes passes later.
if self.sizes[panel_ix].as_f32() == PANEL_MIN_SIZE.as_f32() {
self.sizes[panel_ix] = size;
self.panels[panel_ix].size = Some(size);
}
self.panels[panel_ix].bounds = bounds;
self.panels[panel_ix].size_range = size_range;
cx.notify();
@ -149,7 +154,7 @@ impl ResizableState {
fn sync_real_panel_sizes(&mut self, _: &App) {
for (i, panel) in self.panels.iter().enumerate() {
self.sizes[i] = panel.bounds.size.along(self.axis).floor();
self.sizes[i] = panel.bounds.size.along(self.axis);
}
}
@ -163,7 +168,6 @@ impl ResizableState {
if ix >= old_sizes.len() - 1 {
return;
}
let size = size.floor();
let container_size = self.bounds.size.along(self.axis);
self.sync_real_panel_sizes(cx);
@ -193,7 +197,6 @@ impl ResizableState {
}
} else {
let mut changed = new_size - size;
new_sizes[ix + 1] += old_sizes[ix] - new_size;
new_sizes[ix] = new_size;
while changed > px(0.) && ix > 0 {
@ -204,6 +207,8 @@ impl ResizableState {
changed -= to_reduce;
new_sizes[ix] -= to_reduce;
}
new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
}
// If total size exceeds container size, adjust the main panel
@ -217,10 +222,27 @@ impl ResizableState {
let size = new_sizes[i];
self.panels[i].size = Some(size);
}
self.sizes = new_sizes;
cx.notify();
}
/// 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();
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;
self.sizes[i] = new_size;
self.panels[i].size = Some(new_size);
}
cx.notify();
}
}
impl EventEmitter<ResizablePanelEvent> for ResizableState {}

View file

@ -4,9 +4,10 @@ use std::{
};
use gpui::{
canvas, div, prelude::FluentBuilder, AnyElement, App, AppContext, Axis, Bounds, Context,
Element, ElementId, Empty, Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
canvas, div, prelude::FluentBuilder, Along, AnyElement, App, AppContext, Axis, Bounds, Context,
Element, ElementId, Empty, Entity, EventEmitter, InteractiveElement as _, IntoElement,
IsZero as _, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style,
Styled, Window,
};
use crate::{h_flex, resizable::PANEL_MIN_SIZE, v_flex, AxisExt};
@ -157,7 +158,18 @@ impl RenderOnce for ResizablePanelGroup {
canvas(
{
let state = state.clone();
move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
move |bounds, _, cx| {
state.update(cx, |state, cx| {
let size_changed = state.bounds.size.along(self.axis)
!= bounds.size.along(self.axis);
state.bounds = bounds;
if size_changed {
state.adjust_to_container_size(cx);
}
})
}
},
|_, _, _, _| {},
)
@ -269,7 +281,7 @@ impl RenderOnce for ResizablePanel {
.flex_basis(initial_size)
})
.map(|this| match panel_state.size {
Some(size) => this.flex_basis(size),
Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
None => this,
})
.child({