gpui_component/resizable/
panel.rs

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