Skip to main content

rgpui_component/resizable/
panel.rs

1use std::{
2    ops::{Deref, Range},
3    rc::Rc,
4};
5
6use rgpui::{
7    Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
8    EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
9    ParentElement, Pixels, Render, RenderOnce, Style, StyleRefinement, Styled, Window, div,
10    prelude::FluentBuilder,
11};
12
13use crate::{
14    AxisExt, ElementExt, h_flex, resizable::PANEL_MIN_SIZE, styled::StyledExt as _, v_flex,
15};
16
17use super::{ResizableState, resizable_panel, resize_handle};
18
19pub enum ResizablePanelEvent {
20    Resized,
21}
22
23#[derive(Clone)]
24pub(crate) struct DragPanel;
25impl Render for DragPanel {
26    fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
27        Empty
28    }
29}
30
31/// A group of resizable panels.
32#[derive(IntoElement)]
33pub struct ResizablePanelGroup {
34    id: ElementId,
35    state: Option<Entity<ResizableState>>,
36    axis: Axis,
37    size: Option<Pixels>,
38    children: Vec<ResizablePanel>,
39    on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
40}
41
42impl ResizablePanelGroup {
43    /// Create a new resizable panel group.
44    pub fn new(id: impl Into<ElementId>) -> Self {
45        Self {
46            id: id.into(),
47            axis: Axis::Horizontal,
48            children: vec![],
49            state: None,
50            size: None,
51            on_resize: Rc::new(|_, _, _| {}),
52        }
53    }
54
55    /// Bind yourself to a resizable state entity.
56    ///
57    /// If not provided, it will handle its own state internally.
58    pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
59        self.state = Some(state.clone());
60        self
61    }
62
63    /// Set the axis of the resizable panel group, default is horizontal.
64    pub fn axis(mut self, axis: Axis) -> Self {
65        self.axis = axis;
66        self
67    }
68
69    /// Add a panel to the group.
70    ///
71    /// - The `axis` will be set to the same axis as the group.
72    /// - The `initial_size` will be set to the average size of all panels if not provided.
73    /// - The `group` will be set to the group entity.
74    pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
75        self.children.push(panel.into());
76        self
77    }
78
79    /// Add multiple panels to the group.
80    pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
81    where
82        I: Into<ResizablePanel>,
83    {
84        self.children = panels.into_iter().map(|panel| panel.into()).collect();
85        self
86    }
87
88    /// Set size of the resizable panel group
89    ///
90    /// - When the axis is horizontal, the size is the height of the group.
91    /// - When the axis is vertical, the size is the width of the group.
92    pub fn size(mut self, size: Pixels) -> Self {
93        self.size = Some(size);
94        self
95    }
96
97    /// Set the callback to be called when the panels are resized.
98    ///
99    /// ## Callback arguments
100    ///
101    /// - Entity<ResizableState>: The state of the ResizablePanelGroup.
102    pub fn on_resize(
103        mut self,
104        on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
105    ) -> Self {
106        self.on_resize = Rc::new(on_resize);
107        self
108    }
109}
110
111impl<T> From<T> for ResizablePanel
112where
113    T: Into<AnyElement>,
114{
115    fn from(value: T) -> Self {
116        resizable_panel().child(value.into())
117    }
118}
119
120impl From<ResizablePanelGroup> for ResizablePanel {
121    fn from(value: ResizablePanelGroup) -> Self {
122        resizable_panel().child(value)
123    }
124}
125
126impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
127
128impl RenderOnce for ResizablePanelGroup {
129    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
130        let state = self.state.unwrap_or(
131            window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
132        );
133        let container = if self.axis.is_horizontal() {
134            h_flex()
135        } else {
136            v_flex()
137        };
138
139        // Sync panels to the state
140        let panels_count = self.children.len();
141        state.update(cx, |state, cx| {
142            state.sync_panels_count(self.axis, panels_count, cx);
143        });
144
145        container
146            .id(self.id)
147            .size_full()
148            .children(
149                self.children
150                    .into_iter()
151                    .enumerate()
152                    .map(|(ix, mut panel)| {
153                        panel.panel_ix = ix;
154                        panel.axis = self.axis;
155                        panel.state = Some(state.clone());
156                        panel
157                    }),
158            )
159            .on_prepaint({
160                let state = state.clone();
161                move |bounds, _, cx| {
162                    state.update(cx, |state, cx| {
163                        let size_changed =
164                            state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
165
166                        state.bounds = bounds;
167
168                        if size_changed {
169                            state.adjust_to_container_size(cx);
170                        }
171                    })
172                }
173            })
174            .child(ResizePanelGroupElement {
175                state: state.clone(),
176                axis: self.axis,
177                on_resize: self.on_resize.clone(),
178            })
179    }
180}
181
182/// A resizable panel inside a [`ResizablePanelGroup`].
183///
184/// Implements [`Styled`], so call sites can override the panel's
185/// rendered styles. User overrides are applied **between** the panel's
186/// flex defaults and its size management 鈥?the caller can override the
187/// internal `flex_grow: 1` (e.g. via `.flex_none()`) and add their own
188/// padding / colors / borders, while the panel's runtime size
189/// constraints (`min_w`/`max_w`/`flex_basis` driven by `ResizableState`)
190/// always win.
191///
192/// A common override is `.flex_none()`: the panel sets `flex_grow: 1`
193/// internally, so a sized panel that should hold its width when a
194/// sibling collapses needs to opt out of growth via `.flex_none()`.
195///
196/// ```ignore
197/// h_resizable("layout")
198///     .child(resizable_panel().size(px(220.)).flex_none().child(sidebar))
199///     .child(resizable_panel().child(content))                // flex
200///     .child(resizable_panel().size(px(280.)).flex_none().child(metadata))
201/// ```
202///
203/// **Reserved styles**: do not call these from outside 鈥?they fight the
204/// panel's own layout management:
205/// - `.flex_basis(...)` 鈥?driven by `ResizableState`, not by the caller.
206/// - `.absolute()` 鈥?would remove the panel from the resizable's flex flow.
207/// - `.overflow_hidden()` 鈥?may clip the resize handle, which is positioned
208///   absolute at `left: -4px` of each panel after the first.
209#[derive(IntoElement)]
210pub struct ResizablePanel {
211    axis: Axis,
212    panel_ix: usize,
213    state: Option<Entity<ResizableState>>,
214    /// Initial size is the size that the panel has when it is created.
215    initial_size: Option<Pixels>,
216    /// size range limit of this panel.
217    size_range: Range<Pixels>,
218    children: Vec<AnyElement>,
219    visible: bool,
220    style: StyleRefinement,
221}
222
223impl ResizablePanel {
224    /// Create a new resizable panel.
225    pub(super) fn new() -> Self {
226        Self {
227            panel_ix: 0,
228            initial_size: None,
229            state: None,
230            size_range: (PANEL_MIN_SIZE..Pixels::MAX),
231            axis: Axis::Horizontal,
232            children: vec![],
233            visible: true,
234            style: StyleRefinement::default(),
235        }
236    }
237
238    /// Set the visibility of the panel, default is true.
239    pub fn visible(mut self, visible: bool) -> Self {
240        self.visible = visible;
241        self
242    }
243
244    /// Set the initial size of the panel.
245    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
246        self.initial_size = Some(size.into());
247        self
248    }
249
250    /// Set the size range to limit panel resize.
251    ///
252    /// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
253    pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
254        self.size_range = range.into();
255        self
256    }
257}
258
259impl Styled for ResizablePanel {
260    fn style(&mut self) -> &mut StyleRefinement {
261        &mut self.style
262    }
263}
264
265impl ParentElement for ResizablePanel {
266    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
267        self.children.extend(elements);
268    }
269}
270
271impl RenderOnce for ResizablePanel {
272    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
273        if !self.visible {
274            return div().id(("resizable-panel", self.panel_ix));
275        }
276
277        let state = self
278            .state
279            .expect("BUG: The `state` in ResizablePanel should be present.");
280        let panel_state = state
281            .read(cx)
282            .panels
283            .get(self.panel_ix)
284            .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
285        let size_range = self.size_range.clone();
286
287        div()
288            .id(("resizable-panel", self.panel_ix))
289            .flex()
290            .flex_grow()
291            .size_full()
292            .relative()
293            // Apply caller style overrides here 鈥?between the flex defaults
294            // above and the size management below. This lets callers cancel
295            // the unconditional `.flex_grow()` (via `.flex_none()`, the load-
296            // bearing case for sized panels next to a collapsing sibling) and
297            // add their own padding / colors / borders, while keeping the
298            // panel's runtime size constraints (min/max + `flex_basis` driven
299            // by `ResizableState`) authoritative.
300            .refine_style(&self.style)
301            .when(self.axis.is_vertical(), |this| {
302                this.min_h(size_range.start).max_h(size_range.end)
303            })
304            .when(self.axis.is_horizontal(), |this| {
305                this.min_w(size_range.start).max_w(size_range.end)
306            })
307            // 1. initial_size is None, to use auto size.
308            // 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
309            // 3. initial_size is Some and size is Some, use `size`.
310            .when(self.initial_size.is_none(), |this| this.flex_shrink())
311            .when_some(self.initial_size, |this, initial_size| {
312                // The `self.size` is None, that mean the initial size for the panel,
313                // so we need set `flex_shrink_0` To let it keep the initial size.
314                this.when(
315                    panel_state.size.is_none() && !initial_size.is_zero(),
316                    |this| this.flex_none(),
317                )
318                .flex_basis(initial_size)
319            })
320            .map(|this| match panel_state.size {
321                Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
322                None => this,
323            })
324            .on_prepaint({
325                let state = state.clone();
326                move |bounds, _, cx| {
327                    state.update(cx, |state, cx| {
328                        state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
329                    })
330                }
331            })
332            .children(self.children)
333            .when(self.panel_ix > 0, |this| {
334                let ix = self.panel_ix - 1;
335                this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag(
336                    DragPanel,
337                    move |drag_panel, _, _, cx| {
338                        cx.stop_propagation();
339                        // Set current resizing panel ix
340                        state.update(cx, |state, _| {
341                            state.resizing_panel_ix = Some(ix);
342                        });
343                        cx.new(|_| drag_panel.deref().clone())
344                    },
345                ))
346            })
347    }
348}
349
350struct ResizePanelGroupElement {
351    state: Entity<ResizableState>,
352    on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
353    axis: Axis,
354}
355
356impl IntoElement for ResizePanelGroupElement {
357    type Element = Self;
358
359    fn into_element(self) -> Self::Element {
360        self
361    }
362}
363
364impl Element for ResizePanelGroupElement {
365    type RequestLayoutState = ();
366    type PrepaintState = ();
367
368    fn id(&self) -> Option<rgpui::ElementId> {
369        None
370    }
371
372    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
373        None
374    }
375
376    fn request_layout(
377        &mut self,
378        _: Option<&rgpui::GlobalElementId>,
379        _: Option<&rgpui::InspectorElementId>,
380        window: &mut Window,
381        cx: &mut App,
382    ) -> (rgpui::LayoutId, Self::RequestLayoutState) {
383        (window.request_layout(Style::default(), None, cx), ())
384    }
385
386    fn prepaint(
387        &mut self,
388        _: Option<&rgpui::GlobalElementId>,
389        _: Option<&rgpui::InspectorElementId>,
390        _: Bounds<Pixels>,
391        _: &mut Self::RequestLayoutState,
392        _window: &mut Window,
393        _cx: &mut App,
394    ) -> Self::PrepaintState {
395        ()
396    }
397
398    fn paint(
399        &mut self,
400        _: Option<&rgpui::GlobalElementId>,
401        _: Option<&rgpui::InspectorElementId>,
402        _: Bounds<Pixels>,
403        _: &mut Self::RequestLayoutState,
404        _: &mut Self::PrepaintState,
405        window: &mut Window,
406        cx: &mut App,
407    ) {
408        window.on_mouse_event({
409            let state = self.state.clone();
410            let axis = self.axis;
411            let current_ix = state.read(cx).resizing_panel_ix;
412            move |e: &MouseMoveEvent, phase, window, cx| {
413                if !phase.bubble() {
414                    return;
415                }
416                let Some(ix) = current_ix else { return };
417
418                state.update(cx, |state, cx| {
419                    let panel = state.panels.get(ix).expect("BUG: invalid panel index");
420
421                    match axis {
422                        Axis::Horizontal => state.resize_panel_at_handle(
423                            ix,
424                            e.position.x - panel.bounds.left(),
425                            window,
426                            cx,
427                        ),
428                        Axis::Vertical => state.resize_panel_at_handle(
429                            ix,
430                            e.position.y - panel.bounds.top(),
431                            window,
432                            cx,
433                        ),
434                    }
435                    cx.notify();
436                })
437            }
438        });
439
440        // When any mouse up, stop dragging
441        window.on_mouse_event({
442            let state = self.state.clone();
443            let current_ix = state.read(cx).resizing_panel_ix;
444            let on_resize = self.on_resize.clone();
445            move |_: &MouseUpEvent, phase, window, cx| {
446                if current_ix.is_none() {
447                    return;
448                }
449                if phase.bubble() {
450                    state.update(cx, |state, cx| state.done_resizing(cx));
451                    on_resize(&state, window, cx);
452                }
453            }
454        })
455    }
456}