ui/components/resizable/
group.rs1use std::rc::Rc;
6
7use gpui::{AnyElement, Bounds, Context, DragMoveEvent, Empty, Render, canvas};
8
9use super::{ResizableHandle, ResizablePanel};
10use crate::prelude::*;
11
12#[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
23pub 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#[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}