resizable: Simplify Resizable API. (#1459)

- Added `on_resize` callback method to `ResizabelPanelGroup`.

## Break Changes

- Remove `state` argument from `h_resizable`, `v_resizable` method.

```diff
- v_resizable("resizable-1", state)
+ v_resizable("resizable-1")
```

- Removed `group` method from `ResizabelPanelGroup`, use `child`
instead.

```diff
- v_resizable(..).group(..)
+ v_resizable(..).child(..)
```
This commit is contained in:
Jason Lee 2025-10-29 23:01:05 +08:00 committed by GitHub
parent a1ca8624af
commit 2727a65494
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 163 additions and 277 deletions

View file

@ -18,7 +18,7 @@ use gpui_component::{
self, CodeActionProvider, CompletionProvider, DefinitionProvider, DocumentColorProvider, self, CodeActionProvider, CompletionProvider, DefinitionProvider, DocumentColorProvider,
HoverProvider, Input, InputEvent, InputState, Position, Rope, RopeExt, TabSize, HoverProvider, Input, InputEvent, InputState, Position, Rope, RopeExt, TabSize,
}, },
resizable::{ResizableState, h_resizable, resizable_panel}, resizable::{h_resizable, resizable_panel},
tree, v_flex, tree, v_flex,
}; };
use lsp_types::{ use lsp_types::{
@ -44,7 +44,6 @@ fn init() {
pub struct Example { pub struct Example {
editor: Entity<InputState>, editor: Entity<InputState>,
tree_state: Entity<TreeState>, tree_state: Entity<TreeState>,
panels_state: Entity<ResizableState>,
go_to_line_state: Entity<InputState>, go_to_line_state: Entity<InputState>,
language: Language, language: Language,
line_number: bool, line_number: bool,
@ -662,12 +661,9 @@ impl Example {
this.lint_document(cx); this.lint_document(cx);
})]; })];
let panels_state = ResizableState::new(cx);
Self { Self {
editor, editor,
tree_state, tree_state,
panels_state,
go_to_line_state, go_to_line_state,
language: default_language, language: default_language,
line_number: true, line_number: true,
@ -993,22 +989,21 @@ impl Render for Example {
.w_full() .w_full()
.flex_1() .flex_1()
.child( .child(
h_resizable("editor-container", self.panels_state.clone()) h_resizable("editor-container")
.child( .child(
resizable_panel() resizable_panel()
.size(px(240.)) .size(px(240.))
.child(self.render_file_tree(window, cx)), .child(self.render_file_tree(window, cx)),
) )
.child( .child(
resizable_panel().child(
Input::new(&self.editor) Input::new(&self.editor)
.bordered(false) .bordered(false)
.p_0() .p_0()
.h_full() .h_full()
.font_family("Monaco") .font_family("Monaco")
.text_size(px(12.)) .text_size(px(12.))
.focus_bordered(false), .focus_bordered(false)
), .into_any_element(),
), ),
) )
.child( .child(

View file

@ -2,14 +2,13 @@ use gpui::*;
use gpui_component::{ use gpui_component::{
highlighter::Language, highlighter::Language,
input::{Input, InputState, TabSize}, input::{Input, InputState, TabSize},
resizable::{ResizableState, h_resizable, resizable_panel}, resizable::h_resizable,
text::TextView, text::TextView,
}; };
use story::Assets; use story::Assets;
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
resizable_state: Entity<ResizableState>,
_subscribe: Subscription, _subscribe: Subscription,
} }
@ -28,8 +27,6 @@ impl Example {
.placeholder("Enter your HTML here...") .placeholder("Enter your HTML here...")
}); });
let resizable_state = ResizableState::new(cx);
let _subscribe = cx.subscribe( let _subscribe = cx.subscribe(
&input_state, &input_state,
|_, _, _: &gpui_component::input::InputEvent, cx| { |_, _, _: &gpui_component::input::InputEvent, cx| {
@ -39,7 +36,6 @@ impl Example {
Self { Self {
input_state, input_state,
resizable_state,
_subscribe, _subscribe,
} }
} }
@ -51,9 +47,8 @@ impl Example {
impl Render for Example { impl Render for Example {
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 {
h_resizable("container", self.resizable_state.clone()) h_resizable("container")
.child( .child(
resizable_panel().child(
div() div()
.id("source") .id("source")
.size_full() .size_full()
@ -64,11 +59,10 @@ impl Render for Example {
.h_full() .h_full()
.appearance(false) .appearance(false)
.focus_bordered(false), .focus_bordered(false),
), )
), .into_any(),
) )
.child( .child(
resizable_panel().child(
TextView::html( TextView::html(
"preview", "preview",
self.input_state.read(cx).value().clone(), self.input_state.read(cx).value().clone(),
@ -77,8 +71,8 @@ impl Render for Example {
) )
.p_5() .p_5()
.scrollable() .scrollable()
.selectable(), .selectable()
), .into_any(),
) )
} }
} }

View file

@ -2,14 +2,13 @@ use gpui::*;
use gpui_component::{ use gpui_component::{
highlighter::Language, highlighter::Language,
input::{Input, InputEvent, InputState, TabSize}, input::{Input, InputEvent, InputState, TabSize},
resizable::{ResizableState, h_resizable, resizable_panel}, resizable::{h_resizable, resizable_panel},
text::TextView, text::TextView,
}; };
use story::{Assets, Open}; use story::{Assets, Open};
pub struct Example { pub struct Example {
input_state: Entity<InputState>, input_state: Entity<InputState>,
resizable_state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -29,12 +28,10 @@ impl Example {
.placeholder("Enter your Markdown here...") .placeholder("Enter your Markdown here...")
.default_value(EXAMPLE) .default_value(EXAMPLE)
}); });
let resizable_state = ResizableState::new(cx);
let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})]; let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})];
Self { Self {
resizable_state,
input_state, input_state,
_subscriptions, _subscriptions,
} }
@ -79,7 +76,7 @@ impl Render for Example {
.size_full() .size_full()
.on_action(cx.listener(Self::on_action_open)) .on_action(cx.listener(Self::on_action_open))
.child( .child(
h_resizable("container", self.resizable_state.clone()) h_resizable("container")
.child( .child(
resizable_panel().child( resizable_panel().child(
div() div()

View file

@ -2,7 +2,7 @@ use gpui::{prelude::*, *};
use gpui_component::{ use gpui_component::{
ActiveTheme as _, Icon, IconName, h_flex, ActiveTheme as _, Icon, IconName, h_flex,
input::{Input, InputEvent, InputState}, input::{Input, InputEvent, InputState},
resizable::{ResizableState, h_resizable, resizable_panel}, resizable::{h_resizable, resizable_panel},
sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem}, sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem},
v_flex, v_flex,
}; };
@ -14,7 +14,6 @@ pub struct Gallery {
active_index: Option<usize>, active_index: Option<usize>,
collapsed: bool, collapsed: bool,
search_input: Entity<InputState>, search_input: Entity<InputState>,
sidebar_state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -91,7 +90,6 @@ impl Gallery {
active_group_index: Some(0), active_group_index: Some(0),
active_index: Some(0), active_index: Some(0),
collapsed: false, collapsed: false,
sidebar_state: ResizableState::new(cx),
_subscriptions, _subscriptions,
}; };
@ -148,7 +146,7 @@ impl Render for Gallery {
("".into(), "".into()) ("".into(), "".into())
}; };
h_resizable("gallery-container", self.sidebar_state.clone()) h_resizable("gallery-container")
.child( .child(
resizable_panel() resizable_panel()
.size(px(255.)) .size(px(255.))

View file

@ -1,17 +1,15 @@
use gpui::{ use gpui::{
div, px, AnyElement, App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, AnyElement, App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement,
ParentElement as _, Pixels, Render, SharedString, Styled, Window, ParentElement as _, Pixels, Render, SharedString, Styled, Window, div, px,
}; };
use gpui_component::{ use gpui_component::{
resizable::{h_resizable, resizable_panel, v_resizable, ResizableState}, ActiveTheme,
v_flex, ActiveTheme, resizable::{h_resizable, resizable_panel, v_resizable},
v_flex,
}; };
pub struct ResizableStory { pub struct ResizableStory {
focus_handle: FocusHandle, focus_handle: FocusHandle,
state1: Entity<ResizableState>,
state2: Entity<ResizableState>,
state3: Entity<ResizableState>,
} }
impl super::Story for ResizableStory { impl super::Story for ResizableStory {
@ -40,15 +38,8 @@ impl ResizableStory {
} }
fn new(_: &mut Window, cx: &mut App) -> Self { fn new(_: &mut Window, cx: &mut App) -> Self {
let state1 = ResizableState::new(cx);
let state2 = ResizableState::new(cx);
let state3 = ResizableState::new(cx);
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
state1,
state2,
state3,
} }
} }
} }
@ -62,7 +53,7 @@ fn panel_box(content: impl Into<SharedString>, _: &App) -> AnyElement {
} }
impl Render for ResizableStory { impl Render for ResizableStory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full() .size_full()
.gap_6() .gap_6()
@ -72,9 +63,12 @@ impl Render for ResizableStory {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.child( .child(
v_resizable("resizable-1", self.state1.clone()) v_resizable("resizable-1")
.group( .on_resize(|state, _, cx| {
h_resizable("resizable-1.1", self.state2.clone()) println!("Resized: {:?}", state.read(cx).sizes());
})
.child(
h_resizable("resizable-1.1")
.size(px(150.)) .size(px(150.))
.child( .child(
resizable_panel() resizable_panel()
@ -82,14 +76,14 @@ impl Render for ResizableStory {
.size_range(px(120.)..px(300.)) .size_range(px(120.)..px(300.))
.child(panel_box("Left (120px .. 300px)", cx)), .child(panel_box("Left (120px .. 300px)", cx)),
) )
.child(resizable_panel().child(panel_box("Center", cx))) .child(panel_box("Center", cx))
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.child(panel_box("Right", cx)), .child(panel_box("Right", cx)),
), ),
) )
.child(resizable_panel().child(panel_box("Center", cx))) .child(panel_box("Center", cx))
.child( .child(
resizable_panel() resizable_panel()
.size(px(80.)) .size(px(80.))
@ -104,14 +98,14 @@ impl Render for ResizableStory {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.child( .child(
h_resizable("resizable-3", self.state3.clone()) h_resizable("resizable-3")
.child( .child(
resizable_panel() resizable_panel()
.size(px(200.)) .size(px(200.))
.size_range(px(200.)..px(400.)) .size_range(px(200.)..px(400.))
.child(panel_box("Left 2", cx)), .child(panel_box("Left 2", cx)),
) )
.child(resizable_panel().child(panel_box("Right (Grow)", cx))), .child(panel_box("Right (Grow)", cx)),
), ),
) )
} }

View file

@ -12,8 +12,9 @@ use crate::{
use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel}; use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel};
use gpui::{ use gpui::{
App, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, App, AppContext as _, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
ParentElement, Pixels, Render, Styled, Subscription, WeakEntity, Window, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, WeakEntity,
Window,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@ -53,7 +54,7 @@ impl Panel for StackPanel {
impl StackPanel { impl StackPanel {
pub fn new(axis: Axis, _: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(axis: Axis, _: &mut Window, cx: &mut Context<Self>) -> Self {
let state = ResizableState::new(cx); let state = cx.new(|_| ResizableState::default());
// Bubble up the resize event. // Bubble up the resize event.
let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| { let _subscriptions = vec![cx.subscribe(&state, |_, _, _: &ResizablePanelEvent, cx| {
@ -417,7 +418,8 @@ impl Render for StackPanel {
.overflow_hidden() .overflow_hidden()
.bg(cx.theme().tab_bar) .bg(cx.theme().tab_bar)
.child( .child(
ResizablePanelGroup::new("stack-panel-group", self.state.clone()) ResizablePanelGroup::new("stack-panel-group")
.with_state(&self.state)
.axis(self.axis) .axis(self.axis)
.children(self.panels.clone().into_iter().map(|panel| { .children(self.panels.clone().into_iter().map(|panel| {
resizable_panel() resizable_panel()

View file

@ -1,9 +1,6 @@
use std::ops::Range; use std::ops::Range;
use gpui::{ use gpui::{px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, Pixels, Window};
px, Along, App, AppContext, Axis, Bounds, Context, ElementId, Entity, EventEmitter, Pixels,
Window,
};
use crate::PixelsExt; use crate::PixelsExt;
@ -15,13 +12,13 @@ pub(crate) use resize_handle::*;
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
/// Create a [`ResizablePanelGroup`] with horizontal resizing /// Create a [`ResizablePanelGroup`] with horizontal resizing
pub fn h_resizable(id: impl Into<ElementId>, state: Entity<ResizableState>) -> ResizablePanelGroup { pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
ResizablePanelGroup::new(id, state).axis(Axis::Horizontal) ResizablePanelGroup::new(id).axis(Axis::Horizontal)
} }
/// Create a [`ResizablePanelGroup`] with vertical resizing /// Create a [`ResizablePanelGroup`] with vertical resizing
pub fn v_resizable(id: impl Into<ElementId>, state: Entity<ResizableState>) -> ResizablePanelGroup { pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
ResizablePanelGroup::new(id, state).axis(Axis::Vertical) ResizablePanelGroup::new(id).axis(Axis::Vertical)
} }
/// Create a [`ResizablePanel`]. /// Create a [`ResizablePanel`].
@ -29,8 +26,8 @@ pub fn resizable_panel() -> ResizablePanel {
ResizablePanel::new() ResizablePanel::new()
} }
#[derive(Debug, Clone)]
/// State for a [`ResizablePanel`] /// State for a [`ResizablePanel`]
#[derive(Debug, Clone)]
pub struct ResizableState { pub struct ResizableState {
/// The `axis` will sync to actual axis of the ResizablePanelGroup in use. /// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
axis: Axis, axis: Axis,
@ -40,18 +37,25 @@ pub struct ResizableState {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
} }
impl ResizableState { impl Default for ResizableState {
pub fn new(cx: &mut App) -> Entity<Self> { fn default() -> Self {
cx.new(|_| Self { Self {
axis: Axis::Horizontal, axis: Axis::Horizontal,
panels: vec![], panels: vec![],
sizes: vec![], sizes: vec![],
resizing_panel_ix: None, resizing_panel_ix: None,
bounds: Bounds::default(), bounds: Bounds::default(),
}) }
}
} }
pub fn insert_panel( impl ResizableState {
/// Get the size of the panels.
pub fn sizes(&self) -> &Vec<Pixels> {
&self.sizes
}
pub(crate) fn insert_panel(
&mut self, &mut self,
size: Option<Pixels>, size: Option<Pixels>,
ix: Option<usize>, ix: Option<usize>,
@ -126,11 +130,6 @@ impl ResizableState {
self.sizes.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 { pub(crate) fn total_size(&self) -> Pixels {
self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into() self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into()
} }

View file

@ -1,4 +1,7 @@
use std::ops::{Deref, Range}; use std::{
ops::{Deref, Range},
rc::Rc,
};
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, AnyElement, App, AppContext, Axis, Bounds, Context, canvas, div, prelude::FluentBuilder, AnyElement, App, AppContext, Axis, Bounds, Context,
@ -26,23 +29,32 @@ impl Render for DragPanel {
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ResizablePanelGroup { pub struct ResizablePanelGroup {
id: ElementId, id: ElementId,
state: Entity<ResizableState>, state: Option<Entity<ResizableState>>,
axis: Axis, axis: Axis,
size: Option<Pixels>, size: Option<Pixels>,
children: Vec<ResizablePanel>, children: Vec<ResizablePanel>,
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
} }
impl ResizablePanelGroup { impl ResizablePanelGroup {
pub(crate) fn new(id: impl Into<ElementId>, state: Entity<ResizableState>) -> Self { /// Create a new resizable panel group.
pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
id: id.into(), id: id.into(),
axis: Axis::Horizontal, axis: Axis::Horizontal,
children: vec![], children: vec![],
state, state: None,
size: None, size: None,
on_resize: Rc::new(|_, _, _| {}),
} }
} }
/// Bind yourself to a resizable state entity.
pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
self.state = Some(state.clone());
self
}
/// Set the axis of the resizable panel group, default is horizontal. /// Set the axis of the resizable panel group, default is horizontal.
pub fn axis(mut self, axis: Axis) -> Self { pub fn axis(mut self, axis: Axis) -> Self {
self.axis = axis; self.axis = axis;
@ -67,11 +79,6 @@ impl ResizablePanelGroup {
self self
} }
/// Add a ResizablePanelGroup as a child to the group.
pub fn group(self, group: ResizablePanelGroup) -> Self {
self.child(resizable_panel().child(group.into_any_element()))
}
/// Set size of the resizable panel group /// Set size of the resizable panel group
/// ///
/// - When the axis is horizontal, the size is the height of the group. /// - When the axis is horizontal, the size is the height of the group.
@ -80,6 +87,19 @@ impl ResizablePanelGroup {
self.size = Some(size); self.size = Some(size);
self self
} }
/// Set the callback to be called when the panels are resized.
///
/// ## Callback arguments
///
/// - Entity<ResizableState>: The state of the ResizablePanelGroup.
pub fn on_resize(
mut self,
on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_resize = Rc::new(on_resize);
self
}
} }
impl<T> From<T> for ResizablePanel impl<T> From<T> for ResizablePanel
where where
@ -90,11 +110,19 @@ where
} }
} }
impl From<ResizablePanelGroup> for ResizablePanel {
fn from(value: ResizablePanelGroup) -> Self {
resizable_panel().child(value)
}
}
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {} impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
impl RenderOnce for ResizablePanelGroup { impl RenderOnce for ResizablePanelGroup {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = self.state.clone(); let state = self.state.unwrap_or(
window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
);
let container = if self.axis.is_horizontal() { let container = if self.axis.is_horizontal() {
h_flex() h_flex()
} else { } else {
@ -103,7 +131,7 @@ impl RenderOnce for ResizablePanelGroup {
// Sync panels to the state // Sync panels to the state
let panels_count = self.children.len(); let panels_count = self.children.len();
self.state.update(cx, |state, _| { state.update(cx, |state, _| {
state.sync_panels_count(self.axis, panels_count); state.sync_panels_count(self.axis, panels_count);
}); });
@ -117,21 +145,25 @@ impl RenderOnce for ResizablePanelGroup {
.map(|(ix, mut panel)| { .map(|(ix, mut panel)| {
panel.panel_ix = ix; panel.panel_ix = ix;
panel.axis = self.axis; panel.axis = self.axis;
panel.state = Some(self.state.clone()); panel.state = Some(state.clone());
panel panel
}), }),
) )
.child({ .child({
canvas( canvas(
move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds), {
let state = state.clone();
move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
},
|_, _, _, _| {}, |_, _, _, _| {},
) )
.absolute() .absolute()
.size_full() .size_full()
}) })
.child(ResizePanelGroupElement { .child(ResizePanelGroupElement {
state: self.state.clone(), state: state.clone(),
axis: self.axis, axis: self.axis,
on_resize: self.on_resize.clone(),
}) })
} }
} }
@ -267,6 +299,7 @@ impl RenderOnce for ResizablePanel {
struct ResizePanelGroupElement { struct ResizePanelGroupElement {
state: Entity<ResizableState>, state: Entity<ResizableState>,
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
axis: Axis, axis: Axis,
} }
@ -352,12 +385,14 @@ impl Element for ResizePanelGroupElement {
window.on_mouse_event({ window.on_mouse_event({
let state = self.state.clone(); let state = self.state.clone();
let current_ix = state.read(cx).resizing_panel_ix; let current_ix = state.read(cx).resizing_panel_ix;
move |_: &MouseUpEvent, phase, _, cx| { let on_resize = self.on_resize.clone();
move |_: &MouseUpEvent, phase, window, cx| {
if current_ix.is_none() { if current_ix.is_none() {
return; return;
} }
if phase.bubble() { if phase.bubble() {
state.update(cx, |state, cx| state.done_resizing(cx)); state.update(cx, |state, cx| state.done_resizing(cx));
on_resize(&state, window, cx);
} }
} }
}) })

View file

@ -18,37 +18,49 @@ use gpui_component::resizable::{
## Usage ## Usage
### Basic Horizontal Layout Use `h_resizable` to create a horizontal layout, `v_resizable` to create a vertical layout.
The first argument is the `id` for this [ResizablePanelGroup].
:::tip
In GPUI, the `id` must be unique within the layout scope (The nearest parent has presents `id`).
:::
```rust ```rust
let state = ResizableState::new(cx); h_resizable("my-layout")
.on_resize(|state, window, cx| {
h_resizable("my-layout", state) // Handle resize event
// You can read the panel sizes from the state.
let state = state.read(cx);
let sizes = state.sizes();
})
.child( .child(
// Use resizable_panel() to create a sized panel.
resizable_panel() resizable_panel()
.size(px(200.)) .size(px(200.))
.child("Left Panel") .child("Left Panel")
) )
.child( .child(
resizable_panel() // Or you can just add AnyElement without a size.
div()
.child("Right Panel") .child("Right Panel")
.into_any_element()
) )
``` ```
### Basic Vertical Layout The `v_resizable` component is used to create a vertical layout.
```rust ```rust
let state = ResizableState::new(cx); v_resizable("vertical-layout")
v_resizable("vertical-layout", state)
.child( .child(
resizable_panel() resizable_panel()
.size(px(100.)) .size(px(100.))
.child("Top Panel") .child("Top Panel")
) )
.child( .child(
resizable_panel() div()
.child("Bottom Panel") .child("Bottom Panel")
.into_any_element()
) )
``` ```
@ -85,15 +97,12 @@ h_resizable("multi-panel", state)
### Nested Layouts ### Nested Layouts
```rust ```rust
let main_state = ResizableState::new(cx); v_resizable("main-layout", window, cx)
let nested_state = ResizableState::new(cx);
v_resizable("main-layout", main_state)
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.child( .child(
h_resizable("nested-layout", nested_state) h_resizable("nested-layout", window, cx)
.child( .child(
resizable_panel() resizable_panel()
.size(px(200.)) .size(px(200.))
@ -114,17 +123,14 @@ v_resizable("main-layout", main_state)
### Nested Panel Groups ### Nested Panel Groups
```rust ```rust
let outer_state = ResizableState::new(cx); h_resizable("outer", window, cx)
let inner_state = ResizableState::new(cx);
h_resizable("outer", outer_state)
.child( .child(
resizable_panel() resizable_panel()
.size(px(200.)) .size(px(200.))
.child("Left Panel") .child("Left Panel")
) )
.group( .group(
v_resizable("inner", inner_state) v_resizable("inner", window, cx)
.child( .child(
resizable_panel() resizable_panel()
.size(px(150.)) .size(px(150.))
@ -137,42 +143,6 @@ h_resizable("outer", outer_state)
) )
``` ```
### Handling Resize Events
```rust
struct MyView {
resizable_state: Entity<ResizableState>,
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let resizable_state = ResizableState::new(cx);
// Subscribe to resize events
let subscription = cx.subscribe(&resizable_state, |this, _, event: &ResizablePanelEvent, cx| {
match event {
ResizablePanelEvent::Resized => {
// Handle resize completion
println!("Panel resized!");
this.handle_resize_complete(cx);
}
}
});
Self {
resizable_state,
}
}
fn handle_resize_complete(&mut self, cx: &mut Context<Self>) {
// Access current panel sizes
let sizes = self.resizable_state.read(cx).sizes();
println!("Current panel sizes: {:?}", sizes);
cx.notify();
}
}
```
### Conditional Panel Visibility ### Conditional Panel Visibility
```rust ```rust
@ -202,114 +172,18 @@ resizable_panel()
.child("Fixed Panel") .child("Fixed Panel")
``` ```
## API Reference
### ResizableState
| Method | Description |
| --------- | ----------------------------------------- |
| `new(cx)` | Create a new resizable state entity |
| `sizes()` | Get current panel sizes as `&Vec<Pixels>` |
### Resizable Panel Group Functions
| Function | Description |
| ------------------------ | --------------------------------------- |
| `h_resizable(id, state)` | Create horizontal resizable panel group |
| `v_resizable(id, state)` | Create vertical resizable panel group |
| `resizable_panel()` | Create a new resizable panel |
### ResizablePanelGroup
| Method | Description |
| ------------------ | ----------------------------------------- |
| `new(id, state)` | Create a new panel group |
| `axis(axis)` | Set resize axis (Horizontal/Vertical) |
| `child(panel)` | Add a resizable panel to the group |
| `children(panels)` | Add multiple panels at once |
| `group(group)` | Add another panel group as a nested child |
| `size(size)` | Set the size of the group container |
### ResizablePanel
| Method | Description |
| ------------------- | ------------------------------- |
| `new()` | Create a new resizable panel |
| `child(element)` | Add child element to the panel |
| `size(pixels)` | Set initial panel size |
| `size_range(range)` | Set size constraints (min..max) |
| `visible(bool)` | Control panel visibility |
### Size Constraints
| Constraint | Description |
| ----------------------------- | ------------------------------- |
| `px(100.)..px(400.)` | Panel can be 100px to 400px |
| `px(150.)..Pixels::MAX` | Panel minimum 150px, no maximum |
| `PANEL_MIN_SIZE..Pixels::MAX` | Default constraints |
### ResizablePanelEvent
| Event | Description |
| --------- | -------------------------------------- |
| `Resized` | Emitted when a panel finishes resizing |
## Drag Handles
Resize handles are automatically created between panels:
- **Horizontal layouts**: Vertical drag handles between panels
- **Vertical layouts**: Horizontal drag handles between panels
- **Visual feedback**: Handles show hover and active states
- **Cursor changes**: Appropriate resize cursors on hover
- **Handle size**: 1px wide with 4px padding for easier interaction
### Handle Behavior
- Handles appear between adjacent panels
- Dragging adjusts sizes of neighboring panels
- Panels respect their size constraints during resize
- Overflow is handled by adjusting panel sizes proportionally
## Direction Support
### Horizontal Resizing
```rust
h_resizable("horizontal", state)
.child(resizable_panel().child("Left"))
.child(resizable_panel().child("Right"))
```
- Panels are arranged side by side
- Vertical drag handles between panels
- Resize by dragging left/right
### Vertical Resizing
```rust
v_resizable("vertical", state)
.child(resizable_panel().child("Top"))
.child(resizable_panel().child("Bottom"))
```
- Panels are stacked vertically
- Horizontal drag handles between panels
- Resize by dragging up/down
## Examples ## Examples
### File Explorer Layout ### File Explorer Layout
```rust ```rust
struct FileExplorer { struct FileExplorer {
layout_state: Entity<ResizableState>,
show_sidebar: bool, show_sidebar: bool,
} }
impl Render for FileExplorer { impl Render for FileExplorer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_resizable("file-explorer", self.layout_state.clone()) h_resizable("file-explorer", window, cx)
.child( .child(
resizable_panel() resizable_panel()
.visible(self.show_sidebar) .visible(self.show_sidebar)
@ -324,8 +198,6 @@ impl Render for FileExplorer {
.child("• Downloads") .child("• Downloads")
) )
) )
.child(
resizable_panel()
.child( .child(
v_flex() v_flex()
.p_4() .p_4()
@ -333,7 +205,7 @@ impl Render for FileExplorer {
.child("file1.txt") .child("file1.txt")
.child("file2.pdf") .child("file2.pdf")
.child("image.png") .child("image.png")
) .into_any_element()
) )
} }
} }