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