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