gpui_component/resizable/
panel.rs

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