Skip to main content

gpui_base/resizable/
panel.rs

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