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, window, 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                            // The adjustment lands after this frame's layout has
187                            // already been computed, and a notify raised during a
188                            // draw only records the view as dirty without scheduling
189                            // a frame for it. Defer the notify so it runs once the
190                            // draw has finished and can schedule the settling frame.
191                            // Otherwise that frame stays pending until some later
192                            // input repaints the window, and the divider appears to
193                            // jump on hover.
194                            let state = cx.entity();
195                            window.defer(cx, move |_, cx| {
196                                state.update(cx, |_, cx| cx.notify());
197                            });
198                        }
199                    })
200                }
201            })
202            .child(ResizePanelGroupElement {
203                state: state.clone(),
204                axis: self.axis,
205                on_resize: self.on_resize.clone(),
206            })
207    }
208}
209
210/// A resizable panel inside a [`ResizablePanelGroup`].
211///
212/// Implements [`Styled`], so call sites can override the panel's
213/// rendered styles. User overrides are applied **between** the panel's
214/// flex defaults and its size management — the caller can override the
215/// internal `flex_grow: 1` (e.g. via `.flex_none()`) and add their own
216/// padding / colors / borders, while the panel's runtime size
217/// constraints (`min_w`/`max_w`/`flex_basis` driven by `ResizableState`)
218/// always win.
219///
220/// A common override is `.flex_none()`: the panel sets `flex_grow: 1`
221/// internally, so a sized panel that should hold its width when a
222/// sibling collapses needs to opt out of growth via `.flex_none()`.
223///
224/// ```ignore
225/// h_resizable("layout")
226///     .child(resizable_panel().size(px(220.)).flex_none().child(sidebar))
227///     .child(resizable_panel().child(content))                // flex
228///     .child(resizable_panel().size(px(280.)).flex_none().child(metadata))
229/// ```
230///
231/// **Reserved styles**: do not call these from outside — they fight the
232/// panel's own layout management:
233/// - `.flex_basis(...)` — driven by `ResizableState`, not by the caller.
234/// - `.absolute()` — would remove the panel from the resizable's flex flow.
235/// - `.overflow_hidden()` — may clip the resize handle, which is positioned
236///   absolute at `left: -4px` of each panel after the first.
237#[derive(IntoElement)]
238pub struct ResizablePanel {
239    axis: Axis,
240    panel_ix: usize,
241    state: Option<Entity<ResizableState>>,
242    /// Initial size is the size that the panel has when it is created.
243    initial_size: Option<Pixels>,
244    /// size range limit of this panel.
245    size_range: Range<Pixels>,
246    children: Vec<AnyElement>,
247    visible: bool,
248    style: StyleRefinement,
249    handle_appearance: Option<ResizeHandleRenderer>,
250}
251
252impl ResizablePanel {
253    /// Create a new resizable panel.
254    pub(super) fn new() -> Self {
255        Self {
256            panel_ix: 0,
257            initial_size: None,
258            state: None,
259            size_range: (PANEL_MIN_SIZE..Pixels::MAX),
260            axis: Axis::Horizontal,
261            children: vec![],
262            visible: true,
263            style: StyleRefinement::default(),
264            handle_appearance: None,
265        }
266    }
267
268    /// Set the visibility of the panel, default is true.
269    pub fn visible(mut self, visible: bool) -> Self {
270        self.visible = visible;
271        self
272    }
273
274    /// Set the initial size of the panel.
275    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
276        self.initial_size = Some(size.into());
277        self
278    }
279
280    /// Set the size range to limit panel resize.
281    ///
282    /// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
283    pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
284        self.size_range = range.into();
285        self
286    }
287}
288
289impl Styled for ResizablePanel {
290    fn style(&mut self) -> &mut StyleRefinement {
291        &mut self.style
292    }
293}
294
295impl ParentElement for ResizablePanel {
296    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
297        self.children.extend(elements);
298    }
299}
300
301impl RenderOnce for ResizablePanel {
302    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
303        if !self.visible {
304            return div().id(("resizable-panel", self.panel_ix));
305        }
306
307        let state = self
308            .state
309            .expect("BUG: The `state` in ResizablePanel should be present.");
310        let panel_state = state
311            .read(cx)
312            .panels
313            .get(self.panel_ix)
314            .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
315        let size_range = self.size_range.clone();
316
317        div()
318            .id(("resizable-panel", self.panel_ix))
319            .flex()
320            .flex_grow_1()
321            .size_full()
322            .relative()
323            // Apply caller style overrides here — between the flex defaults
324            // above and the size management below. This lets callers cancel
325            // the unconditional `.flex_grow_1()` (via `.flex_none()`, the load-
326            // bearing case for sized panels next to a collapsing sibling) and
327            // add their own padding / colors / borders, while keeping the
328            // panel's runtime size constraints (min/max + `flex_basis` driven
329            // by `ResizableState`) authoritative.
330            .refine_style(&self.style)
331            .when(self.axis.is_vertical(), |this| {
332                this.min_h(size_range.start).max_h(size_range.end)
333            })
334            .when(self.axis.is_horizontal(), |this| {
335                this.min_w(size_range.start).max_w(size_range.end)
336            })
337            // 1. initial_size is None, to use auto size.
338            // 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
339            // 3. initial_size is Some and size is Some, use `size`.
340            .when(self.initial_size.is_none(), |this| this.flex_shrink_1())
341            .when_some(self.initial_size, |this, initial_size| {
342                // The `self.size` is None, that mean the initial size for the panel,
343                // so we need set `flex_shrink_0` To let it keep the initial size.
344                this.when(
345                    panel_state.size.is_none() && !initial_size.is_zero(),
346                    |this| this.flex_none(),
347                )
348                .flex_basis(initial_size)
349            })
350            .map(|this| match panel_state.size {
351                Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
352                None => this,
353            })
354            .on_prepaint({
355                let state = state.clone();
356                move |bounds, _, cx| {
357                    state.update(cx, |state, cx| {
358                        state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
359                    })
360                }
361            })
362            .children(self.children)
363            .when(self.panel_ix > 0, |this| {
364                let ix = self.panel_ix - 1;
365                this.child(
366                    resize_handle(("resizable-handle", ix), self.axis)
367                        .when_some(self.handle_appearance.clone(), |handle, appearance| {
368                            handle.with_appearance(appearance)
369                        })
370                        .on_drag(DragPanel, move |drag_panel, _, _, cx| {
371                            cx.stop_propagation();
372                            // Set current resizing panel ix
373                            state.update(cx, |state, _| {
374                                state.resizing_panel_ix = Some(ix);
375                            });
376                            cx.new(|_| drag_panel.deref().clone())
377                        }),
378                )
379            })
380    }
381}
382
383struct ResizePanelGroupElement {
384    state: Entity<ResizableState>,
385    on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
386    axis: Axis,
387}
388
389impl IntoElement for ResizePanelGroupElement {
390    type Element = Self;
391
392    fn into_element(self) -> Self::Element {
393        self
394    }
395}
396
397impl Element for ResizePanelGroupElement {
398    type RequestLayoutState = ();
399    type PrepaintState = ();
400
401    fn id(&self) -> Option<gpui::ElementId> {
402        None
403    }
404
405    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
406        None
407    }
408
409    fn request_layout(
410        &mut self,
411        _: Option<&gpui::GlobalElementId>,
412        _: Option<&gpui::InspectorElementId>,
413        window: &mut Window,
414        cx: &mut App,
415    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
416        (window.request_layout(Style::default(), None, cx), ())
417    }
418
419    fn prepaint(
420        &mut self,
421        _: Option<&gpui::GlobalElementId>,
422        _: Option<&gpui::InspectorElementId>,
423        _: Bounds<Pixels>,
424        _: &mut Self::RequestLayoutState,
425        _window: &mut Window,
426        _cx: &mut App,
427    ) -> Self::PrepaintState {
428        ()
429    }
430
431    fn paint(
432        &mut self,
433        _: Option<&gpui::GlobalElementId>,
434        _: Option<&gpui::InspectorElementId>,
435        _: Bounds<Pixels>,
436        _: &mut Self::RequestLayoutState,
437        _: &mut Self::PrepaintState,
438        window: &mut Window,
439        cx: &mut App,
440    ) {
441        window.on_mouse_event({
442            let state = self.state.clone();
443            let axis = self.axis;
444            let current_ix = state.read(cx).resizing_panel_ix;
445            move |e: &MouseMoveEvent, phase, window, cx| {
446                if !phase.bubble() {
447                    return;
448                }
449                let Some(ix) = current_ix else { return };
450
451                state.update(cx, |state, cx| {
452                    let panel = state.panels.get(ix).expect("BUG: invalid panel index");
453
454                    match axis {
455                        Axis::Horizontal => state.resize_panel_at_handle(
456                            ix,
457                            e.position.x - panel.bounds.left(),
458                            window,
459                            cx,
460                        ),
461                        Axis::Vertical => state.resize_panel_at_handle(
462                            ix,
463                            e.position.y - panel.bounds.top(),
464                            window,
465                            cx,
466                        ),
467                    }
468                    cx.notify();
469                })
470            }
471        });
472
473        // When any mouse up, stop dragging
474        window.on_mouse_event({
475            let state = self.state.clone();
476            let current_ix = state.read(cx).resizing_panel_ix;
477            let on_resize = self.on_resize.clone();
478            move |_: &MouseUpEvent, phase, window, cx| {
479                if current_ix.is_none() {
480                    return;
481                }
482                if phase.bubble() {
483                    state.update(cx, |state, cx| state.done_resizing(cx));
484                    on_resize(&state, window, cx);
485                }
486            }
487        })
488    }
489}