resizable: Add size_range to limit panel size and fix resize and initial_size of panel. (#844)

## Fix panel resize bug

- Fix some panel resizing details.
- Fix initial_size for panel and ensure split dock panel to get average
size.

### Before


https://github.com/user-attachments/assets/cd6c7e10-aec7-46a6-8a4d-11bf4d221f92

### After


https://github.com/user-attachments/assets/90c8b865-630a-4abd-98c9-310800b12fee
This commit is contained in:
Jason Lee 2025-05-12 11:07:06 +08:00 committed by GitHub
parent 28582bc905
commit 0a95f7c0f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 149 additions and 107 deletions

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, px, AnyElement, App, AppContext, Context, Entity, Focusable, IntoElement, div, px, AnyElement, App, AppContext, Context, Entity, Focusable, IntoElement,
ParentElement as _, Render, SharedString, Styled, Window, ParentElement as _, Pixels, Render, SharedString, Styled, Window,
}; };
use gpui_component::{ use gpui_component::{
resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanelGroup}, resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanelGroup},
@ -56,34 +56,32 @@ impl ResizableStory {
.size(px(150.)) .size(px(150.))
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(150.))
.content(|_, cx| panel_box("Left 1 (Min 120px)", cx)), .size_range(px(120.)..px(300.))
.content(|_, cx| panel_box("Left (120px .. 300px)", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel().content(|_, cx| panel_box("Center", cx)),
.size(px(400.))
.content(|_, cx| panel_box("Center 1", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.content(|_, cx| panel_box("Right (Grow)", cx)), .content(|_, cx| panel_box("Right", cx)),
cx, cx,
), ),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel().content(|_, cx| panel_box("Center", cx)),
.size(px(150.))
.content(|_, cx| panel_box("Center (Grow)", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(210.)) .size(px(80.))
.content(|_, cx| panel_box("Bottom", cx)), .size_range(px(80.)..Pixels::MAX)
.content(|_, cx| panel_box("Bottom (80px .. 150px)", cx)),
cx, cx,
) )
}); });
@ -92,14 +90,13 @@ impl ResizableStory {
h_resizable() h_resizable()
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(200.))
.size_range(px(200.)..px(400.))
.content(|_, cx| panel_box("Left 2", cx)), .content(|_, cx| panel_box("Left 2", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel().content(|_, cx| panel_box("Right (Grow)", cx)),
.size(px(400.))
.content(|_, cx| panel_box("Right (Grow)", cx)),
cx, cx,
) )
}); });
@ -114,8 +111,9 @@ impl ResizableStory {
impl Render for ResizableStory { impl Render for ResizableStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full()
.gap_6() .gap_6()
.child(self.group1.clone()) .child(div().h(px(800.)).child(self.group1.clone()))
.child(self.group2.clone()) .child(self.group2.clone())
} }
} }

View file

