Skip to main content

gpui_base/resizable/
panel.rs

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