From 0a95f7c0f17e6a409c9214e56d293a9ec79e7efc Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 May 2025 11:07:06 +0800 Subject: [PATCH] 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 --- crates/story/src/resizable_story.rs | 32 ++--- crates/ui/src/dock/stack_panel.rs | 14 +- crates/ui/src/resizable/panel.rs | 210 ++++++++++++++++------------ 3 files changed, 149 insertions(+), 107 deletions(-) diff --git a/crates/story/src/resizable_story.rs b/crates/story/src/resizable_story.rs index 644524c1..7a128741 100644 --- a/crates/story/src/resizable_story.rs +++ b/crates/story/src/resizable_story.rs @@ -1,6 +1,6 @@ use gpui::{ 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::{ resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanelGroup}, @@ -56,34 +56,32 @@ impl ResizableStory { .size(px(150.)) .child( resizable_panel() - .size(px(300.)) - .content(|_, cx| panel_box("Left 1 (Min 120px)", cx)), + .size(px(150.)) + .size_range(px(120.)..px(300.)) + .content(|_, cx| panel_box("Left (120px .. 300px)", cx)), cx, ) .child( - resizable_panel() - .size(px(400.)) - .content(|_, cx| panel_box("Center 1", cx)), + resizable_panel().content(|_, cx| panel_box("Center", cx)), cx, ) .child( resizable_panel() .size(px(300.)) - .content(|_, cx| panel_box("Right (Grow)", cx)), + .content(|_, cx| panel_box("Right", cx)), cx, ), cx, ) .child( - resizable_panel() - .size(px(150.)) - .content(|_, cx| panel_box("Center (Grow)", cx)), + resizable_panel().content(|_, cx| panel_box("Center", cx)), cx, ) .child( resizable_panel() - .size(px(210.)) - .content(|_, cx| panel_box("Bottom", cx)), + .size(px(80.)) + .size_range(px(80.)..Pixels::MAX) + .content(|_, cx| panel_box("Bottom (80px .. 150px)", cx)), cx, ) }); @@ -92,14 +90,13 @@ impl ResizableStory { h_resizable() .child( resizable_panel() - .size(px(300.)) + .size(px(200.)) + .size_range(px(200.)..px(400.)) .content(|_, cx| panel_box("Left 2", cx)), cx, ) .child( - resizable_panel() - .size(px(400.)) - .content(|_, cx| panel_box("Right (Grow)", cx)), + resizable_panel().content(|_, cx| panel_box("Right (Grow)", cx)), cx, ) }); @@ -114,8 +111,9 @@ impl ResizableStory { impl Render for ResizableStory { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() + .size_full() .gap_6() - .child(self.group1.clone()) + .child(div().h(px(800.)).child(self.group1.clone())) .child(self.group2.clone()) } } diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index 25eb2aa7..363b3bf9 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -5,7 +5,7 @@ use crate::{ h_flex, resizable::{ h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, - ResizablePanelGroup, + ResizablePanelGroup, PANEL_MIN_SIZE, }, ActiveTheme, AxisExt as _, Placement, }; @@ -238,10 +238,20 @@ impl StackPanel { 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.panel_group.update(cx, |view, cx| { view.insert_child( - Self::new_resizable_panel(panel.clone(), size), + Self::new_resizable_panel(panel.clone(), Some(size)), ix, window, cx, diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 4c35a07f..36043897 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -1,14 +1,17 @@ -use std::{ops::Deref, rc::Rc}; +use std::{ + ops::{Deref, Range}, + rc::Rc, +}; use gpui::{ - canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext, - Axis, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero, + canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, App, AppContext, Axis, + Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, Style, Styled, WeakEntity, Window, }; 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.); @@ -31,6 +34,7 @@ pub struct ResizablePanelGroup { sizes: Vec, axis: Axis, size: Option, + bounds: Bounds, resizing_panel_ix: Option, } @@ -63,20 +67,19 @@ impl ResizablePanelGroup { 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.add_child(panel, cx); + self._insert_child(panel, self.panels.len(), cx); self } /// Add a ResizablePanelGroup as a child to the group. pub fn group(self, group: ResizablePanelGroup, cx: &mut Context) -> Self { - let group: ResizablePanelGroup = group; - 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) + self.child(resizable_panel().content_view(cx.new(|_| group).into()), cx) } /// Set size of the resizable panel group @@ -98,29 +101,46 @@ impl ResizablePanelGroup { self.sizes.iter().fold(px(0.0), |acc, &size| acc + size) } - pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut Context) { - let mut panel = panel; - panel.axis = self.axis; - panel.group = Some(cx.entity().downgrade()); - self.sizes.push(panel.initial_size.unwrap_or_default()); - self.panels.push(cx.new(|_| panel)); - } - + /// Insert child to panel group. + /// + /// - The `ix` is the index of the panel to insert. + /// - 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 insert_child( &mut self, panel: ResizablePanel, ix: usize, - _: &mut Window, + window: &mut Window, cx: &mut Context, ) { + 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) { let mut panel = panel; panel.axis = self.axis; 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 - .insert(ix, panel.initial_size.unwrap_or_default()); + // Here we need allows `initial_size` is none, for some children use flex auto size. + + self.sizes.insert(ix, initial_size); self.panels.insert(ix, cx.new(|_| panel)); - cx.notify() } /// Replace a child panel with a new panel at the given index. @@ -131,17 +151,14 @@ impl ResizablePanelGroup { _: &mut Window, cx: &mut Context, ) { + let old_panel = self.panels[ix].read(cx); + let mut panel = panel; - - let old_panel = self.panels[ix].clone(); - 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.initial_size = old_panel.initial_size; + panel.size = old_panel.size; panel.axis = self.axis; panel.group = Some(cx.entity().downgrade()); - self.sizes[ix] = panel.initial_size.unwrap_or_default(); + self.panels[ix] = cx.new(|_| panel); cx.notify() } @@ -185,10 +202,18 @@ impl ResizablePanelGroup { fn sync_real_panel_sizes(&mut self, _: &Window, cx: &App) { 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 { + 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, /// and the `size` is the new size for the panel. fn resize_panels( @@ -205,38 +230,43 @@ impl ResizablePanelGroup { } let size = size.floor(); let container_size = self.bounds.size.along(self.axis); - self.sync_real_panel_sizes(window, cx); - let mut changed = size - self.sizes[ix]; - let is_expand = changed > px(0.); + let move_changed = size - self.sizes[ix]; + 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 mut new_sizes = self.sizes.clone(); 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 { 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); new_sizes[ix] -= to_reduce; changed -= to_reduce; } } else { - let new_size = size.max(PANEL_MIN_SIZE); - new_sizes[ix] = new_size; - changed = size - PANEL_MIN_SIZE; + let mut changed = new_size - 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; - let available_size = self.sizes[ix] - PANEL_MIN_SIZE; - let to_increase = (changed).min(available_size); - new_sizes[ix] += to_increase; - changed += to_increase; + 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); + 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::().into(); if 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() { - let size = self.sizes[i]; - if size > px(0.) { + let size = new_sizes[i]; + let is_changed = self.sizes[i] != size; + if size > px(0.) && is_changed { panel.update(cx, |this, _| { this.size = Some(size); - this.size_ratio = Some(size / total_size); }); } } + self.sizes = new_sizes; } } impl EventEmitter for ResizablePanelGroup {} @@ -303,8 +332,8 @@ pub struct ResizablePanel { initial_size: Option, /// size is the size that the panel has when it is resized or adjusted by flex layout. size: Option, - /// the size ratio that the panel has relative to its group - size_ratio: Option, + /// size range limit of this panel. + size_range: Range, axis: Axis, content_builder: Option AnyElement>>, content_view: Option, @@ -320,7 +349,7 @@ impl ResizablePanel { group: None, initial_size: None, size: None, - size_ratio: None, + size_range: (PANEL_MIN_SIZE..Pixels::MAX), axis: Axis::Horizontal, content_builder: None, content_view: None, @@ -352,18 +381,28 @@ impl ResizablePanel { } /// Set the initial size of the panel. - pub fn size(mut self, size: Pixels) -> Self { - self.initial_size = Some(size); + pub fn size(mut self, size: impl Into) -> Self { + 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>) -> Self { + self.size_range = range.into(); self } /// Save the real panel size, and update group sizes fn update_size(&mut self, bounds: Bounds, _: &mut Window, cx: &mut Context) { - let new_size = bounds.size.along(self.axis); self.bounds = bounds; - self.size_ratio = None; - self.size = Some(new_size); + let new_size = bounds.size.along(self.axis); + if self.size == Some(new_size) { + return; + } + self.size = Some(new_size); let entity_id = cx.entity().entity_id(); if let Some(group) = self.group.as_ref() { _ = 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 total_size = self - .group - .as_ref() - .and_then(|group| group.upgrade()) - .map(|group| group.read(cx).total_size()); + let size_range = self.size_range.clone(); div() .flex() .flex_grow() .size_full() .relative() - .when(self.initial_size.is_none(), |this| this.flex_shrink()) - .when(self.axis.is_vertical(), |this| this.min_h(PANEL_MIN_SIZE)) - .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) - } + .when(self.axis.is_vertical(), |this| { + this.min_h(size_range.start).max_h(size_range.end) }) - .map(|this| match (self.size_ratio, self.size, total_size) { - (Some(size_ratio), _, _) => this.flex_basis(relative(size_ratio)), - (None, Some(size), Some(total_size)) => { - this.flex_basis(relative(size / total_size)) - } - (None, Some(size), None) => this.flex_basis(size), - _ => this, + .when(self.axis.is_horizontal(), |this| { + this.min_w(size_range.start).max_w(size_range.end) + }) + // 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. + // 3. initial_size is Some and size is Some, use `size`. + .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({ canvas(