diff --git a/crates/story/src/main.rs b/crates/story/src/main.rs index 2611e19c..280732a4 100644 --- a/crates/story/src/main.rs +++ b/crates/story/src/main.rs @@ -2,6 +2,7 @@ use gpui::{prelude::*, *}; use gpui_component::{ h_flex, input::{InputEvent, TextInput}, + resizable::{h_resizable, resizable_panel, ResizableState}, sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem}, v_flex, ActiveTheme as _, Icon, IconName, }; @@ -13,6 +14,7 @@ pub struct Gallery { active_index: Option, collapsed: bool, search_input: Entity, + sidebar_state: Entity, _subscriptions: Vec, } @@ -86,6 +88,7 @@ impl Gallery { active_group_index: Some(0), active_index: Some(0), collapsed: false, + sidebar_state: ResizableState::new(cx), _subscriptions, } } @@ -129,89 +132,103 @@ impl Render for Gallery { ("".into(), "".into()) }; - h_flex() - .id("gallery-container") - .size_full() + h_resizable("gallery-container", self.sidebar_state.clone()) .child( - Sidebar::left() - .collapsed(self.collapsed) - .header( - v_flex() - .w_full() - .gap_4() - .child( - SidebarHeader::new() + resizable_panel() + .size(px(255.)) + .size_range(px(200.)..px(320.)) + .child( + Sidebar::left() + .width(relative(1.)) + .border_width(px(0.)) + .collapsed(self.collapsed) + .header( + v_flex() .w_full() + .gap_4() + .child( + SidebarHeader::new() + .w_full() + .child( + div() + .flex() + .items_center() + .justify_center() + .rounded(cx.theme().radius) + .bg(cx.theme().primary) + .text_color(cx.theme().primary_foreground) + .size_8() + .flex_shrink_0() + .when(!self.collapsed, |this| { + this.child(Icon::new( + IconName::GalleryVerticalEnd, + )) + }) + .when(self.collapsed, |this| { + this.size_4() + .bg(cx.theme().transparent) + .text_color(cx.theme().foreground) + .child(Icon::new( + IconName::GalleryVerticalEnd, + )) + }) + .rounded_lg(), + ) + .when(!self.collapsed, |this| { + this.child( + v_flex() + .gap_0() + .text_sm() + .flex_1() + .line_height(relative(1.25)) + .overflow_hidden() + .text_ellipsis() + .child("GPUI Component") + .child( + div() + .text_color( + cx.theme().muted_foreground, + ) + .child("Gallery") + .text_xs(), + ), + ) + }), + ) .child( div() - .flex() - .items_center() - .justify_center() - .rounded(cx.theme().radius) - .bg(cx.theme().primary) - .text_color(cx.theme().primary_foreground) - .size_8() - .flex_shrink_0() - .when(!self.collapsed, |this| { - this.child(Icon::new(IconName::GalleryVerticalEnd)) - }) - .when(self.collapsed, |this| { - this.size_4() - .bg(cx.theme().transparent) - .text_color(cx.theme().foreground) - .child(Icon::new(IconName::GalleryVerticalEnd)) - }) - .rounded_lg(), - ) - .when(!self.collapsed, |this| { - this.child( - v_flex() - .gap_0() - .text_sm() - .flex_1() - .line_height(relative(1.25)) - .overflow_hidden() - .text_ellipsis() - .child("GPUI Component") - .child( - div() - .text_color(cx.theme().muted_foreground) - .child("Gallery") - .text_xs(), - ), - ) - }), + .bg(cx.theme().sidebar_border) + .px_1() + .rounded_full() + .flex_1() + .mx_1() + .child(self.search_input.clone()), + ), ) - .child( - div() - .bg(cx.theme().sidebar_border) - .px_1() - .rounded_full() - .flex_1() - .mx_1() - .child(self.search_input.clone()), - ), - ) - .children(stories.clone().into_iter().enumerate().map( - |(group_ix, (group_name, sub_stories))| { - SidebarGroup::new(*group_name).child(SidebarMenu::new().children( - sub_stories.iter().enumerate().map(|(ix, story)| { - SidebarMenuItem::new(story.read(cx).name.clone()) - .active( - self.active_group_index == Some(group_ix) - && self.active_index == Some(ix), - ) - .on_click(cx.listener( - move |this, _: &ClickEvent, _, cx| { - this.active_group_index = Some(group_ix); - this.active_index = Some(ix); - cx.notify(); - }, - )) - }), - )) - }, - )), + .children(stories.clone().into_iter().enumerate().map( + |(group_ix, (group_name, sub_stories))| { + SidebarGroup::new(*group_name).child( + SidebarMenu::new().children( + sub_stories.iter().enumerate().map(|(ix, story)| { + SidebarMenuItem::new(story.read(cx).name.clone()) + .active( + self.active_group_index == Some(group_ix) + && self.active_index == Some(ix), + ) + .on_click(cx.listener( + move |this, _: &ClickEvent, _, cx| { + this.active_group_index = + Some(group_ix); + this.active_index = Some(ix); + cx.notify(); + }, + )) + }), + ), + ) + }, + )), + ), ) .child( v_flex() @@ -245,7 +262,8 @@ impl Render for Gallery { .when_some(active_story, |this, active_story| { this.child(active_story.clone()) }), - ), + ) + .into_any_element(), ) } } diff --git a/crates/story/src/resizable_story.rs b/crates/story/src/resizable_story.rs index 7a128741..784d0a50 100644 --- a/crates/story/src/resizable_story.rs +++ b/crates/story/src/resizable_story.rs @@ -3,14 +3,15 @@ use gpui::{ ParentElement as _, Pixels, Render, SharedString, Styled, Window, }; use gpui_component::{ - resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanelGroup}, + resizable::{h_resizable, resizable_panel, v_resizable, ResizableState}, v_flex, ActiveTheme, }; pub struct ResizableStory { focus_handle: gpui::FocusHandle, - group1: Entity, - group2: Entity, + state1: Entity, + state2: Entity, + state3: Entity, } impl super::Story for ResizableStory { @@ -39,81 +40,71 @@ impl ResizableStory { } fn new(_: &mut Window, cx: &mut App) -> Self { - fn panel_box(content: impl Into, cx: &App) -> AnyElement { - div() - .p_4() - .border_1() - .border_color(cx.theme().border) - .size_full() - .child(content.into()) - .into_any_element() - } + let state1 = ResizableState::new(cx); + let state2 = ResizableState::new(cx); + let state3 = ResizableState::new(cx); - let group1 = cx.new(|cx| { - v_resizable() - .group( - h_resizable() - .size(px(150.)) - .child( - resizable_panel() - .size(px(150.)) - .size_range(px(120.)..px(300.)) - .content(|_, cx| panel_box("Left (120px .. 300px)", cx)), - cx, - ) - .child( - resizable_panel().content(|_, cx| panel_box("Center", cx)), - cx, - ) - .child( - resizable_panel() - .size(px(300.)) - .content(|_, cx| panel_box("Right", cx)), - cx, - ), - cx, - ) - .child( - resizable_panel().content(|_, cx| panel_box("Center", cx)), - cx, - ) - .child( - resizable_panel() - .size(px(80.)) - .size_range(px(80.)..Pixels::MAX) - .content(|_, cx| panel_box("Bottom (80px .. 150px)", cx)), - cx, - ) - }); - - let group2 = cx.new(|cx| { - h_resizable() - .child( - resizable_panel() - .size(px(200.)) - .size_range(px(200.)..px(400.)) - .content(|_, cx| panel_box("Left 2", cx)), - cx, - ) - .child( - resizable_panel().content(|_, cx| panel_box("Right (Grow)", cx)), - cx, - ) - }); Self { focus_handle: cx.focus_handle(), - group1, - group2, + state1, + state2, + state3, } } } +fn panel_box(content: impl Into, cx: &App) -> AnyElement { + div() + .p_4() + .border_1() + .border_color(cx.theme().border) + .size_full() + .child(content.into()) + .into_any_element() +} + impl Render for ResizableStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .gap_6() - .child(div().h(px(800.)).child(self.group1.clone())) - .child(self.group2.clone()) + .child( + div().h(px(800.)).child( + v_resizable("resizable-1", self.state1.clone()) + .group( + h_resizable("resizable-1.1", self.state2.clone()) + .size(px(150.)) + .child( + resizable_panel() + .size(px(150.)) + .size_range(px(120.)..px(300.)) + .child(panel_box("Left (120px .. 300px)", cx)), + ) + .child(resizable_panel().child(panel_box("Center", cx))) + .child( + resizable_panel() + .size(px(300.)) + .child(panel_box("Right", cx)), + ), + ) + .child(resizable_panel().child(panel_box("Center", cx))) + .child( + resizable_panel() + .size(px(80.)) + .size_range(px(80.)..Pixels::MAX) + .child(panel_box("Bottom (80px .. 150px)", cx)), + ), + ), + ) + .child( + h_resizable("resizable-3", self.state3.clone()) + .child( + resizable_panel() + .size(px(200.)) + .size_range(px(200.)..px(400.)) + .child(panel_box("Left 2", cx)), + ) + .child(resizable_panel().child(panel_box("Right (Grow)", cx))), + ) } } diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index 363b3bf9..5bec8395 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -4,17 +4,16 @@ use crate::{ dock::PanelInfo, h_flex, resizable::{ - h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, - ResizablePanelGroup, PANEL_MIN_SIZE, + resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, + ResizableState, PANEL_MIN_SIZE, }, ActiveTheme, AxisExt as _, Placement, }; use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel}; use gpui::{ - prelude::FluentBuilder as _, App, AppContext, Axis, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, - Subscription, WeakEntity, Window, + App, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, + ParentElement, Pixels, Render, Styled, Subscription, WeakEntity, Window, }; use smallvec::SmallVec; @@ -23,7 +22,7 @@ pub struct StackPanel { pub(super) axis: Axis, focus_handle: FocusHandle, pub(crate) panels: SmallVec<[Arc; 2]>, - panel_group: Entity, + state: Entity, _subscriptions: Vec, } @@ -41,7 +40,7 @@ impl Panel for StackPanel { } } fn dump(&self, cx: &App) -> PanelState { - let sizes = self.panel_group.read(cx).sizes(); + let sizes = self.state.read(cx).sizes().clone(); let mut state = PanelState::new(self); for panel in &self.panels { state.add_child(panel.dump(cx)); @@ -54,26 +53,19 @@ impl Panel for StackPanel { impl StackPanel { pub fn new(axis: Axis, _: &mut Window, cx: &mut Context) -> Self { - let panel_group = cx.new(|_| { - if axis == Axis::Horizontal { - h_resizable() - } else { - v_resizable() - } - }); + let state = ResizableState::new(cx); // Bubble up the resize event. - let _subscriptions = vec![cx - .subscribe(&panel_group, |_, _, _: &ResizablePanelEvent, cx| { - cx.emit(PanelEvent::LayoutChanged) - })]; + let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| { + cx.emit(PanelEvent::LayoutChanged) + })]; Self { axis, parent: None, focus_handle: cx.focus_handle(), panels: SmallVec::new(), - panel_group, + state, _subscriptions, } } @@ -186,13 +178,6 @@ impl StackPanel { self.insert_panel(panel, ix + 1, size, dock_area, window, cx); } - fn new_resizable_panel(panel: Arc, size: Option) -> ResizablePanel { - resizable_panel() - .content_view(panel.view()) - .content_visible(move |_, cx| panel.visible(cx)) - .when_some(size, |this, size| this.size(size)) - } - fn insert_panel( &mut self, panel: Arc, @@ -242,22 +227,15 @@ impl StackPanel { 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) + let state = self.state.read(cx); + (state.total_size() / (state.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(), Some(size)), - ix, - window, - cx, - ) + self.state.update(cx, |state, cx| { + state.insert_panel(Some(size), Some(ix), cx); }); - cx.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -271,8 +249,8 @@ impl StackPanel { ) { if let Some(ix) = self.index_of_panel(panel.clone()) { self.panels.remove(ix); - self.panel_group.update(cx, |view, cx| { - view.remove_child(ix, window, cx); + self.state.update(cx, |state, cx| { + state.remove_panel(ix, cx); }); cx.emit(PanelEvent::LayoutChanged); @@ -287,18 +265,15 @@ impl StackPanel { &mut self, old_panel: Arc, new_panel: Entity, - window: &mut Window, + _: &mut Window, cx: &mut Context, ) { if let Some(ix) = self.index_of_panel(old_panel.clone()) { self.panels[ix] = Arc::new(new_panel.clone()); - self.panel_group.update(cx, |view, cx| { - view.replace_child( - Self::new_resizable_panel(Arc::new(new_panel.clone()), None), - ix, - window, - cx, - ); + + let panel_state = ResizablePanelState::default(); + self.state.update(cx, |state, cx| { + state.replace_panel(ix, panel_state, cx); }); cx.emit(PanelEvent::LayoutChanged); } @@ -387,17 +362,17 @@ impl StackPanel { } /// Remove all panels from the stack. - pub(super) fn remove_all_panels(&mut self, window: &mut Window, cx: &mut Context) { + pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { self.panels.clear(); - self.panel_group - .update(cx, |view, cx| view.remove_all_children(window, cx)); + self.state.update(cx, |state, cx| { + state.clear(); + cx.notify(); + }); } /// Change the axis of the stack panel. - pub(super) fn set_axis(&mut self, axis: Axis, window: &mut Window, cx: &mut Context) { + pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { self.axis = axis; - self.panel_group - .update(cx, |view, cx| view.set_axis(axis, window, cx)); cx.notify(); } } @@ -415,6 +390,14 @@ impl Render for StackPanel { .size_full() .overflow_hidden() .bg(cx.theme().tab_bar) - .child(self.panel_group.clone()) + .child( + ResizablePanelGroup::new("stack-panel-group", self.state.clone()) + .axis(self.axis) + .children(self.panels.clone().into_iter().map(|panel| { + resizable_panel() + .child(panel.view()) + .visible(panel.visible(cx)) + })), + ) } } diff --git a/crates/ui/src/resizable/mod.rs b/crates/ui/src/resizable/mod.rs index d05bd784..354644f6 100644 --- a/crates/ui/src/resizable/mod.rs +++ b/crates/ui/src/resizable/mod.rs @@ -1,18 +1,232 @@ -use gpui::Axis; +use std::ops::Range; + +use gpui::{ + px, Along, App, AppContext, Axis, Bounds, Context, ElementId, Entity, EventEmitter, Pixels, + Window, +}; mod panel; mod resize_handle; pub use panel::*; pub(crate) use resize_handle::*; -pub fn h_resizable() -> ResizablePanelGroup { - ResizablePanelGroup::new().axis(Axis::Horizontal) +pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); + +/// Create a [`ResizablePanelGroup`] with horizontal resizing +pub fn h_resizable(id: impl Into, state: Entity) -> ResizablePanelGroup { + ResizablePanelGroup::new(id, state).axis(Axis::Horizontal) } -pub fn v_resizable() -> ResizablePanelGroup { - ResizablePanelGroup::new().axis(Axis::Vertical) +/// Create a [`ResizablePanelGroup`] with vertical resizing +pub fn v_resizable(id: impl Into, state: Entity) -> ResizablePanelGroup { + ResizablePanelGroup::new(id, state).axis(Axis::Vertical) } +/// Create a [`ResizablePanel`]. pub fn resizable_panel() -> ResizablePanel { ResizablePanel::new() } + +#[derive(Debug, Clone)] +/// State for a [`ResizablePanel`] +pub struct ResizableState { + /// The `axis` will sync to actual axis of the ResizablePanelGroup in use. + axis: Axis, + panels: Vec, + sizes: Vec, + pub(crate) resizing_panel_ix: Option, + bounds: Bounds, +} + +impl ResizableState { + pub fn new(cx: &mut App) -> Entity { + cx.new(|_| Self { + axis: Axis::Horizontal, + panels: vec![], + sizes: vec![], + resizing_panel_ix: None, + bounds: Bounds::default(), + }) + } + + pub fn insert_panel( + &mut self, + size: Option, + ix: Option, + cx: &mut Context, + ) { + let panel_state = ResizablePanelState { + size, + ..Default::default() + }; + + if let Some(ix) = ix { + self.panels.insert(ix, panel_state); + self.sizes.insert(ix, size.unwrap_or(PANEL_MIN_SIZE)); + } else { + self.panels.push(panel_state); + self.sizes.push(size.unwrap_or(PANEL_MIN_SIZE)); + }; + cx.notify(); + } + + pub(crate) fn sync_panels_count(&mut self, axis: Axis, panels_count: usize) { + self.axis = axis; + if panels_count > self.panels.len() { + let diff = panels_count - self.panels.len(); + self.panels + .extend(vec![ResizablePanelState::default(); diff]); + self.sizes.extend(vec![PANEL_MIN_SIZE; diff]); + } + } + + pub(crate) fn update_panel_size( + &mut self, + panel_ix: usize, + bounds: Bounds, + size_range: Range, + cx: &mut Context, + ) { + let size = bounds.size.along(self.axis); + 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(); + } + + pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context) { + self.panels.remove(panel_ix); + self.sizes.remove(panel_ix); + if let Some(resizing_panel_ix) = self.resizing_panel_ix { + if resizing_panel_ix > panel_ix { + self.resizing_panel_ix = Some(resizing_panel_ix - 1); + } + } + cx.notify(); + } + + pub(crate) fn replace_panel( + &mut self, + panel_ix: usize, + panel: ResizablePanelState, + cx: &mut Context, + ) { + let old_size = self.sizes[panel_ix]; + + self.panels[panel_ix] = panel; + self.sizes[panel_ix] = old_size; + cx.notify(); + } + + pub(crate) fn clear(&mut self) { + self.panels.clear(); + self.sizes.clear(); + } + + /// Get the size of the panels. + pub fn sizes(&self) -> &Vec { + &self.sizes + } + + pub(crate) fn total_size(&self) -> Pixels { + self.sizes.iter().map(|s| s.0).sum::().into() + } + + pub(crate) fn done_resizing(&mut self, cx: &mut Context) { + self.resizing_panel_ix = None; + cx.emit(ResizablePanelEvent::Resized); + } + + fn panel_size_range(&self, ix: usize) -> Range { + let Some(panel) = self.panels.get(ix) else { + return PANEL_MIN_SIZE..Pixels::MAX; + }; + + panel.size_range.clone() + } + + 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(); + } + } + + /// The `ix`` is the index of the panel to resize, + /// and the `size` is the new size for the panel. + fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context) { + let old_sizes = self.sizes.clone(); + + let mut ix = ix; + // Only resize the left panels. + 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); + + let move_changed = size - old_sizes[ix]; + if move_changed == px(0.) { + return; + } + + let size_range = self.panel_size_range(ix); + 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 = old_sizes.clone(); + + if is_expand { + let mut changed = new_size - old_sizes[ix]; + new_sizes[ix] = new_size; + + while changed > px(0.) && ix < old_sizes.len() - 1 { + ix += 1; + let size_range = self.panel_size_range(ix); + 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 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 { + ix -= 1; + let size_range = self.panel_size_range(ix); + 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; + } + } + + // 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(size_range.start); + } + + for (i, _) in old_sizes.iter().enumerate() { + let size = new_sizes[i]; + self.panels[i].size = Some(size); + } + + self.sizes = new_sizes; + cx.notify(); + } +} + +impl EventEmitter for ResizableState {} + +#[derive(Debug, Clone, Default)] +pub(crate) struct ResizablePanelState { + pub size: Option, + pub size_range: Range, + bounds: Bounds, +} diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 36043897..6de3a701 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -1,26 +1,21 @@ -use std::{ - ops::{Deref, Range}, - rc::Rc, -}; +use std::ops::{Deref, Range}; use gpui::{ - 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, + 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, }; -use crate::{h_flex, v_flex, AxisExt}; +use crate::{h_flex, resizable::PANEL_MIN_SIZE, v_flex, AxisExt}; -use super::{resizable_panel, resize_handle}; - -pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); +use super::{resizable_panel, resize_handle, ResizableState}; pub enum ResizablePanelEvent { Resized, } #[derive(Clone)] -pub struct DragPanel(pub (EntityId, usize, Axis)); +pub struct DragPanel(pub (usize, Axis)); impl Render for DragPanel { fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { @@ -28,58 +23,53 @@ impl Render for DragPanel { } } -#[derive(Clone)] +#[derive(IntoElement)] pub struct ResizablePanelGroup { - panels: Vec>, - sizes: Vec, + id: ElementId, + state: Entity, axis: Axis, size: Option, - - bounds: Bounds, - resizing_panel_ix: Option, + children: Vec, } impl ResizablePanelGroup { - pub(super) fn new() -> Self { + pub(crate) fn new(id: impl Into, state: Entity) -> Self { Self { + id: id.into(), axis: Axis::Horizontal, - sizes: Vec::new(), - panels: Vec::new(), + children: vec![], + state, size: None, - bounds: Bounds::default(), - resizing_panel_ix: None, } } - pub fn load(&mut self, sizes: Vec, panels: Vec>) { - self.sizes = sizes; - self.panels = panels; - } - /// Set the axis of the resizable panel group, default is horizontal. pub fn axis(mut self, axis: Axis) -> Self { self.axis = axis; self } - pub(crate) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { - self.axis = axis; - cx.notify(); - } - /// 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._insert_child(panel, self.panels.len(), cx); + pub fn child(mut self, panel: impl Into) -> Self { + self.children.push(panel.into()); + self + } + + pub fn children(mut self, panels: impl IntoIterator) -> Self + where + I: Into, + { + self.children = panels.into_iter().map(|panel| panel.into()).collect(); self } /// Add a ResizablePanelGroup as a child to the group. - pub fn group(self, group: ResizablePanelGroup, cx: &mut Context) -> Self { - self.child(resizable_panel().content_view(cx.new(|_| group).into()), cx) + pub fn group(self, group: ResizablePanelGroup) -> Self { + self.child(resizable_panel().child(group.into_any_element())) } /// Set size of the resizable panel group @@ -90,293 +80,95 @@ impl ResizablePanelGroup { self.size = Some(size); self } - - /// Returns the sizes of the resizable panels. - pub(crate) fn sizes(&self) -> Vec { - self.sizes.clone() - } - - /// Calculates the sum of all panel sizes within the group. - pub fn total_size(&self) -> Pixels { - self.sizes.iter().fold(px(0.0), |acc, &size| acc + size) - } - - /// 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, - 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), - }; - - // 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)); - } - - /// Replace a child panel with a new panel at the given index. - pub(crate) fn replace_child( - &mut self, - panel: ResizablePanel, - ix: usize, - _: &mut Window, - cx: &mut Context, - ) { - let old_panel = self.panels[ix].read(cx); - - let mut panel = panel; - panel.initial_size = old_panel.initial_size; - panel.size = old_panel.size; - panel.axis = self.axis; - panel.group = Some(cx.entity().downgrade()); - - self.panels[ix] = cx.new(|_| panel); - cx.notify() - } - - pub fn remove_child(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { - self.sizes.remove(ix); - self.panels.remove(ix); - cx.notify() - } - - pub(crate) fn remove_all_children(&mut self, _: &mut Window, cx: &mut Context) { - self.sizes.clear(); - self.panels.clear(); - cx.notify() - } - - fn render_resize_handle( - &self, - ix: usize, - _: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let view = cx.entity().clone(); - resize_handle(("resizable-handle", ix), self.axis).on_drag( - DragPanel((cx.entity_id(), ix, self.axis)), - move |drag_panel, _, _, cx| { - cx.stop_propagation(); - // Set current resizing panel ix - view.update(cx, |view, _| { - view.resizing_panel_ix = Some(ix); - }); - cx.new(|_| drag_panel.deref().clone()) - }, - ) - } - - fn done_resizing(&mut self, _: &mut Window, cx: &mut Context) { - cx.emit(ResizablePanelEvent::Resized); - self.resizing_panel_ix = None; - } - - 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).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( - &mut self, - ix: usize, - size: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - let mut ix = ix; - // Only resize the left panels. - if ix >= self.panels.len() - 1 { - return; - } - let size = size.floor(); - let container_size = self.bounds.size.along(self.axis); - self.sync_real_panel_sizes(window, cx); - - 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 { - let mut changed = new_size - self.sizes[ix]; - new_sizes[ix] = new_size; - - while changed > px(0.) && ix < self.panels.len() - 1 { - ix += 1; - 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 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 { - ix -= 1; - 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; - } - } - - // 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(size_range.start); - } - - for (i, panel) in self.panels.iter().enumerate() { - 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); - }); - } - } - self.sizes = new_sizes; +} +impl From for ResizablePanel +where + T: Into, +{ + fn from(value: T) -> Self { + resizable_panel().child(value.into()) } } + impl EventEmitter for ResizablePanelGroup {} -impl Render for ResizablePanelGroup { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let view = cx.entity().clone(); + +impl RenderOnce for ResizablePanelGroup { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let state = self.state.clone(); let container = if self.axis.is_horizontal() { h_flex() } else { v_flex() }; - container - .size_full() - .children(self.panels.iter().enumerate().map(|(ix, panel)| { - if ix > 0 { - let handle = self.render_resize_handle(ix - 1, window, cx); - panel.update(cx, |view, _| { - view.resize_handle = Some(handle.into_any_element()) - }); - } + // Sync panels to the state + let panels_count = self.children.len(); + self.state.update(cx, |state, _| { + state.sync_panels_count(self.axis, panels_count); + }); - panel.clone() - })) + container + .id(self.id) + .size_full() + .children( + self.children + .into_iter() + .enumerate() + .map(|(ix, mut panel)| { + panel.panel_ix = ix; + panel.axis = self.axis; + panel.state = Some(self.state.clone()); + panel + }), + ) .child({ canvas( - move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), + move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds), |_, _, _, _| {}, ) .absolute() .size_full() }) .child(ResizePanelGroupElement { - view: cx.entity().clone(), + state: self.state.clone(), axis: self.axis, }) } } +#[derive(IntoElement)] pub struct ResizablePanel { - group: Option>, + axis: Axis, + panel_ix: usize, + state: Option>, /// Initial size is the size that the panel has when it is created. initial_size: Option, - /// size is the size that the panel has when it is resized or adjusted by flex layout. - size: Option, /// size range limit of this panel. size_range: Range, - axis: Axis, - content_builder: Option AnyElement>>, - content_view: Option, - content_visible: Rc bool>>, - /// The bounds of the resizable panel, when render the bounds will be updated. - bounds: Bounds, - resize_handle: Option, + children: Vec, + visible: bool, } impl ResizablePanel { pub(super) fn new() -> Self { Self { - group: None, + panel_ix: 0, initial_size: None, - size: None, + state: None, size_range: (PANEL_MIN_SIZE..Pixels::MAX), axis: Axis::Horizontal, - content_builder: None, - content_view: None, - content_visible: Rc::new(Box::new(|_, _| true)), - bounds: Bounds::default(), - resize_handle: None, + children: vec![], + visible: true, } } - pub fn content(mut self, content: F) -> Self - where - F: Fn(&mut Window, &mut App) -> AnyElement + 'static, - { - self.content_builder = Some(Rc::new(content)); + pub fn child(mut self, child: impl IntoElement) -> Self { + self.children.push(child.into_any_element()); self } - pub(crate) fn content_visible(mut self, content_visible: F) -> Self - where - F: Fn(&Window, &App) -> bool + 'static, - { - self.content_visible = Rc::new(Box::new(content_visible)); - self - } - - pub fn content_view(mut self, content: AnyView) -> Self { - self.content_view = Some(content); + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; self } @@ -393,43 +185,26 @@ impl ResizablePanel { 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) { - self.bounds = bounds; - 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, _| { - if let Some(ix) = view.panels.iter().position(|v| v.entity_id() == entity_id) { - view.sizes[ix] = new_size; - } - }); - } - // cx.notify(); - } } -impl FluentBuilder for ResizablePanel {} - -impl Render for ResizablePanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !(self.content_visible)(window, cx) { - // To keep size as initial size, to make sure the size will not be changed. - self.initial_size = self.size; - self.size = None; - return div(); +impl RenderOnce for ResizablePanel { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + if !self.visible { + return div().id(("resizable-panel", self.panel_ix)); } - let view = cx.entity().clone(); + let state = self + .state + .expect("BUG: The `state` in ResizablePanel should be present."); + let panel_state = state + .read(cx) + .panels + .get(self.panel_ix) + .expect("BUG: The `index` of ResizablePanel should be one of in `state`."); let size_range = self.size_range.clone(); div() + .id(("resizable-panel", self.panel_ix)) .flex() .flex_grow() .size_full() @@ -447,36 +222,52 @@ impl Render for ResizablePanel { .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() - }) + this.when( + panel_state.size.is_none() && !initial_size.is_zero(), + |this| this.flex_none(), + ) .flex_basis(initial_size) }) - .map(|this| match self.size { + .map(|this| match panel_state.size { Some(size) => this.flex_basis(size), None => this, }) .child({ canvas( - move |bounds, window, cx| { - view.update(cx, |r, cx| r.update_size(bounds, window, cx)) + { + let state = state.clone(); + move |bounds, _, cx| { + state.update(cx, |state, cx| { + state.update_panel_size(self.panel_ix, bounds, self.size_range, cx) + }) + } }, |_, _, _, _| {}, ) .absolute() .size_full() }) - .when_some(self.content_builder.clone(), |this, c| { - this.child(c(window, cx)) + .children(self.children) + .when(self.panel_ix > 0, |this| { + let ix = self.panel_ix - 1; + this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag( + DragPanel((ix, self.axis)), + move |drag_panel, _, _, cx| { + cx.stop_propagation(); + // Set current resizing panel ix + state.update(cx, |state, _| { + state.resizing_panel_ix = Some(ix); + }); + cx.new(|_| drag_panel.deref().clone()) + }, + )) }) - .when_some(self.content_view.clone(), |this, c| this.child(c)) - .when_some(self.resize_handle.take(), |this, c| this.child(c)) } } struct ResizePanelGroupElement { + state: Entity, axis: Axis, - view: Entity, } impl IntoElement for ResizePanelGroupElement { @@ -525,44 +316,41 @@ impl Element for ResizePanelGroupElement { cx: &mut App, ) { window.on_mouse_event({ - let view = self.view.clone(); + let state = self.state.clone(); let axis = self.axis; - let current_ix = view.read(cx).resizing_panel_ix; + let current_ix = state.read(cx).resizing_panel_ix; move |e: &MouseMoveEvent, phase, window, cx| { if !phase.bubble() { return; } let Some(ix) = current_ix else { return }; - view.update(cx, |view, cx| { - let panel = view - .panels - .get(ix) - .expect("BUG: invalid panel index") - .read(cx); + state.update(cx, |state, cx| { + let panel = state.panels.get(ix).expect("BUG: invalid panel index"); match axis { Axis::Horizontal => { - view.resize_panels(ix, e.position.x - panel.bounds.left(), window, cx) + state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx) } Axis::Vertical => { - view.resize_panels(ix, e.position.y - panel.bounds.top(), window, cx); + state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx); } } + cx.notify(); }) } }); // When any mouse up, stop dragging window.on_mouse_event({ - let view = self.view.clone(); - let current_ix = view.read(cx).resizing_panel_ix; - move |_: &MouseUpEvent, phase, window, cx| { + let state = self.state.clone(); + let current_ix = state.read(cx).resizing_panel_ix; + move |_: &MouseUpEvent, phase, _, cx| { if current_ix.is_none() { return; } if phase.bubble() { - view.update(cx, |view, cx| view.done_resizing(window, cx)); + state.update(cx, |state, cx| state.done_resizing(cx)); } } }) diff --git a/crates/ui/src/sidebar/mod.rs b/crates/ui/src/sidebar/mod.rs index 3e6f1a1a..5c77db69 100644 --- a/crates/ui/src/sidebar/mod.rs +++ b/crates/ui/src/sidebar/mod.rs @@ -5,8 +5,8 @@ use crate::{ v_flex, ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt, }; use gpui::{ - div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, InteractiveElement as _, - IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, + div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, DefiniteLength, + InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, }; use std::rc::Rc; @@ -33,7 +33,8 @@ pub struct Sidebar { /// The side of the sidebar side: Side, collapsible: bool, - width: Pixels, + width: DefiniteLength, + border_width: Pixels, collapsed: bool, } @@ -45,7 +46,8 @@ impl Sidebar { footer: None, side, collapsible: true, - width: DEFAULT_WIDTH, + width: DEFAULT_WIDTH.into(), + border_width: px(1.), collapsed: false, } } @@ -59,8 +61,14 @@ impl Sidebar { } /// Set the width of the sidebar - pub fn width(mut self, width: Pixels) -> Self { - self.width = width; + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Set border width of the sidebar + pub fn border_width(mut self, border_width: impl Into) -> Self { + self.border_width = border_width.into(); self } @@ -191,8 +199,8 @@ impl RenderOnce for Sidebar { .text_color(cx.theme().sidebar_foreground) .border_color(cx.theme().sidebar_border) .map(|this| match self.side { - Side::Left => this.border_r_1(), - Side::Right => this.border_l_1(), + Side::Left => this.border_r(self.border_width), + Side::Right => this.border_l(self.border_width), }) .when_some(self.header.take(), |this, header| { this.child(h_flex().id("header").p_2().gap_2().child(header))