@ -5,7 +5,7 @@ use crate::{
h_flex, h_flex,
resizable::{ resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup, ResizablePanelGroup, PANEL_MIN_SIZE,
}, },
ActiveTheme, AxisExt as _, Placement, ActiveTheme, AxisExt as _, Placement,
}; };
@ -238,10 +238,20 @@ impl StackPanel {
ix ix
}; };
// Get avg size of all panels to insert new panel, if size is None.
let size = match size {
Some(size) => size,
None => {
let panel_group = self.panel_group.read(cx);
(panel_group.total_size() / (panel_group.sizes().len() + 1) as f32)
.max(PANEL_MIN_SIZE)
}
};
self.panels.insert(ix, panel.clone()); self.panels.insert(ix, panel.clone());
self.panel_group.update(cx, |view, cx| { self.panel_group.update(cx, |view, cx| {
view.insert_child( view.insert_child(
Self::new_resizable_panel(panel.clone(), size), Self::new_resizable_panel(panel.clone(), Some(size)),
ix, ix,
window, window,
cx, cx,

View file

@ -1,14 +1,17 @@
use std::{ops::Deref, rc::Rc}; use std::{
ops::{Deref, Range},
rc::Rc,
};
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext, canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, App, AppContext, Axis,
Axis, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, Style, Styled, WeakEntity, Window, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, Style, Styled, WeakEntity, Window,
}; };
use crate::{h_flex, v_flex, AxisExt}; use crate::{h_flex, v_flex, AxisExt};
use super::resize_handle; use super::{resizable_panel, resize_handle};
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
@ -31,6 +34,7 @@ pub struct ResizablePanelGroup {
sizes: Vec<Pixels>, sizes: Vec<Pixels>,
axis: Axis, axis: Axis,
size: Option<Pixels>, size: Option<Pixels>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>, resizing_panel_ix: Option<usize>,
} }
@ -63,20 +67,19 @@ impl ResizablePanelGroup {
cx.notify(); cx.notify();
} }
/// Add a resizable panel to the group. /// Add a panel to the group.
///
/// - The `axis` will be set to the same axis as the group.
/// - The `initial_size` will be set to the average size of all panels if not provided.
/// - The `group` will be set to the group entity.
pub fn child(mut self, panel: ResizablePanel, cx: &mut Context<Self>) -> Self { pub fn child(mut self, panel: ResizablePanel, cx: &mut Context<Self>) -> Self {
self.add_child(panel, cx); self._insert_child(panel, self.panels.len(), cx);
self self
} }
/// Add a ResizablePanelGroup as a child to the group. /// Add a ResizablePanelGroup as a child to the group.
pub fn group(self, group: ResizablePanelGroup, cx: &mut Context<Self>) -> Self { pub fn group(self, group: ResizablePanelGroup, cx: &mut Context<Self>) -> Self {
let group: ResizablePanelGroup = group; self.child(resizable_panel().content_view(cx.new(|_| group).into()), cx)
let size = group.size;
let panel = ResizablePanel::new()
.content_view(cx.new(|_| group).into())
.when_some(size, |this, size| this.size(size));
self.child(panel, cx)
} }
/// Set size of the resizable panel group /// Set size of the resizable panel group
@ -98,29 +101,46 @@ impl ResizablePanelGroup {
self.sizes.iter().fold(px(0.0), |acc, &size| acc + size) self.sizes.iter().fold(px(0.0), |acc, &size| acc + size)
} }
pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut Context<Self>) { /// Insert child to panel group.
let mut panel = panel; ///
panel.axis = self.axis; /// - The `ix` is the index of the panel to insert.
panel.group = Some(cx.entity().downgrade()); /// - The `axis` will be set to the same axis as the group.
self.sizes.push(panel.initial_size.unwrap_or_default()); /// - The `initial_size` will be set to the average size of all panels if not provided.
self.panels.push(cx.new(|_| panel)); /// - The `group` will be set to the group entity.
}
pub fn insert_child( pub fn insert_child(
&mut self, &mut self,
panel: ResizablePanel, panel: ResizablePanel,
ix: usize, ix: usize,
_: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self._insert_child(panel, ix, cx);
window.on_next_frame({
let view = cx.entity();
move |window, cx| {
view.update(cx, |this, cx| {
this.sync_real_panel_sizes(window, cx);
})
}
});
cx.notify()
}
fn _insert_child(&mut self, panel: ResizablePanel, ix: usize, cx: &mut Context<Self>) {
let mut panel = panel; let mut panel = panel;
panel.axis = self.axis; panel.axis = self.axis;
panel.group = Some(cx.entity().downgrade()); panel.group = Some(cx.entity().downgrade());
let initial_size = match panel.initial_size {
// Use the initial size if provided.
Some(size) => size,
// Split to add child, use average size of all panels
None => (self.total_size() / (self.panels.len() + 1) as f32).max(PANEL_MIN_SIZE),
};
self.sizes // Here we need allows `initial_size` is none, for some children use flex auto size.
.insert(ix, panel.initial_size.unwrap_or_default());
self.sizes.insert(ix, initial_size);
self.panels.insert(ix, cx.new(|_| panel)); self.panels.insert(ix, cx.new(|_| panel));
cx.notify()
} }
/// Replace a child panel with a new panel at the given index. /// Replace a child panel with a new panel at the given index.
@ -131,17 +151,14 @@ impl ResizablePanelGroup {
_: &mut Window, _: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let old_panel = self.panels[ix].read(cx);
let mut panel = panel; let mut panel = panel;
panel.initial_size = old_panel.initial_size;
let old_panel = self.panels[ix].clone(); panel.size = old_panel.size;
let old_panel_initial_size = old_panel.read(cx).initial_size;
let old_panel_size_ratio = old_panel.read(cx).size_ratio;
panel.initial_size = old_panel_initial_size;
panel.size_ratio = old_panel_size_ratio;
panel.axis = self.axis; panel.axis = self.axis;
panel.group = Some(cx.entity().downgrade()); panel.group = Some(cx.entity().downgrade());
self.sizes[ix] = panel.initial_size.unwrap_or_default();
self.panels[ix] = cx.new(|_| panel); self.panels[ix] = cx.new(|_| panel);
cx.notify() cx.notify()
} }
@ -185,10 +202,18 @@ impl ResizablePanelGroup {
fn sync_real_panel_sizes(&mut self, _: &Window, cx: &App) { fn sync_real_panel_sizes(&mut self, _: &Window, cx: &App) {
for (i, panel) in self.panels.iter().enumerate() { for (i, panel) in self.panels.iter().enumerate() {
self.sizes[i] = panel.read(cx).bounds.size.along(self.axis) self.sizes[i] = panel.read(cx).bounds.size.along(self.axis).floor();
} }
} }
fn panel_size_range(&self, ix: usize, cx: &App) -> Range<Pixels> {
let Some(panel) = self.panels.get(ix) else {
return PANEL_MIN_SIZE..Pixels::MAX;
};
panel.read(cx).size_range.clone()
}
/// The `ix`` is the index of the panel to resize, /// The `ix`` is the index of the panel to resize,
/// and the `size` is the new size for the panel. /// and the `size` is the new size for the panel.
fn resize_panels( fn resize_panels(
@ -205,38 +230,43 @@ impl ResizablePanelGroup {
} }
let size = size.floor(); let size = size.floor();
let container_size = self.bounds.size.along(self.axis); let container_size = self.bounds.size.along(self.axis);
self.sync_real_panel_sizes(window, cx); self.sync_real_panel_sizes(window, cx);
let mut changed = size - self.sizes[ix]; let move_changed = size - self.sizes[ix];
let is_expand = changed > px(0.); if move_changed == px(0.) {
return;
}
let size_range = self.panel_size_range(ix, cx);
let new_size = size.clamp(size_range.start, size_range.end);
let is_expand = move_changed > px(0.);
let main_ix = ix; let main_ix = ix;
let mut new_sizes = self.sizes.clone(); let mut new_sizes = self.sizes.clone();
if is_expand { if is_expand {
new_sizes[ix] = size; let mut changed = new_size - self.sizes[ix];
new_sizes[ix] = new_size;
// Now to expand logic is correct.
while changed > px(0.) && ix < self.panels.len() - 1 { while changed > px(0.) && ix < self.panels.len() - 1 {
ix += 1; ix += 1;
let available_size = (new_sizes[ix] - PANEL_MIN_SIZE).max(px(0.)); let size_range = self.panel_size_range(ix, cx);
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
let to_reduce = changed.min(available_size); let to_reduce = changed.min(available_size);
new_sizes[ix] -= to_reduce; new_sizes[ix] -= to_reduce;
changed -= to_reduce; changed -= to_reduce;
} }
} else { } else {
let new_size = size.max(PANEL_MIN_SIZE); let mut changed = new_size - size;
new_sizes[ix] = new_size;
changed = size - PANEL_MIN_SIZE;
new_sizes[ix + 1] += self.sizes[ix] - new_size; new_sizes[ix + 1] += self.sizes[ix] - new_size;
new_sizes[ix] = new_size;
while changed < px(0.) && ix > 0 { while changed > px(0.) && ix > 0 {
ix -= 1; ix -= 1;
let available_size = self.sizes[ix] - PANEL_MIN_SIZE; let size_range = self.panel_size_range(ix, cx);
let to_increase = (changed).min(available_size); let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
new_sizes[ix] += to_increase; let to_reduce = changed.min(available_size);
changed += to_increase; changed -= to_reduce;
new_sizes[ix] -= to_reduce;
} }
} }
@ -244,20 +274,19 @@ impl ResizablePanelGroup {
let total_size: Pixels = new_sizes.iter().map(|s| s.0).sum::<f32>().into(); let total_size: Pixels = new_sizes.iter().map(|s| s.0).sum::<f32>().into();
if total_size > container_size { if total_size > container_size {
let overflow = total_size - container_size; let overflow = total_size - container_size;
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(PANEL_MIN_SIZE); new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
} }
let total_size = new_sizes.iter().fold(px(0.0), |acc, &size| acc + size);
self.sizes = new_sizes;
for (i, panel) in self.panels.iter().enumerate() { for (i, panel) in self.panels.iter().enumerate() {
let size = self.sizes[i]; let size = new_sizes[i];
if size > px(0.) { let is_changed = self.sizes[i] != size;
if size > px(0.) && is_changed {
panel.update(cx, |this, _| { panel.update(cx, |this, _| {
this.size = Some(size); this.size = Some(size);
this.size_ratio = Some(size / total_size);
}); });
} }
} }
self.sizes = new_sizes;
} }
} }
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {} impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
@ -303,8 +332,8 @@ pub struct ResizablePanel {
initial_size: Option<Pixels>, initial_size: Option<Pixels>,
/// size is the size that the panel has when it is resized or adjusted by flex layout. /// size is the size that the panel has when it is resized or adjusted by flex layout.
size: Option<Pixels>, size: Option<Pixels>,
/// the size ratio that the panel has relative to its group /// size range limit of this panel.
size_ratio: Option<f32>, size_range: Range<Pixels>,
axis: Axis, axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>, content_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
content_view: Option<AnyView>, content_view: Option<AnyView>,
@ -320,7 +349,7 @@ impl ResizablePanel {
group: None, group: None,
initial_size: None, initial_size: None,
size: None, size: None,
size_ratio: None, size_range: (PANEL_MIN_SIZE..Pixels::MAX),
axis: Axis::Horizontal, axis: Axis::Horizontal,
content_builder: None, content_builder: None,
content_view: None, content_view: None,
@ -352,18 +381,28 @@ impl ResizablePanel {
} }
/// Set the initial size of the panel. /// Set the initial size of the panel.
pub fn size(mut self, size: Pixels) -> Self { pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.initial_size = Some(size); self.initial_size = Some(size.into());
self
}
/// Set the size range to limit panel resize.
///
/// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
self.size_range = range.into();
self self
} }
/// Save the real panel size, and update group sizes /// Save the real panel size, and update group sizes
fn update_size(&mut self, bounds: Bounds<Pixels>, _: &mut Window, cx: &mut Context<Self>) { fn update_size(&mut self, bounds: Bounds<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
let new_size = bounds.size.along(self.axis);
self.bounds = bounds; self.bounds = bounds;
self.size_ratio = None; let new_size = bounds.size.along(self.axis);
self.size = Some(new_size); if self.size == Some(new_size) {
return;
}
self.size = Some(new_size);
let entity_id = cx.entity().entity_id(); let entity_id = cx.entity().entity_id();
if let Some(group) = self.group.as_ref() { if let Some(group) = self.group.as_ref() {
_ = group.update(cx, |view, _| { _ = group.update(cx, |view, _| {
@ -372,7 +411,7 @@ impl ResizablePanel {
} }
}); });
} }
cx.notify(); // cx.notify();
} }
} }
@ -388,39 +427,34 @@ impl Render for ResizablePanel {
} }
let view = cx.entity().clone(); let view = cx.entity().clone();
let total_size = self let size_range = self.size_range.clone();
.group
.as_ref()
.and_then(|group| group.upgrade())
.map(|group| group.read(cx).total_size());
div() div()
.flex() .flex()
.flex_grow() .flex_grow()
.size_full() .size_full()
.relative() .relative()
.when(self.initial_size.is_none(), |this| this.flex_shrink()) .when(self.axis.is_vertical(), |this| {
.when(self.axis.is_vertical(), |this| this.min_h(PANEL_MIN_SIZE)) this.min_h(size_range.start).max_h(size_range.end)
.when(self.axis.is_horizontal(), |this| this.min_w(PANEL_MIN_SIZE))
.when_some(self.initial_size, |this, size| {
if size.is_zero() {
this
} else {
// The `self.size` is None, that mean the initial size for the panel, so we need set flex_shrink_0
// To let it keep the initial size.
this.when(self.size.is_none() && size > px(0.), |this| {
this.flex_shrink_0()
})
.flex_basis(size)
}
}) })
.map(|this| match (self.size_ratio, self.size, total_size) { .when(self.axis.is_horizontal(), |this| {
(Some(size_ratio), _, _) => this.flex_basis(relative(size_ratio)), this.min_w(size_range.start).max_w(size_range.end)
(None, Some(size), Some(total_size)) => { })
this.flex_basis(relative(size / total_size)) // 1. initial_size is None, to use auto size.
} // 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
(None, Some(size), None) => this.flex_basis(size), // 3. initial_size is Some and size is Some, use `size`.
_ => this, .when(self.initial_size.is_none(), |this| this.flex_shrink())
.when_some(self.initial_size, |this, initial_size| {
// The `self.size` is None, that mean the initial size for the panel,
// so we need set `flex_shrink_0` To let it keep the initial size.
this.when(self.size.is_none() && !initial_size.is_zero(), |this| {
this.flex_shrink_0()
})
.flex_basis(initial_size)
})
.map(|this| match self.size {
Some(size) => this.flex_basis(size),
None => this,
}) })
.child({ .child({
canvas( canvas(