1use std::{ops::Deref as _, rc::Rc, sync::Arc};
5
6use gpui::{
7 AnyElement, App, AppContext as _, Axis, Context, Div, Element, Empty, InteractiveElement as _,
8 IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Stateful, Style,
9 Styled as _, Window, div, prelude::FluentBuilder as _,
10};
11use gpui_base::dock::{
12 DockAreaRenderer, DockContext, DockEvent, DockPlacement, NodeId, PanelState, PanelView,
13 TabGroupRenderer,
14};
15
16use crate::{
17 ActiveTheme as _, Side,
18 dock::{
19 DockSkin, SkinShared, invalid_panel::InvalidPanel, panel_handle, tab_panel::TabGroupSkin,
20 },
21 resize_handle,
22};
23
24#[derive(Clone)]
27struct ResizePanel;
28
29impl Render for ResizePanel {
30 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
31 Empty
32 }
33}
34
35impl DockAreaRenderer for DockSkin {
36 fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
40 div().id("dock-area")
41 }
42
43 fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
44 div().id("dock-area-center")
45 }
46
47 fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful<Div> {
48 div()
51 .id(("dock-split-frame", node.as_u64()))
52 .bg(cx.theme().tokens.tab_bar)
53 }
54
55 fn render_dock(
56 &self,
57 dock: &DockContext,
58 content: AnyElement,
59 window: &mut Window,
60 cx: &mut App,
61 ) -> AnyElement {
62 div()
67 .flex()
68 .size_full()
69 .relative()
70 .child(content)
71 .child(self.render_resize_handle(dock, window, cx))
72 .child(DockResizeTracker {
73 dock: dock.clone(),
74 shared: self.shared().clone(),
75 })
76 .into_any_element()
77 }
78
79 fn build_placeholder(
84 &self,
85 state: &PanelState,
86 _: &mut Window,
87 cx: &mut App,
88 ) -> Option<Arc<dyn PanelView>> {
89 let state = state.clone();
90 Some(panel_handle(cx.new(|cx| {
91 InvalidPanel::new(state.panel_name.clone(), state, cx)
92 })))
93 }
94
95 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
96 Rc::new(TabGroupSkin::new(self.shared().clone()))
97 }
98}
99
100impl DockSkin {
101 fn render_resize_handle(
102 &self,
103 dock: &DockContext,
104 _: &mut Window,
105 _: &mut App,
106 ) -> impl IntoElement {
107 let placement = dock.placement();
108 let shared = self.shared().clone();
109
110 let id = match placement {
115 DockPlacement::Left => "resize-handle-left",
116 DockPlacement::Right => "resize-handle-right",
117 DockPlacement::Bottom => "resize-handle-bottom",
118 DockPlacement::Center => "resize-handle-center",
119 };
120
121 resize_handle(id, placement.axis())
122 .when(placement.is_left(), |this| this.placement(Side::Left))
123 .on_drag(ResizePanel, move |info, _, _, cx| {
124 cx.stop_propagation();
125 shared.resizing_dock().set(Some(placement));
126 cx.new(|_| info.deref().clone())
127 })
128 }
129}
130
131struct DockResizeTracker {
140 dock: DockContext,
141 shared: Rc<SkinShared>,
142}
143
144impl IntoElement for DockResizeTracker {
145 type Element = Self;
146
147 fn into_element(self) -> Self::Element {
148 self
149 }
150}
151
152impl Element for DockResizeTracker {
153 type RequestLayoutState = ();
154 type PrepaintState = ();
155
156 fn id(&self) -> Option<gpui::ElementId> {
157 None
158 }
159
160 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
161 None
162 }
163
164 fn request_layout(
165 &mut self,
166 _: Option<&gpui::GlobalElementId>,
167 _: Option<&gpui::InspectorElementId>,
168 window: &mut Window,
169 cx: &mut App,
170 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
171 (window.request_layout(Style::default(), None, cx), ())
172 }
173
174 fn prepaint(
175 &mut self,
176 _: Option<&gpui::GlobalElementId>,
177 _: Option<&gpui::InspectorElementId>,
178 _: gpui::Bounds<Pixels>,
179 _: &mut Self::RequestLayoutState,
180 _: &mut Window,
181 _: &mut App,
182 ) -> Self::PrepaintState {
183 }
184
185 fn paint(
186 &mut self,
187 _: Option<&gpui::GlobalElementId>,
188 _: Option<&gpui::InspectorElementId>,
189 _: gpui::Bounds<Pixels>,
190 _: &mut Self::RequestLayoutState,
191 _: &mut Self::PrepaintState,
192 window: &mut Window,
193 _: &mut App,
194 ) {
195 let placement = self.dock.placement();
196
197 window.on_mouse_event({
198 let dock = self.dock.clone();
199 let shared = self.shared.clone();
200 move |event: &MouseMoveEvent, phase, window, cx| {
201 if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
202 return;
203 }
204 let open = shared
209 .area()
210 .upgrade()
211 .is_some_and(|area| area.read(cx).is_dock_open(placement));
212 if !open {
213 dock.toggle(window, cx);
214 }
215 dock.resize_to(event.position, window, cx);
216 }
217 });
218
219 window.on_mouse_event({
220 let shared = self.shared.clone();
221 move |_: &MouseUpEvent, phase, _, cx| {
222 if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
223 return;
224 }
225 shared.resizing_dock().set(None);
226 _ = shared
229 .area()
230 .update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged));
231 }
232 });
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use std::rc::Rc;
239
240 use gpui::{
241 App, Entity, IntoElement as _, Modifiers, MouseButton, TestAppContext, VisualTestContext,
242 Window, point, px, size,
243 };
244
245 use std::cell::Cell;
246
247 use gpui_base::dock::DockAreaRenderer;
248
249 use crate::dock::{
250 DockArea, DockLayout, DockPlacement, DockSkin,
251 test_support::{MeasuredProbe, SizedProbe},
252 };
253
254 struct ChromelessDockSkin;
260
261 impl DockAreaRenderer for ChromelessDockSkin {
262 fn tab_group_renderer(&self) -> Rc<dyn gpui_base::dock::TabGroupRenderer> {
263 Rc::new(ChromelessTabs)
264 }
265 }
266
267 struct ChromelessTabs;
268 impl gpui_base::dock::TabGroupRenderer for ChromelessTabs {
269 fn render_tab_bar(
273 &self,
274 _: &gpui_base::dock::TabGroupContext,
275 _: &mut Window,
276 _: &mut App,
277 ) -> gpui::AnyElement {
278 gpui::Empty.into_any_element()
279 }
280 }
281
282 #[gpui::test]
291 fn a_dock_is_its_own_width_under_a_renderer_that_draws_no_chrome(cx: &mut TestAppContext) {
292 cx.update(|cx| crate::init(cx));
293 let measured = Rc::new(Cell::new(gpui::Size::default()));
294 let probe = measured.clone();
295 let centre = Rc::new(Cell::new(gpui::Size::default()));
296 let centre_probe = centre.clone();
297 let (area, cx) = cx.add_window_view(|window, cx| {
298 DockArea::new("test", None, window, cx).with_renderer(Rc::new(ChromelessDockSkin))
299 });
300 cx.simulate_resize(size(px(800.), px(600.)));
301 cx.update(|window, cx| {
302 area.update(cx, |area, cx| {
303 area.set_center(
304 DockLayout::tabs().panel(SizedProbe::new(centre_probe, cx)),
305 window,
306 cx,
307 );
308 area.set_dock(
309 DockPlacement::Right,
310 DockLayout::tabs().panel(SizedProbe::new(probe, cx)),
311 window,
312 cx,
313 );
314 area.set_dock_size(DockPlacement::Right, px(200.), window, cx);
315 });
316 });
317 cx.run_until_parked();
318 cx.update(|window, cx| {
319 area.update(cx, |area, cx| {
320 if !area.is_dock_open(DockPlacement::Right) {
321 area.toggle_dock(DockPlacement::Right, window, cx);
322 }
323 });
324 });
325 cx.run_until_parked();
326 cx.update(|window, cx| window.draw(cx).clear(cx));
327
328 let dock = measured.get().width;
329 assert_eq!(
330 dock,
331 px(200.),
332 "the right dock has to be its own width, not the area's: got {dock:?}"
333 );
334 let middle = centre.get().width;
338 assert_eq!(
339 middle,
340 px(600.),
341 "the centre has to be what the docks leave: got {middle:?}"
342 );
343 let tall = centre.get().height;
349 assert_eq!(
350 tall,
351 px(600.),
352 "a group has to fill its slot, or its panel gets no height: got {tall:?}"
353 );
354 }
355
356 fn area_with_side_docks(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
357 cx.update(|cx| crate::init(cx));
358 let (area, cx) = cx.add_window_view(|window, cx| {
359 DockArea::new("test", None, window, cx).with_renderer(DockSkin::new(cx))
360 });
361 cx.simulate_resize(size(px(800.), px(600.)));
362 cx.update(|window, cx| {
363 area.update(cx, |area, cx| {
364 area.set_center(
365 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
366 window,
367 cx,
368 );
369 area.set_dock(
370 DockPlacement::Left,
371 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
372 window,
373 cx,
374 );
375 area.set_dock(
376 DockPlacement::Right,
377 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
378 window,
379 cx,
380 );
381 });
382 });
383 cx.run_until_parked();
384 (area, cx)
385 }
386
387 #[gpui::test]
394 fn dragging_the_left_handle_resizes_only_the_left_dock(cx: &mut TestAppContext) {
395 let (area, cx) = area_with_side_docks(cx);
396 cx.update(|window, cx| window.draw(cx).clear(cx));
397
398 cx.simulate_mouse_down(
400 point(px(198.5), px(300.)),
401 MouseButton::Left,
402 Modifiers::none(),
403 );
404 cx.simulate_mouse_move(
406 point(px(204.), px(300.)),
407 MouseButton::Left,
408 Modifiers::none(),
409 );
410 cx.simulate_mouse_move(
412 point(px(240.), px(300.)),
413 MouseButton::Left,
414 Modifiers::none(),
415 );
416 cx.simulate_mouse_up(
417 point(px(240.), px(300.)),
418 MouseButton::Left,
419 Modifiers::none(),
420 );
421 cx.run_until_parked();
422
423 let (left, right) = cx.update(|_, cx| {
424 let area = area.read(cx);
425 (
426 area.dock_size(DockPlacement::Left),
427 area.dock_size(DockPlacement::Right),
428 )
429 });
430 assert_eq!(
431 right,
432 Some(px(200.)),
433 "the right dock must not move when the left handle is dragged"
434 );
435 assert_eq!(left, Some(px(240.)), "the left dock follows the pointer");
436 }
437}