1use std::{any::Any, sync::Arc};
20
21use gpui::{
22 AnyElement, AnyView, App, Context, Entity, FocusHandle, Hsla, IntoElement, SharedString,
23 WeakEntity, Window,
24};
25use gpui_base::dock::{PanelId, PanelState, TabGroup};
26use rust_i18n::t;
27
28use crate::{button::Button, menu::PopupMenu};
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum PanelStyle {
32 #[default]
34 Auto,
35 TabBar,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct TitleStyle {
41 pub background: Hsla,
42 pub foreground: Hsla,
43}
44
45#[derive(Clone, Copy, Default)]
46pub enum PanelControl {
47 Both,
48 #[default]
49 Menu,
50 Toolbar,
51}
52
53impl PanelControl {
54 #[inline]
55 pub fn toolbar_visible(&self) -> bool {
56 matches!(self, PanelControl::Both | PanelControl::Toolbar)
57 }
58
59 #[inline]
60 pub fn menu_visible(&self) -> bool {
61 matches!(self, PanelControl::Both | PanelControl::Menu)
62 }
63}
64
65#[allow(unused_variables)]
71pub trait Panel: gpui_base::dock::Panel {
72 fn tab_name(&self, cx: &App) -> Option<SharedString> {
77 None
78 }
79
80 fn title(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
83 t!("Dock.Unnamed")
84 }
85
86 fn title_style(&self, cx: &App) -> Option<TitleStyle> {
88 None
89 }
90
91 fn title_suffix(
93 &mut self,
94 window: &mut Window,
95 cx: &mut Context<Self>,
96 ) -> Option<impl IntoElement> {
97 None::<gpui::Div>
98 }
99
100 fn toolbar_buttons(
102 &mut self,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) -> Option<Vec<Button>> {
106 None
107 }
108
109 fn dropdown_menu(
111 &mut self,
112 menu: PopupMenu,
113 window: &mut Window,
114 cx: &mut Context<Self>,
115 ) -> PopupMenu {
116 menu
117 }
118
119 fn zoom_control(&self, cx: &App) -> Option<PanelControl> {
132 Some(PanelControl::Menu)
133 }
134
135 fn inner_padding(&self, cx: &App) -> bool {
138 true
139 }
140}
141
142pub trait PanelView: gpui_base::dock::PanelView {
145 fn tab_name(&self, cx: &App) -> Option<SharedString>;
146 fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement;
147 fn title_style(&self, cx: &App) -> Option<TitleStyle>;
148 fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement>;
149 fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option<Vec<Button>>;
150 fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu;
151 fn zoom_control(&self, cx: &App) -> Option<PanelControl>;
152 fn inner_padding(&self, cx: &App) -> bool;
153}
154
155impl<T: Panel> PanelView for Entity<T> {
156 fn tab_name(&self, cx: &App) -> Option<SharedString> {
157 self.read(cx).tab_name(cx)
158 }
159
160 fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement {
161 self.update(cx, |this, cx| this.title(window, cx).into_any_element())
162 }
163
164 fn title_style(&self, cx: &App) -> Option<TitleStyle> {
165 self.read(cx).title_style(cx)
166 }
167
168 fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
169 self.update(cx, |this, cx| {
170 this.title_suffix(window, cx)
171 .map(|element| element.into_any_element())
172 })
173 }
174
175 fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option<Vec<Button>> {
176 self.update(cx, |this, cx| this.toolbar_buttons(window, cx))
177 }
178
179 fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu {
180 self.update(cx, |this, cx| this.dropdown_menu(menu, window, cx))
181 }
182
183 fn zoom_control(&self, cx: &App) -> Option<PanelControl> {
184 self.read(cx).zoom_control(cx)
185 }
186
187 fn inner_padding(&self, cx: &App) -> bool {
188 self.read(cx).inner_padding(cx)
189 }
190}
191
192#[derive(Clone)]
201pub struct PanelHandle(Arc<dyn PanelView>);
202
203impl PanelHandle {
204 pub fn new<P: Panel>(panel: Entity<P>) -> Self {
205 Self(Arc::new(panel))
206 }
207
208 pub fn from_view(panel: Arc<dyn PanelView>) -> Self {
210 Self(panel)
211 }
212
213 pub fn of(panel: &Arc<dyn gpui_base::dock::PanelView>) -> Option<&Self> {
220 panel.as_any().downcast_ref::<Self>()
221 }
222
223 pub fn panel(&self) -> Arc<dyn PanelView> {
230 self.0.clone()
231 }
232}
233
234impl std::ops::Deref for PanelHandle {
238 type Target = dyn PanelView;
239
240 fn deref(&self) -> &Self::Target {
241 &*self.0
242 }
243}
244
245impl gpui_base::dock::PanelView for PanelHandle {
246 fn panel_name(&self, cx: &App) -> &'static str {
247 self.0.panel_name(cx)
248 }
249
250 fn panel_id(&self, cx: &App) -> PanelId {
251 self.0.panel_id(cx)
252 }
253
254 fn closable(&self, cx: &App) -> bool {
255 self.0.closable(cx)
256 }
257
258 fn zoomable(&self, cx: &App) -> bool {
259 self.0.zoomable(cx)
260 }
261
262 fn visible(&self, cx: &App) -> bool {
263 self.0.visible(cx)
264 }
265
266 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
267 self.0.set_active(active, window, cx);
268 }
269
270 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
271 self.0.set_zoomed(zoomed, window, cx);
272 }
273
274 fn on_added_to(&self, group: WeakEntity<TabGroup>, window: &mut Window, cx: &mut App) {
275 self.0.on_added_to(group, window, cx);
276 }
277
278 fn on_removed(&self, window: &mut Window, cx: &mut App) {
279 self.0.on_removed(window, cx);
280 }
281
282 fn view(&self) -> AnyView {
283 self.0.view()
284 }
285
286 fn focus_handle(&self, cx: &App) -> FocusHandle {
287 self.0.focus_handle(cx)
288 }
289
290 fn dump(&self, cx: &App) -> PanelState {
291 self.0.dump(cx)
292 }
293
294 fn as_any(&self) -> &dyn Any {
295 self
296 }
297}
298
299pub fn panel_handle<P: Panel>(panel: Entity<P>) -> Arc<dyn gpui_base::dock::PanelView> {
314 Arc::new(PanelHandle::new(panel))
315}
316
317#[cfg(test)]
318mod tests {
319 use std::{cell::RefCell, rc::Rc};
320
321 use gpui::{
322 AppContext as _, Bounds, Div, Empty, EventEmitter, Focusable, InteractiveElement as _,
323 ParentElement as _, Render, Stateful, Styled as _, TestAppContext, div, point, px, size,
324 };
325 use gpui_base::dock::{
326 DockArea, DockAreaRenderer, DockLayout, PanelEvent, TabGroupContext, TabGroupRenderer,
327 TileContext, TilesRenderer,
328 };
329
330 use super::*;
331
332 struct Probe {
333 focus_handle: FocusHandle,
334 tab_name: SharedString,
335 }
336
337 impl Probe {
338 fn new(tab_name: &str, cx: &mut App) -> Entity<Self> {
339 let tab_name = SharedString::from(tab_name.to_string());
340 cx.new(|cx| Self {
341 focus_handle: cx.focus_handle(),
342 tab_name,
343 })
344 }
345 }
346
347 impl gpui_base::dock::Panel for Probe {
348 fn panel_name(&self) -> &'static str {
349 "Probe"
350 }
351 }
352
353 impl Panel for Probe {
354 fn tab_name(&self, _: &App) -> Option<SharedString> {
355 Some(self.tab_name.clone())
356 }
357 }
358
359 impl EventEmitter<PanelEvent> for Probe {}
360
361 impl Focusable for Probe {
362 fn focus_handle(&self, _: &App) -> FocusHandle {
363 self.focus_handle.clone()
364 }
365 }
366
367 impl Render for Probe {
368 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
369 Empty
370 }
371 }
372
373 type DeferredRead = Box<dyn Fn(&mut Window, &mut App) -> Option<SharedString>>;
375
376 #[derive(Default)]
377 struct Recovered {
378 tab_names: Vec<Option<SharedString>>,
381 drag_bar_names: Vec<Option<SharedString>>,
383 deferred: Option<DeferredRead>,
386 }
387
388 struct Skin {
391 recovered: Rc<RefCell<Recovered>>,
392 }
393
394 impl TabGroupRenderer for Skin {
395 fn render_tab_bar(
396 &self,
397 group: &TabGroupContext,
398 window: &mut Window,
399 cx: &mut App,
400 ) -> AnyElement {
401 let mut recovered = self.recovered.borrow_mut();
402 let mut tabs = Vec::new();
403 for panel in group.panels() {
404 let Some(handle) = PanelHandle::of(panel) else {
405 recovered.tab_names.push(None);
406 continue;
407 };
408 recovered.tab_names.push(handle.tab_name(cx));
409
410 tabs.push(handle.title(window, cx));
414
415 let panel = handle.panel();
419 recovered.deferred = Some(Box::new(move |window, cx| {
420 let _ = panel.title(window, cx);
421 panel.tab_name(cx)
422 }));
423 }
424
425 div().children(tabs).into_any_element()
426 }
427 }
428
429 impl TilesRenderer for Skin {
430 fn render_drag_bar(
431 &self,
432 tile: &TileContext,
433 window: &mut Window,
434 cx: &mut App,
435 ) -> AnyElement {
436 let handle = PanelHandle::of(tile.panel());
437 self.recovered
438 .borrow_mut()
439 .drag_bar_names
440 .push(handle.and_then(|handle| handle.tab_name(cx)));
441
442 match handle {
443 Some(handle) => div().child(handle.title(window, cx)).into_any_element(),
444 None => Empty.into_any_element(),
445 }
446 }
447 }
448
449 impl DockAreaRenderer for Skin {
450 fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
451 div().id("skin-dock-area").size_full()
452 }
453
454 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
455 Rc::new(Skin {
456 recovered: self.recovered.clone(),
457 })
458 }
459
460 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
461 Rc::new(Skin {
462 recovered: self.recovered.clone(),
463 })
464 }
465 }
466
467 #[gpui::test]
475 fn the_tab_bar_recovers_presentation_from_a_base_panel_handle(cx: &mut TestAppContext) {
476 cx.update(|cx| {
477 let _ = gpui_base::Theme::global_mut(cx);
478 });
479 let recovered = Rc::new(RefCell::new(Recovered::default()));
480 let skin = Rc::new(Skin {
481 recovered: recovered.clone(),
482 });
483
484 let (area, cx) = cx.add_window_view(|window, cx| {
485 DockArea::new("seam", None, window, cx).with_renderer(skin)
486 });
487
488 cx.update(|window, cx| {
489 let panel = PanelHandle::new(Probe::new("Probe Tab", cx));
490 let layout = DockLayout::tabs().panel_view(Arc::new(panel), cx);
491 area.update(cx, |area, cx| area.set_center(layout, window, cx));
492 });
493 cx.run_until_parked();
494 recovered.borrow_mut().tab_names.clear();
495 cx.update(|window, cx| window.draw(cx).clear(cx));
496
497 assert_eq!(
498 recovered.borrow().tab_names,
499 vec![Some(SharedString::from("Probe Tab"))],
500 "the skin read its own panel trait off base's handle"
501 );
502
503 let deferred = recovered.borrow_mut().deferred.take().expect("kept");
506 let later = cx.update(|window, cx| deferred(window, cx));
507 assert_eq!(later, Some(SharedString::from("Probe Tab")));
508 }
509
510 #[gpui::test]
515 fn a_panel_rebuilt_from_persisted_state_is_recoverable(cx: &mut TestAppContext) {
516 cx.update(|cx| {
517 let _ = gpui_base::Theme::global_mut(cx);
518 });
519 let recovered = Rc::new(RefCell::new(Recovered::default()));
520 let skin = Rc::new(Skin {
521 recovered: recovered.clone(),
522 });
523
524 let (area, cx) = cx.add_window_view(|window, cx| {
525 DockArea::new("seam", None, window, cx).with_renderer(skin)
526 });
527
528 cx.update(|window, cx| {
529 gpui_base::dock::register_panel(cx, "Probe", |_, _, cx| {
530 Arc::new(PanelHandle::new(Probe::new("Restored Tab", cx)))
531 as Arc<dyn gpui_base::dock::PanelView>
532 });
533 let panel = PanelHandle::new(Probe::new("Probe Tab", cx));
534 let layout = DockLayout::tabs().panel_view(Arc::new(panel), cx);
535 area.update(cx, |area, cx| area.set_center(layout, window, cx));
536 });
537 cx.run_until_parked();
538
539 let state = cx.read(|cx| area.read(cx).dump(cx));
540 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
541 cx.run_until_parked();
542 recovered.borrow_mut().tab_names.clear();
543 cx.update(|window, cx| window.draw(cx).clear(cx));
544
545 assert_eq!(
546 recovered.borrow().tab_names,
547 vec![Some(SharedString::from("Restored Tab"))],
548 "the rebuilt panel reached the tab bar as a handle, not a bare entity"
549 );
550 }
551
552 #[gpui::test]
559 fn a_tile_drag_bar_reaches_the_same_presentation(cx: &mut TestAppContext) {
560 cx.update(|cx| {
561 let _ = gpui_base::Theme::global_mut(cx);
562 });
563 let recovered = Rc::new(RefCell::new(Recovered::default()));
564 let skin = Rc::new(Skin {
565 recovered: recovered.clone(),
566 });
567
568 let (area, cx) = cx.add_window_view(|window, cx| {
569 DockArea::new("seam", None, window, cx).with_renderer(skin)
570 });
571
572 cx.update(|window, cx| {
573 let panel = PanelHandle::new(Probe::new("Tile Tab", cx));
574 let bounds = Bounds {
575 origin: point(px(0.), px(0.)),
576 size: size(px(200.), px(150.)),
577 };
578 let layout = DockLayout::tiles().tile_view(Arc::new(panel), bounds, cx);
579 area.update(cx, |area, cx| area.set_center(layout, window, cx));
580 });
581 cx.run_until_parked();
582 recovered.borrow_mut().drag_bar_names.clear();
583 cx.update(|window, cx| window.draw(cx).clear(cx));
584
585 assert_eq!(
586 recovered.borrow().drag_bar_names,
587 vec![Some(SharedString::from("Tile Tab"))],
588 "the drag bar can draw a title and a menu off the panel"
589 );
590 }
591
592 #[gpui::test]
596 fn a_panel_base_was_handed_directly_is_not_recoverable(cx: &mut TestAppContext) {
597 cx.update(|cx| {
598 let _ = gpui_base::Theme::global_mut(cx);
599 });
600 let recovered = Rc::new(RefCell::new(Recovered::default()));
601 let skin = Rc::new(Skin {
602 recovered: recovered.clone(),
603 });
604
605 let (area, cx) = cx.add_window_view(|window, cx| {
606 DockArea::new("seam", None, window, cx).with_renderer(skin)
607 });
608
609 cx.update(|window, cx| {
610 let layout = DockLayout::tabs().panel(Probe::new("Probe Tab", cx));
611 area.update(cx, |area, cx| area.set_center(layout, window, cx));
612 });
613 cx.run_until_parked();
614 recovered.borrow_mut().tab_names.clear();
615 cx.update(|window, cx| window.draw(cx).clear(cx));
616
617 assert_eq!(
618 recovered.borrow().tab_names,
619 vec![None],
620 "an unwrapped panel is reported as unrecoverable, not wrongly recovered"
621 );
622 }
623}