1use std::rc::Rc;
7
8use gpui::{AnyElement, Bounds, Context, DragMoveEvent, Empty, Render, canvas};
9use smallvec::SmallVec;
10
11use crate::prelude::*;
12
13#[derive(Debug)]
15struct ResizableDrag {
16 start_fraction: f32,
17 start_x: Pixels,
18}
19
20fn clamp_fraction(fraction: f32, min: f32, max: f32) -> f32 {
21 fraction.clamp(min, max)
22}
23
24#[derive(IntoElement)]
26pub struct ResizablePanel {
27 children: SmallVec<[AnyElement; 2]>,
28 fraction: f32,
29}
30
31impl ResizablePanel {
32 pub fn new() -> Self {
33 Self {
34 children: SmallVec::new(),
35 fraction: 1.0,
36 }
37 }
38
39 pub(crate) fn fraction(mut self, fraction: f32) -> Self {
40 self.fraction = fraction;
41 self
42 }
43}
44
45impl Default for ResizablePanel {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl ParentElement for ResizablePanel {
52 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
53 self.children.extend(elements);
54 }
55}
56
57impl RenderOnce for ResizablePanel {
58 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
59 div()
60 .h_full()
61 .overflow_hidden()
62 .when(self.fraction >= 1.0, |this| this.flex_grow())
63 .when(self.fraction < 1.0, |this| {
64 this.flex_shrink_0().w(relative(self.fraction))
65 })
66 .bg(semantic::surface(cx))
67 .children(self.children)
68 }
69}
70
71#[derive(IntoElement)]
73pub struct ResizableHandle;
74
75impl RenderOnce for ResizableHandle {
76 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
77 div()
78 .id("resizable-handle")
79 .w(px(1.))
80 .h_full()
81 .bg(semantic::border(cx))
82 }
83}
84
85pub struct ResizablePanelGroup {
90 left: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
91 right: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
92 left_fraction: f32,
93 min_left_fraction: f32,
94 max_left_fraction: f32,
95 container_bounds: Rc<std::cell::Cell<Option<Bounds<Pixels>>>>,
96}
97
98impl ResizablePanelGroup {
99 pub fn new(
100 left: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
101 right: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
102 ) -> Self {
103 Self {
104 left: Rc::new(left),
105 right: Rc::new(right),
106 left_fraction: 0.5,
107 min_left_fraction: 0.2,
108 max_left_fraction: 0.8,
109 container_bounds: Rc::new(std::cell::Cell::new(None)),
110 }
111 }
112
113 pub fn left_fraction(mut self, fraction: f32) -> Self {
114 self.left_fraction =
115 clamp_fraction(fraction, self.min_left_fraction, self.max_left_fraction);
116 self
117 }
118
119 pub fn min_left_fraction(mut self, min: f32) -> Self {
120 self.min_left_fraction = min.clamp(0.05, 0.95);
121 self
122 }
123
124 pub fn max_left_fraction(mut self, max: f32) -> Self {
125 self.max_left_fraction = max.clamp(self.min_left_fraction, 0.95);
126 self
127 }
128
129 pub fn left_fraction_value(&self) -> f32 {
130 self.left_fraction
131 }
132
133 fn on_drag_move(&mut self, event: &DragMoveEvent<ResizableDrag>, cx: &mut Context<Self>) {
134 let drag = event.drag(cx);
135 let container_width = self
136 .container_bounds
137 .get()
138 .map(|b| b.size.width)
139 .unwrap_or(px(1.));
140 if container_width <= px(0.) {
141 return;
142 }
143 let delta = event.event.position.x - drag.start_x;
144 let delta_fraction = delta / container_width;
145 self.left_fraction = clamp_fraction(
146 drag.start_fraction + delta_fraction,
147 self.min_left_fraction,
148 self.max_left_fraction,
149 );
150 cx.notify();
151 }
152}
153
154impl Render for ResizablePanelGroup {
155 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
156 let left_fraction = self.left_fraction;
157 let right_fraction = 1.0 - left_fraction;
158 let left = (self.left)(window, cx);
159 let right = (self.right)(window, cx);
160 let container_bounds = self.container_bounds.clone();
161 let mouse_x = window.mouse_position().x;
162
163 let handle = div()
164 .id("resizable-handle-hit")
165 .w(px(8.))
166 .h_full()
167 .flex_shrink_0()
168 .cursor_col_resize()
169 .flex()
170 .items_center()
171 .justify_center()
172 .child(ResizableHandle)
173 .on_drag(
174 ResizableDrag {
175 start_fraction: left_fraction,
176 start_x: mouse_x,
177 },
178 |_, _, _, cx| cx.new(|_| Empty),
179 )
180 .on_drag_move::<ResizableDrag>(cx.listener(|this, event, _, cx| {
181 this.on_drag_move(event, cx);
182 }));
183
184 h_flex()
185 .id("resizable-panel-group")
186 .w_full()
187 .h(px(240.))
188 .rounded_md()
189 .border_1()
190 .border_color(semantic::border(cx))
191 .overflow_hidden()
192 .child({
193 let measure = container_bounds.clone();
194 canvas(
195 move |bounds, _, _| measure.set(Some(bounds)),
196 |_, _, _, _| {},
197 )
198 .absolute()
199 .size_full()
200 })
201 .child(
202 ResizablePanel::new()
203 .fraction(left_fraction)
204 .child(div().h_full().p_4().child(left)),
205 )
206 .child(handle)
207 .child(
208 ResizablePanel::new()
209 .fraction(right_fraction)
210 .child(div().h_full().p_4().child(right)),
211 )
212 }
213}
214
215#[derive(IntoElement, RegisterComponent)]
217pub struct ResizablePreview;
218
219impl RenderOnce for ResizablePreview {
220 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
221 cx.new(|_| {
222 ResizablePanelGroup::new(
223 |_, _| {
224 v_flex()
225 .gap_2()
226 .child(Label::new("Left panel").weight(gpui::FontWeight::SEMIBOLD))
227 .child(Label::new("Drag the handle to resize.").color(Color::Muted))
228 .into_any_element()
229 },
230 |_, _| {
231 v_flex()
232 .gap_2()
233 .child(Label::new("Right panel").weight(gpui::FontWeight::SEMIBOLD))
234 .child(Label::new("Clamped between 20% and 80%.").color(Color::Muted))
235 .into_any_element()
236 },
237 )
238 .min_left_fraction(0.2)
239 .max_left_fraction(0.8)
240 })
241 }
242}
243
244impl Component for ResizablePreview {
245 fn scope() -> ComponentScope {
246 ComponentScope::Layout
247 }
248
249 fn description() -> Option<&'static str> {
250 Some("Horizontal split panels with a draggable divider and min/max clamping.")
251 }
252
253 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
254 ResizablePreview
255 .render(window, cx)
256 .into_any_element()
257 .into()
258 }
259}