1use std::{cell::Cell, rc::Rc};
2
3use gpui::{
4 AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
5 IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
6 StatefulInteractiveElement, Styled as _, Window, div, prelude::FluentBuilder as _, px,
7};
8
9use crate::{AxisExt as _, Side, theme::ActiveTheme as _};
10
11pub(crate) const HANDLE_PADDING: Pixels = px(4.);
12pub(crate) const HANDLE_SIZE: Pixels = px(1.);
13
14#[doc(hidden)]
16pub fn resize_handle<T: 'static, E: 'static + Render>(
17 id: impl Into<ElementId>,
18 axis: Axis,
19) -> ResizeHandle<T, E> {
20 ResizeHandle::new(id, axis)
21}
22
23pub type ResizeHandleRenderer =
28 Rc<dyn Fn(&ResizeHandleContext, &mut Window, &mut App) -> Option<AnyElement>>;
29
30pub struct ResizeHandleContext {
35 axis: Axis,
36 active: bool,
37}
38
39impl ResizeHandleContext {
40 pub fn axis(&self) -> Axis {
43 self.axis
44 }
45
46 pub fn is_active(&self) -> bool {
48 self.active
49 }
50}
51
52#[doc(hidden)]
53pub struct ResizeHandle<T: 'static, E: 'static + Render> {
54 id: ElementId,
55 axis: Axis,
56 drag_value: Option<Rc<T>>,
57 placement: Option<Side>,
58 on_drag: Option<Rc<dyn Fn(&Point<Pixels>, &mut Window, &mut App) -> Entity<E>>>,
59 appearance: Option<ResizeHandleRenderer>,
60}
61
62impl<T: 'static, E: 'static + Render> ResizeHandle<T, E> {
63 fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
64 let id = id.into();
65 Self {
66 id: id.clone(),
67 on_drag: None,
68 drag_value: None,
69 placement: None,
70 appearance: None,
71 axis,
72 }
73 }
74
75 pub fn with_appearance(mut self, appearance: ResizeHandleRenderer) -> Self {
77 self.appearance = Some(appearance);
78 self
79 }
80
81 pub fn on_drag(
82 mut self,
83 value: T,
84 f: impl Fn(Rc<T>, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static,
85 ) -> Self {
86 let value = Rc::new(value);
87 self.drag_value = Some(value.clone());
88 self.on_drag = Some(Rc::new(move |p, window, cx| {
89 f(value.clone(), p, window, cx)
90 }));
91 self
92 }
93
94 pub fn placement(mut self, placement: Side) -> Self {
95 self.placement = Some(placement);
96 self
97 }
98}
99
100#[derive(Default, Debug, Clone)]
101struct ResizeHandleState {
102 active: Cell<bool>,
103}
104
105impl ResizeHandleState {
106 fn set_active(&self, active: bool) {
107 self.active.set(active);
108 }
109
110 fn is_active(&self) -> bool {
111 self.active.get()
112 }
113}
114
115impl<T: 'static, E: 'static + Render> IntoElement for ResizeHandle<T, E> {
116 type Element = ResizeHandle<T, E>;
117 fn into_element(self) -> Self::Element {
118 self
119 }
120}
121
122impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
123 type RequestLayoutState = AnyElement;
124 type PrepaintState = ();
125
126 fn id(&self) -> Option<ElementId> {
127 Some(self.id.clone())
128 }
129
130 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
131 None
132 }
133
134 fn request_layout(
135 &mut self,
136 id: Option<&GlobalElementId>,
137 _: Option<&gpui::InspectorElementId>,
138 window: &mut Window,
139 cx: &mut App,
140 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
141 let neg_offset = -HANDLE_PADDING;
142 let axis = self.axis;
143
144 window.with_element_state(id.unwrap(), |state, window| {
145 let state = state.unwrap_or(ResizeHandleState::default());
146
147 let bg_color = handle_color(&cx.theme(), state.is_active());
148
149 let mut el = div()
150 .id(self.id.clone())
151 .occlude()
152 .absolute()
153 .flex_shrink_0()
154 .group("handle")
155 .when_some(self.on_drag.clone(), |this, on_drag| {
156 this.on_drag(
157 self.drag_value.clone().unwrap(),
158 move |_, position, window, cx| on_drag(&position, window, cx),
159 )
160 })
161 .map(|this| match self.placement {
162 Some(Side::Left) => {
163 this.cursor_col_resize()
166 .top_0()
167 .right(px(1.))
168 .h_full()
169 .w(HANDLE_SIZE)
170 .pl(HANDLE_PADDING)
171 }
172 _ => this
173 .when(axis.is_horizontal(), |this| {
174 this.cursor_col_resize()
175 .top_0()
176 .left(neg_offset)
177 .h_full()
178 .w(HANDLE_SIZE)
179 .px(HANDLE_PADDING)
180 })
181 .when(axis.is_vertical(), |this| {
182 this.cursor_row_resize()
183 .top(neg_offset)
184 .left_0()
185 .w_full()
186 .h(HANDLE_SIZE)
187 .py(HANDLE_PADDING)
188 }),
189 })
190 .child(
191 self.appearance
195 .as_ref()
196 .and_then(|appearance| {
197 appearance(
198 &ResizeHandleContext {
199 axis,
200 active: state.is_active(),
201 },
202 window,
203 cx,
204 )
205 })
206 .unwrap_or_else(|| {
207 div()
208 .flex_none()
212 .bg(bg_color)
213 .group_hover("handle", |this| this.bg(bg_color))
214 .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
215 .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE))
216 .into_any_element()
217 }),
218 )
219 .into_any_element();
220
221 let layout_id = el.request_layout(window, cx);
222
223 ((layout_id, el), state)
224 })
225 }
226
227 fn prepaint(
228 &mut self,
229 _: Option<&GlobalElementId>,
230 _: Option<&gpui::InspectorElementId>,
231 _: gpui::Bounds<Pixels>,
232 request_layout: &mut Self::RequestLayoutState,
233 window: &mut Window,
234 cx: &mut App,
235 ) -> Self::PrepaintState {
236 request_layout.prepaint(window, cx);
237 }
238
239 fn paint(
240 &mut self,
241 id: Option<&GlobalElementId>,
242 _: Option<&gpui::InspectorElementId>,
243 bounds: gpui::Bounds<Pixels>,
244 request_layout: &mut Self::RequestLayoutState,
245 _: &mut Self::PrepaintState,
246 window: &mut Window,
247 cx: &mut App,
248 ) {
249 request_layout.paint(window, cx);
250
251 window.with_element_state(id.unwrap(), |state: Option<ResizeHandleState>, window| {
252 let state = state.unwrap_or(ResizeHandleState::default());
253
254 window.on_mouse_event({
255 let state = state.clone();
256 move |ev: &MouseDownEvent, phase, window, _| {
257 if bounds.contains(&ev.position) && phase.bubble() {
258 state.set_active(true);
259 window.refresh();
260 }
261 }
262 });
263
264 window.on_mouse_event({
265 let state = state.clone();
266 move |_: &MouseUpEvent, _, window, _| {
267 if state.is_active() {
268 state.set_active(false);
269 window.refresh();
270 }
271 }
272 });
273
274 ((), state)
275 });
276 }
277}
278
279pub(crate) fn handle_color(theme: &crate::Theme, active: bool) -> gpui::Hsla {
287 if active {
288 theme
289 .resizable
290 .active_handle
291 .unwrap_or(theme.tokens.colors.ring)
292 } else {
293 theme.resizable.handle.unwrap_or(theme.tokens.colors.border)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use gpui::{TestAppContext, hsla};
300
301 use super::handle_color;
302 use crate::{ResizableTheme, Theme};
303
304 #[gpui::test]
305 fn an_unprojected_handle_resolves_from_the_theme_tokens(cx: &mut TestAppContext) {
306 cx.update(|cx| {
307 let border = hsla(0., 0., 0.5, 1.0);
308 let ring = hsla(0.6, 0.5, 0.5, 1.0);
309 let theme = Theme::global_mut(cx);
310 theme.tokens.colors.border = border;
311 theme.tokens.colors.ring = ring;
312 theme.resizable = ResizableTheme::default();
313
314 let theme = Theme::global(cx);
315 assert_eq!(handle_color(&theme, false), border);
316 assert_eq!(handle_color(&theme, true), ring);
317 assert_ne!(handle_color(&theme, false), gpui::Hsla::default());
320 });
321 }
322
323 #[gpui::test]
324 fn a_projected_handle_still_wins(cx: &mut TestAppContext) {
325 cx.update(|cx| {
326 let projected = hsla(0.3, 0.4, 0.5, 1.0);
327 let active = hsla(0.9, 0.4, 0.5, 1.0);
328 let theme = Theme::global_mut(cx);
329 theme.tokens.colors.border = hsla(0., 0., 0.5, 1.0);
330 theme.resizable = ResizableTheme {
331 handle: Some(projected),
332 active_handle: Some(active),
333 };
334
335 let theme = Theme::global(cx);
336 assert_eq!(handle_color(&theme, false), projected);
337 assert_eq!(handle_color(&theme, true), active);
338 });
339 }
340}