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, TilesRenderer,
14};
15
16use crate::{
17 ActiveTheme as _, Side,
18 dock::{
19 DockSkin, SkinShared, invalid_panel::InvalidPanel, panel_handle, tab_panel::TabGroupSkin,
20 tiles::TilesSkin,
21 },
22 resize_handle,
23};
24
25#[derive(Clone)]
28struct ResizePanel;
29
30impl Render for ResizePanel {
31 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
32 Empty
33 }
34}
35
36impl DockAreaRenderer for DockSkin {
37 fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
41 div().id("dock-area")
42 }
43
44 fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
45 div().id("dock-area-center")
46 }
47
48 fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful<Div> {
49 div()
52 .id(("dock-split-frame", node.as_u64()))
53 .bg(cx.theme().tokens.tab_bar)
54 }
55
56 fn render_dock(
57 &self,
58 dock: &DockContext,
59 content: AnyElement,
60 window: &mut Window,
61 cx: &mut App,
62 ) -> AnyElement {
63 div()
68 .flex()
69 .size_full()
70 .relative()
71 .child(content)
72 .child(self.render_resize_handle(dock, window, cx))
73 .child(DockResizeTracker {
74 dock: dock.clone(),
75 shared: self.shared().clone(),
76 })
77 .into_any_element()
78 }
79
80 fn build_placeholder(
85 &self,
86 state: &PanelState,
87 _: &mut Window,
88 cx: &mut App,
89 ) -> Option<Arc<dyn PanelView>> {
90 let state = state.clone();
91 Some(panel_handle(cx.new(|cx| {
92 InvalidPanel::new(state.panel_name.clone(), state, cx)
93 })))
94 }
95
96 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
97 Rc::new(TabGroupSkin::new(self.shared().clone()))
98 }
99
100 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
101 Rc::new(TilesSkin::new(self.shared().clone()))
102 }
103}
104
105impl DockSkin {
106 fn render_resize_handle(
107 &self,
108 dock: &DockContext,
109 _: &mut Window,
110 _: &mut App,
111 ) -> impl IntoElement {
112 let placement = dock.placement();
113 let shared = self.shared().clone();
114
115 let id = match placement {
120 DockPlacement::Left => "resize-handle-left",
121 DockPlacement::Right => "resize-handle-right",
122 DockPlacement::Bottom => "resize-handle-bottom",
123 DockPlacement::Center => "resize-handle-center",
124 };
125
126 resize_handle(id, placement.axis())
127 .when(placement.is_left(), |this| this.placement(Side::Left))
128 .on_drag(ResizePanel, move |info, _, _, cx| {
129 cx.stop_propagation();
130 shared.resizing_dock().set(Some(placement));
131 cx.new(|_| info.deref().clone())
132 })
133 }
134}
135
136struct DockResizeTracker {
145 dock: DockContext,
146 shared: Rc<SkinShared>,
147}
148
149impl IntoElement for DockResizeTracker {
150 type Element = Self;
151
152 fn into_element(self) -> Self::Element {
153 self
154 }
155}
156
157impl Element for DockResizeTracker {
158 type RequestLayoutState = ();
159 type PrepaintState = ();
160
161 fn id(&self) -> Option<gpui::ElementId> {
162 None
163 }
164
165 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
166 None
167 }
168
169 fn request_layout(
170 &mut self,
171 _: Option<&gpui::GlobalElementId>,
172 _: Option<&gpui::InspectorElementId>,
173 window: &mut Window,
174 cx: &mut App,
175 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
176 (window.request_layout(Style::default(), None, cx), ())
177 }
178
179 fn prepaint(
180 &mut self,
181 _: Option<&gpui::GlobalElementId>,
182 _: Option<&gpui::InspectorElementId>,
183 _: gpui::Bounds<Pixels>,
184 _: &mut Self::RequestLayoutState,
185 _: &mut Window,
186 _: &mut App,
187 ) -> Self::PrepaintState {
188 }
189
190 fn paint(
191 &mut self,
192 _: Option<&gpui::GlobalElementId>,
193 _: Option<&gpui::InspectorElementId>,
194 _: gpui::Bounds<Pixels>,
195 _: &mut Self::RequestLayoutState,
196 _: &mut Self::PrepaintState,
197 window: &mut Window,
198 _: &mut App,
199 ) {
200 let placement = self.dock.placement();
201
202 window.on_mouse_event({
203 let dock = self.dock.clone();
204 let shared = self.shared.clone();
205 move |event: &MouseMoveEvent, phase, window, cx| {
206 if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
207 return;
208 }
209 let open = shared
214 .area()
215 .upgrade()
216 .is_some_and(|area| area.read(cx).is_dock_open(placement));
217 if !open {
218 dock.toggle(window, cx);
219 }
220 dock.resize_to(event.position, window, cx);
221 }
222 });
223
224 window.on_mouse_event({
225 let shared = self.shared.clone();
226 move |_: &MouseUpEvent, phase, _, cx| {
227 if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
228 return;
229 }
230 shared.resizing_dock().set(None);
231 _ = shared
234 .area()
235 .update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged));
236 }
237 });
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use std::rc::Rc;
244
245 use gpui::{
246 App, Entity, IntoElement as _, Modifiers, MouseButton, TestAppContext, VisualTestContext,
247 Window, point, px, size,
248 };
249
250 use std::cell::Cell;
251
252 use gpui_base::dock::DockAreaRenderer;
253
254 use crate::dock::{
255 DockArea, DockLayout, DockPlacement, DockSkin,
256 test_support::{MeasuredProbe, SizedProbe},
257 };
258
259 struct ChromelessDockSkin;
265
266 impl DockAreaRenderer for ChromelessDockSkin {
267 fn tab_group_renderer(&self) -> Rc<dyn gpui_base::dock::TabGroupRenderer> {
268 Rc::new(ChromelessTabs)
269 }
270
271 fn tiles_renderer(&self) -> Rc<dyn gpui_base::dock::TilesRenderer> {
272 Rc::new(ChromelessTiles)
273 }
274 }
275
276 struct ChromelessTabs;
277 impl gpui_base::dock::TabGroupRenderer for ChromelessTabs {
278 fn render_tab_bar(
282 &self,
283 _: &gpui_base::dock::TabGroupContext,
284 _: &mut Window,
285 _: &mut App,
286 ) -> gpui::AnyElement {
287 gpui::Empty.into_any_element()
288 }
289 }
290
291 struct ChromelessTiles;
292 impl gpui_base::dock::TilesRenderer for ChromelessTiles {
293 fn render_drag_bar(
294 &self,
295 _: &gpui_base::dock::TileContext,
296 _: &mut Window,
297 _: &mut App,
298 ) -> gpui::AnyElement {
299 gpui::Empty.into_any_element()
300 }
301 }
302
303 #[gpui::test]
312 fn a_dock_is_its_own_width_under_a_renderer_that_draws_no_chrome(cx: &mut TestAppContext) {
313 cx.update(|cx| crate::init(cx));
314 let measured = Rc::new(Cell::new(gpui::Size::default()));
315 let probe = measured.clone();
316 let centre = Rc::new(Cell::new(gpui::Size::default()));
317 let centre_probe = centre.clone();
318 let (area, cx) = cx.add_window_view(|window, cx| {
319 DockArea::new("test", None, window, cx).with_renderer(Rc::new(ChromelessDockSkin))
320 });
321 cx.simulate_resize(size(px(800.), px(600.)));
322 cx.update(|window, cx| {
323 area.update(cx, |area, cx| {
324 area.set_center(
325 DockLayout::tabs().panel(SizedProbe::new(centre_probe, cx)),
326 window,
327 cx,
328 );
329 area.set_dock(
330 DockPlacement::Right,
331 DockLayout::tabs().panel(SizedProbe::new(probe, cx)),
332 window,
333 cx,
334 );
335 area.set_dock_size(DockPlacement::Right, px(200.), window, cx);
336 });
337 });
338 cx.run_until_parked();
339 cx.update(|window, cx| {
340 area.update(cx, |area, cx| {
341 if !area.is_dock_open(DockPlacement::Right) {
342 area.toggle_dock(DockPlacement::Right, window, cx);
343 }
344 });
345 });
346 cx.run_until_parked();
347 cx.update(|window, cx| window.draw(cx).clear(cx));
348
349 let dock = measured.get().width;
350 assert_eq!(
351 dock,
352 px(200.),
353 "the right dock has to be its own width, not the area's: got {dock:?}"
354 );
355 let middle = centre.get().width;
359 assert_eq!(
360 middle,
361 px(600.),
362 "the centre has to be what the docks leave: got {middle:?}"
363 );
364 let tall = centre.get().height;
370 assert_eq!(
371 tall,
372 px(600.),
373 "a group has to fill its slot, or its panel gets no height: got {tall:?}"
374 );
375 }
376
377 fn area_with_side_docks(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
378 cx.update(|cx| crate::init(cx));
379 let (area, cx) = cx.add_window_view(|window, cx| {
380 DockArea::new("test", None, window, cx).with_renderer(DockSkin::new(cx))
381 });
382 cx.simulate_resize(size(px(800.), px(600.)));
383 cx.update(|window, cx| {
384 area.update(cx, |area, cx| {
385 area.set_center(
386 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
387 window,
388 cx,
389 );
390 area.set_dock(
391 DockPlacement::Left,
392 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
393 window,
394 cx,
395 );
396 area.set_dock(
397 DockPlacement::Right,
398 DockLayout::tabs().panel(MeasuredProbe::new(Rc::default(), cx)),
399 window,
400 cx,
401 );
402 });
403 });
404 cx.run_until_parked();
405 (area, cx)
406 }
407
408 #[gpui::test]
415 fn dragging_the_left_handle_resizes_only_the_left_dock(cx: &mut TestAppContext) {
416 let (area, cx) = area_with_side_docks(cx);
417 cx.update(|window, cx| window.draw(cx).clear(cx));
418
419 cx.simulate_mouse_down(
421 point(px(198.5), px(300.)),
422 MouseButton::Left,
423 Modifiers::none(),
424 );
425 cx.simulate_mouse_move(
427 point(px(204.), px(300.)),
428 MouseButton::Left,
429 Modifiers::none(),
430 );
431 cx.simulate_mouse_move(
433 point(px(240.), px(300.)),
434 MouseButton::Left,
435 Modifiers::none(),
436 );
437 cx.simulate_mouse_up(
438 point(px(240.), px(300.)),
439 MouseButton::Left,
440 Modifiers::none(),
441 );
442 cx.run_until_parked();
443
444 let (left, right) = cx.update(|_, cx| {
445 let area = area.read(cx);
446 (
447 area.dock_size(DockPlacement::Left),
448 area.dock_size(DockPlacement::Right),
449 )
450 });
451 assert_eq!(
452 right,
453 Some(px(200.)),
454 "the right dock must not move when the left handle is dragged"
455 );
456 assert_eq!(left, Some(px(240.)), "the left dock follows the pointer");
457 }
458}