resizable: Refactor to use stateless Resizable element. (#852)

## Break Changes

- The `ResizablePanelGroup` and `ResizablePanel` are changed to as
stateless element, please check out the diff to migrate.
This commit is contained in:
Jason Lee 2025-05-12 17:15:11 +08:00 committed by GitHub
parent 0a95f7c0f1
commit 1535df796e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 553 additions and 551 deletions

View file

@ -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<usize>,
collapsed: bool,
search_input: Entity<TextInput>,
sidebar_state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>,
}
@ -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(),
)
}
}

View file

@ -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<ResizablePanelGroup>,
group2: Entity<ResizablePanelGroup>,
state1: Entity<ResizableState>,
state2: Entity<ResizableState>,
state3: Entity<ResizableState>,
}
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<SharedString>, 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<SharedString>, 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<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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))),
)
}
}

View file

@ -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<dyn PanelView>; 2]>,
panel_group: Entity<ResizablePanelGroup>,
state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>,
}
@ -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>) -> 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<dyn PanelView>, size: Option<Pixels>) -> 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<dyn PanelView>,
@ -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<dyn PanelView>,
new_panel: Entity<StackPanel>,
window: &mut Window,
_: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>) {
pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
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))
})),
)
}
}

View file

@ -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<ElementId>, state: Entity<ResizableState>) -> 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<ElementId>, state: Entity<ResizableState>) -> 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<ResizablePanelState>,
sizes: Vec<Pixels>,
pub(crate) resizing_panel_ix: Option<usize>,
bounds: Bounds<Pixels>,
}
impl ResizableState {
pub fn new(cx: &mut App) -> Entity<Self> {
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<Pixels>,
ix: Option<usize>,
cx: &mut Context<Self>,
) {
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<Pixels>,
size_range: Range<Pixels>,
cx: &mut Context<Self>,
) {
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>) {
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<Self>,
) {
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<Pixels> {
&self.sizes
}
pub(crate) fn total_size(&self) -> Pixels {
self.sizes.iter().map(|s| s.0).sum::<f32>().into()
}
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
self.resizing_panel_ix = None;
cx.emit(ResizablePanelEvent::Resized);
}
fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
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<Self>) {
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::<f32>().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<ResizablePanelEvent> for ResizableState {}
#[derive(Debug, Clone, Default)]
pub(crate) struct ResizablePanelState {
pub size: Option<Pixels>,
pub size_range: Range<Pixels>,
bounds: Bounds<Pixels>,
}

View file

@ -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<Entity<ResizablePanel>>,
sizes: Vec<Pixels>,
id: ElementId,
state: Entity<ResizableState>,
axis: Axis,
size: Option<Pixels>,
bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>,
children: Vec<ResizablePanel>,
}
impl ResizablePanelGroup {
pub(super) fn new() -> Self {
pub(crate) fn new(id: impl Into<ElementId>, state: Entity<ResizableState>) -> 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<Pixels>, panels: Vec<Entity<ResizablePanel>>) {
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>) {
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 {
self._insert_child(panel, self.panels.len(), cx);
pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
self.children.push(panel.into());
self
}
pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
where
I: Into<ResizablePanel>,
{
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 {
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<Pixels> {
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>,
) {
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;
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<Self>,
) {
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>) {
self.sizes.remove(ix);
self.panels.remove(ix);
cx.notify()
}
pub(crate) fn remove_all_children(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.sizes.clear();
self.panels.clear();
cx.notify()
}
fn render_resize_handle(
&self,
ix: usize,
_: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) {
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<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,
/// 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<Self>,
) {
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::<f32>().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<T> From<T> for ResizablePanel
where
T: Into<AnyElement>,
{
fn from(value: T) -> Self {
resizable_panel().child(value.into())
}
}
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
impl Render for ResizablePanelGroup {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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<WeakEntity<ResizablePanelGroup>>,
axis: Axis,
panel_ix: usize,
state: Option<Entity<ResizableState>>,
/// Initial size is the size that the panel has when it is created.
initial_size: Option<Pixels>,
/// size is the size that the panel has when it is resized or adjusted by flex layout.
size: Option<Pixels>,
/// size range limit of this panel.
size_range: Range<Pixels>,
axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
content_view: Option<AnyView>,
content_visible: Rc<Box<dyn Fn(&Window, &App) -> bool>>,
/// The bounds of the resizable panel, when render the bounds will be updated.
bounds: Bounds<Pixels>,
resize_handle: Option<AnyElement>,
children: Vec<AnyElement>,
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<F>(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<F>(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<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
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<Self>) -> 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<ResizableState>,
axis: Axis,
view: Entity<ResizablePanelGroup>,
}
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));
}
}
})

View file

@ -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<E: Collapsible + IntoElement + 'static> {
/// The side of the sidebar
side: Side,
collapsible: bool,
width: Pixels,
width: DefiniteLength,
border_width: Pixels,
collapsed: bool,
}
@ -45,7 +46,8 @@ impl<E: Collapsible + IntoElement> Sidebar<E> {
footer: None,
side,
collapsible: true,
width: DEFAULT_WIDTH,
width: DEFAULT_WIDTH.into(),
border_width: px(1.),
collapsed: false,
}
}
@ -59,8 +61,14 @@ impl<E: Collapsible + IntoElement> Sidebar<E> {
}
/// 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<DefiniteLength>) -> Self {
self.width = width.into();
self
}
/// Set border width of the sidebar
pub fn border_width(mut self, border_width: impl Into<Pixels>) -> Self {
self.border_width = border_width.into();
self
}
@ -191,8 +199,8 @@ impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
.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))