gpui_component/resizable/
panel.rs

1use std::{
2    ops::{Deref, Range},
3    rc::Rc,
4};
5
6use gpui::{
7    canvas, div, prelude::FluentBuilder, Along, AnyElement, App, AppContext, Axis, Bounds, Context,
8    Element, ElementId, Empty, Entity, EventEmitter, InteractiveElement as _, IntoElement,
9    IsZero as _, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style,
10    Styled, Window,
11};
12
13use crate::{h_flex, resizable::PANEL_MIN_SIZE, v_flex, AxisExt};
14
15use super::{resizable_panel, resize_handle, ResizableState};
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}
39
40impl ResizablePanelGroup {
41    /// Create a new resizable panel group.
42    pub fn new(id: impl Into<ElementId>) -> Self {
43        Self {
44            id: id.into(),
45            axis: Axis::Horizontal,
46            children: vec![],
47            state: None,
48            size: None,
49            on_resize: Rc::new(|_, _, _| {}),
50        }
51    }
52
53    /// Bind yourself to a resizable state entity.
54    ///
55    /// If not provided, it will handle its own state internally.
56    pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
57        self.state = Some(state.clone());
58        self
59    }
60
61    /// Set the axis of the resizable panel group, default is horizontal.
62    pub fn axis(mut self, axis: Axis) -> Self {
63        self.axis = axis;
64        self
65    }
66
67    /// Add a panel to the group.
68    ///
69    /// - The `axis` will be set to the same axis as the group.
70    /// - The `initial_size` will be set to the average size of all panels if not provided.
71    /// - The `group` will be set to the group entity.
72    pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
73        self.children.push(panel.into());
74        self
75    }
76
77    /// Add multiple panels to the group.
78    pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
79    where
80        I: Into<ResizablePanel>,
81    {
82        self.children = panels.into_iter().map(|panel| panel.into()).collect();
83        self
84    }
85
86    /// Set size of the resizable panel group
87    ///
88    /// - When the axis is horizontal, the size is the height of the group.
89    /// - When the axis is vertical, the size is the width of the group.
90    pub fn size(mut self, size: Pixels) -> Self {
91        self.size = Some(size);
92        self
93    }
94
95    /// Set the callback to be called when the panels are resized.
96    ///
97    /// ## Callback arguments
98    ///
99    /// - Entity<ResizableState>: The state of the ResizablePanelGroup.
100    pub fn on_resize(
101        mut self,
102        on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
103    ) -> Self {
104        self.on_resize = Rc::new(on_resize);
105        self
106    }
107}
108
109impl<T> From<T> for ResizablePanel
110where
111    T: Into<AnyElement>,
112{
113    fn from(value: T) -> Self {
114        resizable_panel().child(value.into())
115    }
116}
117
118impl From<ResizablePanelGroup> for ResizablePanel {
119    fn from(value: ResizablePanelGroup) -> Self {
120        resizable_panel().child(value)
121    }
122}
123
124impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
125
126impl RenderOnce for ResizablePanelGroup {
127    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
128        let state = self.state.unwrap_or(
129            window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
130        );
131        let container = if self.axis.is_horizontal() {
132            h_flex()
133        } else {
134            v_flex()
135        };
136
137        // Sync panels to the state
138        let panels_count = self.children.len();
139        state.update(cx, |state, cx| {
140            state.sync_panels_count(self.axis, panels_count, cx);
141        });
142
143        container
144            .id(self.id)
145            .size_full()
146            .children(
147                self.children
148                    .into_iter()
149                    .enumerate()
150                    .map(|(ix, mut panel)| {
151                        panel.panel_ix = ix;
152                        panel.axis = self.axis;
153                        panel.state = Some(state.clone());
154                        panel
155                    }),
156            )
157            .child({
158                canvas(
159                    {
160                        let state = state.clone();
161                        move |bounds, _, cx| {
162                            state.update(cx, |state, cx| {
163                                let size_changed = state.bounds.size.along(self.axis)
164                                    != 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                    |_, _, _, _| {},
175                )
176                .absolute()
177                .size_full()
178            })
179            .child(ResizePanelGroupElement {
180                state: state.clone(),
181                axis: self.axis,
182                on_resize: self.on_resize.clone(),
183            })
184    }
185}
186
187/// A resizable panel inside a [`ResizablePanelGroup`].
188#[derive(IntoElement)]
189pub struct ResizablePanel {
190    axis: Axis,
191    panel_ix: usize,
192    state: Option<Entity<ResizableState>>,
193    /// Initial size is the size that the panel has when it is created.
194    initial_size: Option<Pixels>,
195    /// size range limit of this panel.
196    size_range: Range<Pixels>,
197    children: Vec<AnyElement>,
198    visible: bool,
199}
200
201impl ResizablePanel {
202    /// Create a new resizable panel.
203    pub(super) fn new() -> Self {
204        Self {
205            panel_ix: 0,
206            initial_size: None,
207            state: None,
208            size_range: (PANEL_MIN_SIZE..Pixels::MAX),
209            axis: Axis::Horizontal,
210            children: vec![],
211            visible: true,
212        }
213    }
214
215    /// Set the visibility of the panel, default is true.
216    pub fn visible(mut self, visible: bool) -> Self {
217        self.visible = visible;
218        self
219    }
220
221    /// Set the initial size of the panel.
222    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
223        self.initial_size = Some(size.into());
224        self
225    }
226
227    /// Set the size range to limit panel resize.
228    ///
229    /// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
230    pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
231        self.size_range = range.into();
232        self
233    }
234}
235
236impl ParentElement for ResizablePanel {
237    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
238        self.children.extend(elements);
239    }
240}
241
242impl RenderOnce for ResizablePanel {
243    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
244        if !self.visible {
245            return div().id(("resizable-panel", self.panel_ix));
246        }
247
248        let state = self
249            .state
250            .expect("BUG: The `state` in ResizablePanel should be present.");
251        let panel_state = state
252            .read(cx)
253            .panels
254            .get(self.panel_ix)
255            .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
256        let size_range = self.size_range.clone();
257
258        div()
259            .id(("resizable-panel", self.panel_ix))
260            .flex()
261            .flex_grow()
262            .size_full()
263            .relative()
264            .when(self.axis.is_vertical(), |this| {
265                this.min_h(size_range.start).max_h(size_range.end)
266            })
267            .when(self.axis.is_horizontal(), |this| {
268                this.min_w(size_range.start).max_w(size_range.end)
269            })
270            // 1. initial_size is None, to use auto size.
271            // 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
272            // 3. initial_size is Some and size is Some, use `size`.
273            .when(self.initial_size.is_none(), |this| this.flex_shrink())
274            .when_some(self.initial_size, |this, initial_size| {
275                // The `self.size` is None, that mean the initial size for the panel,
276                // so we need set `flex_shrink_0` To let it keep the initial size.
277                this.when(
278                    panel_state.size.is_none() && !initial_size.is_zero(),
279                    |this| this.flex_none(),
280                )
281                .flex_basis(initial_size)
282            })
283            .map(|this| match panel_state.size {
284                Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
285                None => this,
286            })
287            .child({
288                canvas(
289                    {
290                        let state = state.clone();
291                        move |bounds, _, cx| {
292                            state.update(cx, |state, cx| {
293                                state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
294                            })
295                        }
296                    },
297                    |_, _, _, _| {},
298                )
299                .absolute()
300                .size_full()
301            })
302            .children(self.children)
303            .when(self.panel_ix > 0, |this| {
304                let ix = self.panel_ix - 1;
305                this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag(
306                    DragPanel,
307                    move |drag_panel, _, _, cx| {
308                        cx.stop_propagation();
309                        // Set current resizing panel ix
310                        state.update(cx, |state, _| {
311                            state.resizing_panel_ix = Some(ix);
312                        });
313                        cx.new(|_| drag_panel.deref().clone())
314                    },
315                ))
316            })
317    }
318}
319
320struct ResizePanelGroupElement {
321    state: Entity<ResizableState>,
322    on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
323    axis: Axis,
324}
325
326impl IntoElement for ResizePanelGroupElement {
327    type Element = Self;
328
329    fn into_element(self) -> Self::Element {
330        self
331    }
332}
333
334impl Element for ResizePanelGroupElement {
335    type RequestLayoutState = ();
336    type PrepaintState = ();
337
338    fn id(&self) -> Option<gpui::ElementId> {
339        None
340    }
341
342    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
343        None
344    }
345
346    fn request_layout(
347        &mut self,
348        _: Option<&gpui::GlobalElementId>,
349        _: Option<&gpui::InspectorElementId>,
350        window: &mut Window,
351        cx: &mut App,
352    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
353        (window.request_layout(Style::default(), None, cx), ())
354    }
355
356    fn prepaint(
357        &mut self,
358        _: Option<&gpui::GlobalElementId>,
359        _: Option<&gpui::InspectorElementId>,
360        _: Bounds<Pixels>,
361        _: &mut Self::RequestLayoutState,
362        _window: &mut Window,
363        _cx: &mut App,
364    ) -> Self::PrepaintState {
365        ()
366    }
367
368    fn paint(
369        &mut self,
370        _: Option<&gpui::GlobalElementId>,
371        _: Option<&gpui::InspectorElementId>,
372        _: Bounds<Pixels>,
373        _: &mut Self::RequestLayoutState,
374        _: &mut Self::PrepaintState,
375        window: &mut Window,
376        cx: &mut App,
377    ) {
378        window.on_mouse_event({
379            let state = self.state.clone();
380            let axis = self.axis;
381            let current_ix = state.read(cx).resizing_panel_ix;
382            move |e: &MouseMoveEvent, phase, window, cx| {
383                if !phase.bubble() {
384                    return;
385                }
386                let Some(ix) = current_ix else { return };
387
388                state.update(cx, |state, cx| {
389                    let panel = state.panels.get(ix).expect("BUG: invalid panel index");
390
391                    match axis {
392                        Axis::Horizontal => {
393                            state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx)
394                        }
395                        Axis::Vertical => {
396                            state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx);
397                        }
398                    }
399                    cx.notify();
400                })
401            }
402        });
403
404        // When any mouse up, stop dragging
405        window.on_mouse_event({
406            let state = self.state.clone();
407            let current_ix = state.read(cx).resizing_panel_ix;
408            let on_resize = self.on_resize.clone();
409            move |_: &MouseUpEvent, phase, window, cx| {
410                if current_ix.is_none() {
411                    return;
412                }
413                if phase.bubble() {
414                    state.update(cx, |state, cx| state.done_resizing(cx));
415                    on_resize(&state, window, cx);
416                }
417            }
418        })
419    }
420}