From 7b5efd19bb471c629a1d9cae1fb20072340da317 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 27 Aug 2024 18:38:37 +0800 Subject: [PATCH] Better Panel resize. (#178) - Resize panels when windows is resize to smaller #175. https://github.com/user-attachments/assets/b2c50949-7310-4d3d-8276-5bf15cf3ba9f --- crates/story/src/resizable_story.rs | 15 +- crates/ui/src/dock/stack_panel.rs | 9 +- crates/ui/src/dock/tab_panel.rs | 4 + crates/ui/src/resizable/mod.rs | 10 +- crates/ui/src/resizable/panel.rs | 286 ++++++++++++++++++---------- 5 files changed, 197 insertions(+), 127 deletions(-) diff --git a/crates/story/src/resizable_story.rs b/crates/story/src/resizable_story.rs index 5506c578..54219d89 100644 --- a/crates/story/src/resizable_story.rs +++ b/crates/story/src/resizable_story.rs @@ -30,28 +30,25 @@ impl ResizableStory { } let group1 = cx.new_view(|cx| { - v_resizable() + v_resizable(cx) .group( - h_resizable() + h_resizable(cx) .size(px(150.)) .child( resizable_panel() .size(px(300.)) - .min_size(px(120.)) .content(|cx| panel_box("Left 1 (Min 120px)", cx)), cx, ) .child( resizable_panel() .size(px(400.)) - .min_size(px(100.)) .content(|cx| panel_box("Center 1", cx)), cx, ) .child( resizable_panel() .size(px(300.)) - .min_size(px(100.)) .content(|cx| panel_box("Right (Grow)", cx)), cx, ), @@ -60,34 +57,28 @@ impl ResizableStory { .child( resizable_panel() .size(px(150.)) - .max_size(px(550.)) - .min_size(px(100.)) .content(|cx| panel_box("Center (Grow)", cx)), cx, ) .child( resizable_panel() .size(px(210.)) - .min_size(px(100.)) .content(|cx| panel_box("Bottom", cx)), cx, ) }); let group2 = cx.new_view(|cx| { - h_resizable() + h_resizable(cx) .child( resizable_panel() .size(px(300.)) - .min_size(px(100.)) .content(|cx| panel_box("Left 2", cx)), cx, ) .child( resizable_panel() .size(px(400.)) - .max_size(px(550.)) - .min_size(px(100.)) .content(|cx| panel_box("Right (Grow)", cx)), cx, ) diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index a44ac589..f7979166 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -9,7 +9,7 @@ use crate::{ use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; use gpui::{ - prelude::FluentBuilder as _, px, Axis, DismissEvent, Entity, EventEmitter, FocusHandle, + prelude::FluentBuilder as _, Axis, DismissEvent, Entity, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, VisualContext, WeakView, }; @@ -32,11 +32,11 @@ impl StackPanel { parent: None, focus_handle: cx.focus_handle(), panels: SmallVec::new(), - panel_group: cx.new_view(|_| { + panel_group: cx.new_view(|cx| { if axis == Axis::Horizontal { - h_resizable() + h_resizable(cx) } else { - v_resizable() + v_resizable(cx) } }), } @@ -125,7 +125,6 @@ impl StackPanel { { resizable_panel() .content_view(panel.view()) - .min_size(px(100.)) .when_some(size, |this, size| this.size(size)) } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index fa609c51..6b188a4a 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -39,6 +39,7 @@ impl DragPanel { impl Render for DragPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() + .id("drag-panel") .cursor_grab() .py_1() .px_3() @@ -214,6 +215,8 @@ impl TabPanel { .py_2() .px_3() .min_w_16() + .overflow_hidden() + .whitespace_nowrap() .child(panel.title(cx)) .on_drag( DragPanel { @@ -237,6 +240,7 @@ impl TabPanel { .children(self.panels.iter().enumerate().map(|(ix, panel)| { let active = ix == self.active_ix; Tab::new(("tab", ix), panel.title(cx)) + .py_2() .selected(active) .on_click(cx.listener(move |view, _, cx| { view.set_active_ix(ix, cx); diff --git a/crates/ui/src/resizable/mod.rs b/crates/ui/src/resizable/mod.rs index aca31f3f..287f6e67 100644 --- a/crates/ui/src/resizable/mod.rs +++ b/crates/ui/src/resizable/mod.rs @@ -1,14 +1,14 @@ -use gpui::Axis; +use gpui::{Axis, ViewContext}; mod panel; pub use panel::*; -pub fn h_resizable() -> ResizablePanelGroup { - ResizablePanelGroup::new().axis(Axis::Horizontal) +pub fn h_resizable(cx: &mut ViewContext) -> ResizablePanelGroup { + ResizablePanelGroup::new(cx).axis(Axis::Horizontal) } -pub fn v_resizable() -> ResizablePanelGroup { - ResizablePanelGroup::new().axis(Axis::Vertical) +pub fn v_resizable(cx: &mut ViewContext) -> ResizablePanelGroup { + ResizablePanelGroup::new(cx).axis(Axis::Vertical) } pub fn resizable_panel() -> ResizablePanel { diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 6a6399f4..098c684f 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -1,9 +1,10 @@ use std::rc::Rc; use gpui::{ - canvas, div, prelude::FluentBuilder, px, AnyElement, AnyView, Axis, Bounds, DragMoveEvent, - EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, - StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext, + canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element, + EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement, + Pixels, Render, Size, StatefulInteractiveElement, Style, Styled, View, ViewContext, + VisualContext as _, WindowContext, }; use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; @@ -20,18 +21,25 @@ pub struct ResizablePanelGroup { axis: Axis, handle_size: Pixels, size: Pixels, + bounds: Bounds, resizing_panel_ix: Option, + last_window_size: Size, } impl ResizablePanelGroup { - pub(super) fn new() -> Self { + pub(super) fn new(cx: &mut ViewContext) -> Self { + // Observe the window size to update the bounds of the resizable panel group. + cx.observe_window_bounds(Self::on_window_resize).detach(); + Self { axis: Axis::Horizontal, sizes: Vec::new(), panels: Vec::new(), handle_size: px(1.), size: px(20.), + bounds: Bounds::default(), resizing_panel_ix: None, + last_window_size: cx.bounds().size, } } @@ -121,10 +129,26 @@ impl ResizablePanelGroup { cx.notify() } + fn on_window_resize(&mut self, cx: &mut ViewContext) { + let changed_size = cx.bounds().size - self.last_window_size; + self.last_window_size = cx.bounds().size; + let changed = changed_size.along(self.axis); + + // Avg the change in size across all panels. + // The minimum size limited in ResizablePanel. + let avg_change = changed / self.panels.len() as f32; + for (ix, panel) in self.panels.iter().enumerate() { + self.sizes[ix] += avg_change; + panel.update(cx, |this, _| this.size = self.sizes[ix]); + } + + cx.notify(); + } + fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { let axis = self.axis; - let neg_offset = -HANDLE_PADDING + px(1.); + let view = cx.view().clone(); div() .id(("resizable-handle", ix)) @@ -157,98 +181,78 @@ impl ResizablePanelGroup { this.w_full().h(self.handle_size) }), ) - .on_drag_move(cx.listener( - move |view, e: &DragMoveEvent, cx| match e.drag(cx) { - DragPanel((entity_id, ix, axis)) => { - if cx.entity_id() != *entity_id { - return; - } - - let ix = *ix; + .on_drag( + DragPanel((cx.entity_id(), ix, axis)), + move |drag_panel, cx| { + cx.stop_propagation(); + // Set current resizing panel ix + view.update(cx, |view, _| { view.resizing_panel_ix = Some(ix); - let panel = view - .panels - .get(ix) - .expect("BUG: invalid panel index") - .read(cx); - - view.sync_real_panel_sizes(cx); - match axis { - Axis::Horizontal => { - view.resize_panels(ix, e.event.position.x - panel.bounds.left(), cx) - } - Axis::Vertical => { - view.resize_panels(ix, e.event.position.y - panel.bounds.top(), cx); - } - } - } + }); + cx.new_view(|_| drag_panel.clone()) }, - )) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|view, _, _| { - if view.resizing_panel_ix.is_none() { - return; - } - - view.resizing_panel_ix = None; - }), ) - .on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag_panel.clone()) - }) } fn sync_real_panel_sizes(&mut self, cx: &WindowContext) { - for (i, panel) in self.panels.iter_mut().enumerate() { - if self.axis.is_horizontal() { - self.sizes[i] = panel.read(cx).bounds.size.width; - } else { - self.sizes[i] = panel.read(cx).bounds.size.height; - } + for (i, panel) in self.panels.iter().enumerate() { + self.sizes[i] = panel.read(cx).bounds.size.along(self.axis) } } /// The `ix`` is the index of the panel to resize, /// and the `size` is the new size for the panel. fn resize_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext) { + let mut ix = ix; // Only resize the left panels. - if ix == self.panels.len() - 1 { + if ix >= self.panels.len() - 1 { return; } let size = size.floor(); + let container_size = self.bounds.size.along(self.axis); - // 1. The `size` is the new size for the `ix` offset panel will be. - // 2. Limit `size` with the panel min and max size. - // 3. Get the `ix` panel changed size. - // 4. If the changed size is less than 1px, do nothing. - // 5. Update the next panel size with it old size minus the changed size. - // 6. When the old_size is small than the min_size, get the overflow size and then reduce other panels size with the overflow size. + self.sync_real_panel_sizes(cx); - let old_size = self.sizes[ix]; - let new_size = self.panels[ix].read(cx).limit_size(size); - if new_size < size { - return; - } - let changed_size = (new_size - old_size).floor(); + let mut changed = size - self.sizes[ix]; + let is_expand = changed > px(0.); - // If change size is less than 1px, do nothing. - if changed_size > px(-1.0) && changed_size < px(1.0) { - return; + let main_ix = ix; + let mut new_sizes = self.sizes.clone(); + + if is_expand { + new_sizes[ix] = 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 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; + new_sizes[ix + 1] += self.sizes[ix] - new_size; + + 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 next_size = self.sizes[ix + 1] - changed_size; - let next_new_size = self.panels[ix + 1].read(cx).limit_size(next_size); - let overflow_size = next_new_size - next_size; - if overflow_size != px(0.) { - return; + // If total size exceeds container size, adjust the main panel + 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); } - self.sizes[ix] = new_size; - self.panels[ix].update(cx, |this, _| this.size = new_size); - self.sizes[ix + 1] = next_new_size; - + self.sizes = new_sizes; for (i, panel) in self.panels.iter_mut().enumerate() { let size = self.sizes[i]; panel.update(cx, |this, _| this.size = size); @@ -258,6 +262,7 @@ impl ResizablePanelGroup { impl Render for ResizablePanelGroup { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let view = cx.view().clone(); let container = if self.axis.is_horizontal() { h_flex() } else { @@ -285,13 +290,25 @@ impl Render for ResizablePanelGroup { panel.clone() })) + .child({ + canvas( + move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), + |_, _, _| {}, + ) + .absolute() + .size_full() + }) + .child(ResizePanelGroupElement { + view: cx.view().clone(), + axis: self.axis, + }) } } +const PANEL_MIN_SIZE: Pixels = px(100.); + pub struct ResizablePanel { size: Pixels, - max_size: Option, - min_size: Option, axis: Axis, content_builder: Option AnyElement>>, content_view: Option, @@ -305,8 +322,6 @@ impl ResizablePanel { Self { size: px(20.), axis: Axis::Horizontal, - max_size: None, - min_size: None, content_builder: None, content_view: None, bounds: Bounds::default(), @@ -331,32 +346,6 @@ impl ResizablePanel { self.size = size; self } - - pub fn max_size(mut self, max_size: Pixels) -> Self { - self.max_size = Some(max_size); - self - } - - pub fn min_size(mut self, min_size: Pixels) -> Self { - self.min_size = Some(min_size); - self - } - - fn limit_size(&self, size: Pixels) -> Pixels { - if let Some(max_size) = self.max_size { - if size > max_size { - return max_size; - } - } - - if let Some(min_size) = self.min_size { - if size < min_size { - return min_size; - } - } - - size - } } impl FluentBuilder for ResizablePanel {} @@ -364,7 +353,7 @@ impl FluentBuilder for ResizablePanel {} impl Render for ResizablePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); - let size = self.limit_size(self.size); + let size = self.size.max(PANEL_MIN_SIZE); div() .relative() @@ -386,3 +375,90 @@ impl Render for ResizablePanel { .when_some(self.resize_handle.take(), |this, c| this.child(c)) } } + +struct ResizePanelGroupElement { + axis: Axis, + view: View, +} + +impl IntoElement for ResizePanelGroupElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ResizePanelGroupElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn request_layout( + &mut self, + _: Option<&gpui::GlobalElementId>, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + (cx.request_layout(Style::default(), None), ()) + } + + fn prepaint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut WindowContext, + ) -> Self::PrepaintState { + () + } + + fn paint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + cx.on_mouse_event({ + let view = self.view.clone(); + let axis = self.axis; + let current_ix = view.read(cx).resizing_panel_ix; + move |e: &MouseMoveEvent, phase, cx| { + if phase.bubble() { + if let Some(ix) = current_ix { + view.update(cx, |view, cx| { + let panel = view + .panels + .get(ix) + .expect("BUG: invalid panel index") + .read(cx); + + match axis { + Axis::Horizontal => { + view.resize_panels(ix, e.position.x - panel.bounds.left(), cx) + } + Axis::Vertical => { + view.resize_panels(ix, e.position.y - panel.bounds.top(), cx); + } + } + }) + } + } + } + }); + + // When any mouse up, stop dragging + cx.on_mouse_event({ + let view = self.view.clone(); + move |_: &MouseUpEvent, phase, cx| { + if phase.bubble() { + view.update(cx, |view, _| view.resizing_panel_ix = None); + } + } + }) + } +}