Skip to main content

ui/components/resizable/
group.rs

1//! Standalone two-panel [`ResizablePanelGroup`] built on [`super::ResizablePanel`]/
2//! [`super::ResizableHandle`] — unrelated to [`crate::PaneGroup`]'s recursive
3//! tree, kept for existing simple left/right split use cases.
4
5use std::rc::Rc;
6
7use gpui::{AnyElement, Bounds, Context, DragMoveEvent, Empty, Render, canvas};
8
9use super::{ResizableHandle, ResizablePanel};
10use crate::prelude::*;
11
12/// Drag payload for [`ResizableHandle`].
13#[derive(Debug)]
14struct ResizableDrag {
15    start_fraction: f32,
16    start_x: Pixels,
17}
18
19fn clamp_fraction(fraction: f32, min: f32, max: f32) -> f32 {
20    fraction.clamp(min, max)
21}
22
23/// Horizontal panel group with a draggable split handle.
24///
25/// Create with `cx.new(|_| ResizablePanelGroup::new(left, right))` where
26/// `left`/`right` are render callbacks returning panel content each frame.
27pub struct ResizablePanelGroup {
28    left: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
29    right: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
30    left_fraction: f32,
31    min_left_fraction: f32,
32    max_left_fraction: f32,
33    container_bounds: Rc<std::cell::Cell<Option<Bounds<Pixels>>>>,
34}
35
36impl ResizablePanelGroup {
37    pub fn new(
38        left: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
39        right: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
40    ) -> Self {
41        Self {
42            left: Rc::new(left),
43            right: Rc::new(right),
44            left_fraction: 0.5,
45            min_left_fraction: 0.2,
46            max_left_fraction: 0.8,
47            container_bounds: Rc::new(std::cell::Cell::new(None)),
48        }
49    }
50
51    pub fn left_fraction(mut self, fraction: f32) -> Self {
52        self.left_fraction =
53            clamp_fraction(fraction, self.min_left_fraction, self.max_left_fraction);
54        self
55    }
56
57    pub fn min_left_fraction(mut self, min: f32) -> Self {
58        self.min_left_fraction = min.clamp(0.05, 0.95);
59        self
60    }
61
62    pub fn max_left_fraction(mut self, max: f32) -> Self {
63        self.max_left_fraction = max.clamp(self.min_left_fraction, 0.95);
64        self
65    }
66
67    pub fn left_fraction_value(&self) -> f32 {
68        self.left_fraction
69    }
70
71    fn on_drag_move(&mut self, event: &DragMoveEvent<ResizableDrag>, cx: &mut Context<Self>) {
72        let drag = event.drag(cx);
73        let container_width = self
74            .container_bounds
75            .get()
76            .map(|b| b.size.width)
77            .unwrap_or(px(1.));
78        if container_width <= px(0.) {
79            return;
80        }
81        let delta = event.event.position.x - drag.start_x;
82        let delta_fraction = delta / container_width;
83        self.left_fraction = clamp_fraction(
84            drag.start_fraction + delta_fraction,
85            self.min_left_fraction,
86            self.max_left_fraction,
87        );
88        cx.notify();
89    }
90}
91
92impl Render for ResizablePanelGroup {
93    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
94        let left_fraction = self.left_fraction;
95        let right_fraction = 1.0 - left_fraction;
96        let left = (self.left)(window, cx);
97        let right = (self.right)(window, cx);
98        let container_bounds = self.container_bounds.clone();
99        let mouse_x = window.mouse_position().x;
100
101        let handle = ResizableHandle::new("resizable-handle-hit")
102            .on_drag(
103                ResizableDrag {
104                    start_fraction: left_fraction,
105                    start_x: mouse_x,
106                },
107                |_, _, _, cx| cx.new(|_| Empty),
108            )
109            .on_drag_move::<ResizableDrag>(cx.listener(|this, event, _, cx| {
110                this.on_drag_move(event, cx);
111            }));
112
113        h_flex()
114            .id("resizable-panel-group")
115            .w_full()
116            .h(px(240.))
117            .rounded_md()
118            .border_1()
119            .border_color(semantic::border(cx))
120            .overflow_hidden()
121            .child({
122                let measure = container_bounds.clone();
123                canvas(
124                    move |bounds, _, _| measure.set(Some(bounds)),
125                    |_, _, _, _| {},
126                )
127                .absolute()
128                .size_full()
129            })
130            .child(
131                ResizablePanel::new()
132                    .fraction(left_fraction)
133                    .child(div().h_full().p_4().child(left)),
134            )
135            .child(handle)
136            .child(
137                ResizablePanel::new()
138                    .fraction(right_fraction)
139                    .child(div().h_full().p_4().child(right)),
140            )
141    }
142}
143
144/// Gallery catalog entry for [`ResizablePanelGroup`].
145#[derive(IntoElement, RegisterComponent)]
146pub struct ResizablePreview;
147
148impl RenderOnce for ResizablePreview {
149    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
150        cx.new(|_| {
151            ResizablePanelGroup::new(
152                |_, _| {
153                    v_flex()
154                        .gap_2()
155                        .child(Label::new("Left panel").weight(gpui::FontWeight::SEMIBOLD))
156                        .child(Label::new("Drag the handle to resize.").color(Color::Muted))
157                        .into_any_element()
158                },
159                |_, _| {
160                    v_flex()
161                        .gap_2()
162                        .child(Label::new("Right panel").weight(gpui::FontWeight::SEMIBOLD))
163                        .child(Label::new("Clamped between 20% and 80%.").color(Color::Muted))
164                        .into_any_element()
165                },
166            )
167            .min_left_fraction(0.2)
168            .max_left_fraction(0.8)
169        })
170    }
171}
172
173impl Component for ResizablePreview {
174    fn scope() -> ComponentScope {
175        ComponentScope::Layout
176    }
177
178    fn description() -> Option<&'static str> {
179        Some("Horizontal split panels with a draggable divider and min/max clamping.")
180    }
181
182    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
183        ResizablePreview
184            .render(window, cx)
185            .into_any_element()
186            .into()
187    }
188}