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 fn title_bar(&self, cx: &App) -> bool {
147 true
148 }
149}
150
151pub trait PanelView: gpui_base::dock::PanelView {
154 fn tab_name(&self, cx: &App) -> Option<SharedString>;
155 fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement;
156 fn title_style(&self, cx: &App) -> Option<TitleStyle>;
157 fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement>;
158 fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option<Vec<Button>>;
159 fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu;
160 fn zoom_control(&self, cx: &App) -> Option<PanelControl>;
161 fn inner_padding(&self, cx: &App) -> bool;
162 fn title_bar(&self, cx: &App) -> bool;
163}
164
165impl<T: Panel> PanelView for Entity<T> {
166 fn tab_name(&self, cx: &App) -> Option<SharedString> {
167 self.read(cx).tab_name(cx)
168 }
169
170 fn title(&self, window: &mut Window, cx: &mut App) -> AnyElement {
171 self.update(cx, |this, cx| this.title(window, cx).into_any_element())
172 }
173
174 fn title_style(&self, cx: &App) -> Option<TitleStyle> {
175 self.read(cx).title_style(cx)
176 }
177
178 fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
179 self.update(cx, |this, cx| {
180 this.title_suffix(window, cx)
181 .map(|element| element.into_any_element())
182 })
183 }
184
185 fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option<Vec<Button>> {
186 self.update(cx, |this, cx| this.toolbar_buttons(window, cx))
187 }
188
189 fn dropdown_menu(&self, menu: PopupMenu, window: &mut Window, cx: &mut App) -> PopupMenu {
190 self.update(cx, |this, cx| this.dropdown_menu(menu, window, cx))
191 }
192
193 fn zoom_control(&self, cx: &App) -> Option<PanelControl> {
194 self.read(cx).zoom_control(cx)
195 }
196
197 fn inner_padding(&self, cx: &App) -> bool {
198 self.read(cx).inner_padding(cx)
199 }
200
201 fn title_bar(&self, cx: &App) -> bool {
202 self.read(cx).title_bar(cx)
203 }
204}
205
206#[derive(Clone)]
215pub struct PanelHandle(Arc<dyn PanelView>);
216
217impl PanelHandle {
218 pub fn new<P: Panel>(panel: Entity<P>) -> Self {
219 Self(Arc::new(panel))
220 }
221
222 pub fn from_view(panel: Arc<dyn PanelView>) -> Self {
224 Self(panel)
225 }
226
227 pub fn of(panel: &Arc<dyn gpui_base::dock::PanelView>) -> Option<&Self> {
234 panel.as_any().downcast_ref::<Self>()
235 }
236
237 pub fn panel(&self) -> Arc<dyn PanelView> {
244 self.0.clone()
245 }
246}
247
248impl std::ops::Deref for PanelHandle {
252 type Target = dyn PanelView;
253
254 fn deref(&self) -> &Self::Target {
255 &*self.0
256 }
257}
258
259impl gpui_base::dock::PanelView for PanelHandle {
260 fn panel_name(&self, cx: &App) -> &'static str {
261 self.0.panel_name(cx)
262 }
263
264 fn panel_id(&self, cx: &App) -> PanelId {
265 self.0.panel_id(cx)
266 }
267
268 fn closable(&self, cx: &App) -> bool {
269 self.0.closable(cx)
270 }
271
272 fn zoomable(&self, cx: &App) -> bool {
273 self.0.zoomable(cx)
274 }
275
276 fn visible(&self, cx: &App) -> bool {
277 self.0.visible(cx)
278 }
279
280 fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
281 self.0.set_active(active, window, cx);
282 }
283
284 fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
285 self.0.set_zoomed(zoomed, window, cx);
286 }
287
288 fn on_added_to(&self, group: WeakEntity<TabGroup>, window: &mut Window, cx: &mut App) {
289 self.0.on_added_to(group, window, cx);
290 }
291
292 fn on_removed(&self, window: &mut Window, cx: &mut App) {
293 self.0.on_removed(window, cx);
294 }
295
296 fn view(&self) -> AnyView {
297 self.0.view()
298 }
299
300 fn focus_handle(&self, cx: &App) -> FocusHandle {
301 self.0.focus_handle(cx)
302 }
303
304 fn dump(&self, cx: &App) -> PanelState {
305 self.0.dump(cx)
306 }
307
308 fn as_any(&self) -> &dyn Any {
309 self
310 }
311}
312
313pub fn panel_handle<P: Panel>(panel: Entity<P>) -> Arc<dyn gpui_base::dock::PanelView> {
326 Arc::new(PanelHandle::new(panel))
327}
328
329#[cfg(test)]
330mod tests {
331 use std::{cell::RefCell, rc::Rc};
332
333 use gpui::{
334 AppContext as _, Div, Empty, EventEmitter, Focusable, InteractiveElement as _,
335 ParentElement as _, Render, Stateful, Styled as _, TestAppContext, div,
336 };
337 use gpui_base::dock::{
338 DockArea, DockAreaRenderer, DockLayout, PanelEvent, TabGroupContext, TabGroupRenderer,
339 };
340
341 use super::*;
342
343 struct Probe {
344 focus_handle: FocusHandle,
345 tab_name: SharedString,
346 }
347
348 impl Probe {
349 fn new(tab_name: &str, cx: &mut App) -> Entity<Self> {
350 let tab_name = SharedString::from(tab_name.to_string());
351 cx.new(|cx| Self {
352 focus_handle: cx.focus_handle(),
353 tab_name,
354 })
355 }
356 }
357
358 impl gpui_base::dock::Panel for Probe {
359 fn panel_name(&self) -> &'static str {
360 "Probe"
361 }
362 }
363
364 impl Panel for Probe {
365 fn tab_name(&self, _: &App) -> Option<SharedString> {
366 Some(self.tab_name.clone())
367 }
368 }
369
370 impl EventEmitter<PanelEvent> for Probe {}
371
372 impl Focusable for Probe {
373 fn focus_handle(&self, _: &App) -> FocusHandle {
374 self.focus_handle.clone()
375 }
376 }
377
378 impl Render for Probe {
379 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
380 Empty
381 }
382 }
383
384 type DeferredRead = Box<dyn Fn(&mut Window, &mut App) -> Option<SharedString>>;
386
387 #[derive(Default)]
388 struct Recovered {
389 tab_names: Vec<Option<SharedString>>,
392 deferred: Option<DeferredRead>,
395 }
396
397 struct Skin {
400 recovered: Rc<RefCell<Recovered>>,
401 }
402
403 impl TabGroupRenderer for Skin {
404 fn render_tab_bar(
405 &self,
406 group: &TabGroupContext,
407 window: &mut Window,
408 cx: &mut App,
409 ) -> AnyElement {
410 let mut recovered = self.recovered.borrow_mut();
411 let mut tabs = Vec::new();
412 for panel in group.panels() {
413 let Some(handle) = PanelHandle::of(panel) else {
414 recovered.tab_names.push(None);
415 continue;
416 };
417 recovered.tab_names.push(handle.tab_name(cx));
418
419 tabs.push(handle.title(window, cx));
423
424 let panel = handle.panel();
428 recovered.deferred = Some(Box::new(move |window, cx| {
429 let _ = panel.title(window, cx);
430 panel.tab_name(cx)
431 }));
432 }
433
434 div().children(tabs).into_any_element()
435 }
436 }
437
438 impl DockAreaRenderer for Skin {
439 fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
440 div().id("skin-dock-area").size_full()
441 }
442
443 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
444 Rc::new(Skin {
445 recovered: self.recovered.clone(),
446 })
447 }
448 }
449
450 #[gpui::test]
458 fn the_tab_bar_recovers_presentation_from_a_base_panel_handle(cx: &mut TestAppContext) {
459 cx.update(|cx| {
460 let _ = gpui_base::Theme::global_mut(cx);
461 });
462 let recovered = Rc::new(RefCell::new(Recovered::default()));
463 let skin = Rc::new(Skin {
464 recovered: recovered.clone(),
465 });
466
467 let (area, cx) = cx.add_window_view(|window, cx| {
468 DockArea::new("seam", None, window, cx).with_renderer(skin)
469 });
470
471 cx.update(|window, cx| {
472 let panel = PanelHandle::new(Probe::new("Probe Tab", cx));
473 let layout = DockLayout::tabs().panel_view(Arc::new(panel), cx);
474 area.update(cx, |area, cx| area.set_center(layout, window, cx));
475 });
476 cx.run_until_parked();
477 recovered.borrow_mut().tab_names.clear();
478 cx.update(|window, cx| window.draw(cx).clear(cx));
479
480 assert_eq!(
481 recovered.borrow().tab_names,
482 vec![Some(SharedString::from("Probe Tab"))],
483 "the skin read its own panel trait off base's handle"
484 );
485
486 let deferred = recovered.borrow_mut().deferred.take().expect("kept");
489 let later = cx.update(|window, cx| deferred(window, cx));
490 assert_eq!(later, Some(SharedString::from("Probe Tab")));
491 }
492
493 #[gpui::test]
498 fn a_panel_rebuilt_from_persisted_state_is_recoverable(cx: &mut TestAppContext) {
499 cx.update(|cx| {
500 let _ = gpui_base::Theme::global_mut(cx);
501 });
502 let recovered = Rc::new(RefCell::new(Recovered::default()));
503 let skin = Rc::new(Skin {
504 recovered: recovered.clone(),
505 });
506
507 let (area, cx) = cx.add_window_view(|window, cx| {
508 DockArea::new("seam", None, window, cx).with_renderer(skin)
509 });
510
511 cx.update(|window, cx| {
512 gpui_base::dock::register_panel(cx, "Probe", |_, _, cx| {
513 Arc::new(PanelHandle::new(Probe::new("Restored Tab", cx)))
514 as Arc<dyn gpui_base::dock::PanelView>
515 });
516 let panel = PanelHandle::new(Probe::new("Probe Tab", cx));
517 let layout = DockLayout::tabs().panel_view(Arc::new(panel), cx);
518 area.update(cx, |area, cx| area.set_center(layout, window, cx));
519 });
520 cx.run_until_parked();
521
522 let state = cx.read(|cx| area.read(cx).dump(cx));
523 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
524 cx.run_until_parked();
525 recovered.borrow_mut().tab_names.clear();
526 cx.update(|window, cx| window.draw(cx).clear(cx));
527
528 assert_eq!(
529 recovered.borrow().tab_names,
530 vec![Some(SharedString::from("Restored Tab"))],
531 "the rebuilt panel reached the tab bar as a handle, not a bare entity"
532 );
533 }
534
535 #[gpui::test]
539 fn a_panel_base_was_handed_directly_is_not_recoverable(cx: &mut TestAppContext) {
540 cx.update(|cx| {
541 let _ = gpui_base::Theme::global_mut(cx);
542 });
543 let recovered = Rc::new(RefCell::new(Recovered::default()));
544 let skin = Rc::new(Skin {
545 recovered: recovered.clone(),
546 });
547
548 let (area, cx) = cx.add_window_view(|window, cx| {
549 DockArea::new("seam", None, window, cx).with_renderer(skin)
550 });
551
552 cx.update(|window, cx| {
553 let layout = DockLayout::tabs().panel(Probe::new("Probe Tab", cx));
554 area.update(cx, |area, cx| area.set_center(layout, window, cx));
555 });
556 cx.run_until_parked();
557 recovered.borrow_mut().tab_names.clear();
558 cx.update(|window, cx| window.draw(cx).clear(cx));
559
560 assert_eq!(
561 recovered.borrow().tab_names,
562 vec![None],
563 "an unwrapped panel is reported as unrecoverable, not wrongly recovered"
564 );
565 }
566}