dock.rs

  1use crate::persistence::model::DockData;
  2use crate::DraggedDock;
  3use crate::{status_bar::StatusItemView, Workspace};
  4use gpui::{
  5    deferred, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity,
  6    EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton,
  7    ParentElement, Render, SharedString, StyleRefinement, Styled, Subscription, View, ViewContext,
  8    VisualContext, WeakView, WindowContext,
  9};
 10use schemars::JsonSchema;
 11use serde::{Deserialize, Serialize};
 12use settings::SettingsStore;
 13use std::sync::Arc;
 14use ui::{h_flex, ContextMenu, IconButton, Tooltip};
 15use ui::{prelude::*, right_click_menu};
 16
 17const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
 18
 19pub enum PanelEvent {
 20    ZoomIn,
 21    ZoomOut,
 22    Activate,
 23    Close,
 24}
 25
 26pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
 27    fn persistent_name() -> &'static str;
 28    fn position(&self, cx: &WindowContext) -> DockPosition;
 29    fn position_is_valid(&self, position: DockPosition) -> bool;
 30    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
 31    fn size(&self, cx: &WindowContext) -> Pixels;
 32    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>);
 33    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
 34    fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
 35    fn toggle_action(&self) -> Box<dyn Action>;
 36    fn icon_label(&self, _: &WindowContext) -> Option<String> {
 37        None
 38    }
 39    fn is_zoomed(&self, _cx: &WindowContext) -> bool {
 40        false
 41    }
 42    fn starts_open(&self, _cx: &WindowContext) -> bool {
 43        false
 44    }
 45    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
 46    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
 47}
 48
 49pub trait PanelHandle: Send + Sync {
 50    fn panel_id(&self) -> EntityId;
 51    fn persistent_name(&self) -> &'static str;
 52    fn position(&self, cx: &WindowContext) -> DockPosition;
 53    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
 54    fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
 55    fn is_zoomed(&self, cx: &WindowContext) -> bool;
 56    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
 57    fn set_active(&self, active: bool, cx: &mut WindowContext);
 58    fn size(&self, cx: &WindowContext) -> Pixels;
 59    fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
 60    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
 61    fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
 62    fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
 63    fn icon_label(&self, cx: &WindowContext) -> Option<String>;
 64    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
 65    fn to_any(&self) -> AnyView;
 66}
 67
 68impl<T> PanelHandle for View<T>
 69where
 70    T: Panel,
 71{
 72    fn panel_id(&self) -> EntityId {
 73        Entity::entity_id(self)
 74    }
 75
 76    fn persistent_name(&self) -> &'static str {
 77        T::persistent_name()
 78    }
 79
 80    fn position(&self, cx: &WindowContext) -> DockPosition {
 81        self.read(cx).position(cx)
 82    }
 83
 84    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
 85        self.read(cx).position_is_valid(position)
 86    }
 87
 88    fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
 89        self.update(cx, |this, cx| this.set_position(position, cx))
 90    }
 91
 92    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 93        self.read(cx).is_zoomed(cx)
 94    }
 95
 96    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
 97        self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
 98    }
 99
100    fn set_active(&self, active: bool, cx: &mut WindowContext) {
101        self.update(cx, |this, cx| this.set_active(active, cx))
102    }
103
104    fn size(&self, cx: &WindowContext) -> Pixels {
105        self.read(cx).size(cx)
106    }
107
108    fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext) {
109        self.update(cx, |this, cx| this.set_size(size, cx))
110    }
111
112    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
113        self.read(cx).icon(cx)
114    }
115
116    fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> {
117        self.read(cx).icon_tooltip(cx)
118    }
119
120    fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
121        self.read(cx).toggle_action()
122    }
123
124    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
125        self.read(cx).icon_label(cx)
126    }
127
128    fn to_any(&self) -> AnyView {
129        self.clone().into()
130    }
131
132    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
133        self.read(cx).focus_handle(cx).clone()
134    }
135}
136
137impl From<&dyn PanelHandle> for AnyView {
138    fn from(val: &dyn PanelHandle) -> Self {
139        val.to_any()
140    }
141}
142
143/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge.
144/// Can contain multiple panels and show/hide itself with all contents.
145pub struct Dock {
146    position: DockPosition,
147    panel_entries: Vec<PanelEntry>,
148    is_open: bool,
149    active_panel_index: usize,
150    focus_handle: FocusHandle,
151    pub(crate) serialized_dock: Option<DockData>,
152    _focus_subscription: Subscription,
153}
154
155impl FocusableView for Dock {
156    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
157        self.focus_handle.clone()
158    }
159}
160
161#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
162#[serde(rename_all = "lowercase")]
163pub enum DockPosition {
164    Left,
165    Bottom,
166    Right,
167}
168
169impl DockPosition {
170    fn to_label(&self) -> &'static str {
171        match self {
172            Self::Left => "left",
173            Self::Bottom => "bottom",
174            Self::Right => "right",
175        }
176    }
177
178    pub fn axis(&self) -> Axis {
179        match self {
180            Self::Left | Self::Right => Axis::Horizontal,
181            Self::Bottom => Axis::Vertical,
182        }
183    }
184}
185
186struct PanelEntry {
187    panel: Arc<dyn PanelHandle>,
188    _subscriptions: [Subscription; 3],
189}
190
191pub struct PanelButtons {
192    dock: View<Dock>,
193}
194
195impl Dock {
196    pub fn new(position: DockPosition, cx: &mut ViewContext<Workspace>) -> View<Self> {
197        let focus_handle = cx.focus_handle();
198
199        let dock = cx.new_view(|cx: &mut ViewContext<Self>| {
200            let focus_subscription = cx.on_focus(&focus_handle, |dock, cx| {
201                if let Some(active_entry) = dock.panel_entries.get(dock.active_panel_index) {
202                    active_entry.panel.focus_handle(cx).focus(cx)
203                }
204            });
205            Self {
206                position,
207                panel_entries: Default::default(),
208                active_panel_index: 0,
209                is_open: false,
210                focus_handle: focus_handle.clone(),
211                _focus_subscription: focus_subscription,
212                serialized_dock: None,
213            }
214        });
215
216        cx.on_focus_in(&focus_handle, {
217            let dock = dock.downgrade();
218            move |workspace, cx| {
219                let Some(dock) = dock.upgrade() else {
220                    return;
221                };
222                let Some(panel) = dock.read(cx).active_panel() else {
223                    return;
224                };
225                if panel.is_zoomed(cx) {
226                    workspace.zoomed = Some(panel.to_any().downgrade());
227                    workspace.zoomed_position = Some(position);
228                } else {
229                    workspace.zoomed = None;
230                    workspace.zoomed_position = None;
231                }
232                workspace.dismiss_zoomed_items_to_reveal(Some(position), cx);
233                workspace.update_active_view_for_followers(cx)
234            }
235        })
236        .detach();
237
238        cx.observe(&dock, move |workspace, dock, cx| {
239            if dock.read(cx).is_open() {
240                if let Some(panel) = dock.read(cx).active_panel() {
241                    if panel.is_zoomed(cx) {
242                        workspace.zoomed = Some(panel.to_any().downgrade());
243                        workspace.zoomed_position = Some(position);
244                        return;
245                    }
246                }
247            }
248            if workspace.zoomed_position == Some(position) {
249                workspace.zoomed = None;
250                workspace.zoomed_position = None;
251            }
252        })
253        .detach();
254
255        dock
256    }
257
258    pub fn position(&self) -> DockPosition {
259        self.position
260    }
261
262    pub fn is_open(&self) -> bool {
263        self.is_open
264    }
265
266    pub fn panel<T: Panel>(&self) -> Option<View<T>> {
267        self.panel_entries
268            .iter()
269            .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
270    }
271
272    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
273        self.panel_entries
274            .iter()
275            .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
276    }
277
278    pub fn panel_index_for_persistent_name(
279        &self,
280        ui_name: &str,
281        _cx: &AppContext,
282    ) -> Option<usize> {
283        self.panel_entries
284            .iter()
285            .position(|entry| entry.panel.persistent_name() == ui_name)
286    }
287
288    pub fn active_panel_index(&self) -> usize {
289        self.active_panel_index
290    }
291
292    pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
293        if open != self.is_open {
294            self.is_open = open;
295            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
296                active_panel.panel.set_active(open, cx);
297            }
298
299            cx.notify();
300        }
301    }
302
303    pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
304        for entry in &mut self.panel_entries {
305            if entry.panel.panel_id() == panel.entity_id() {
306                if zoomed != entry.panel.is_zoomed(cx) {
307                    entry.panel.set_zoomed(zoomed, cx);
308                }
309            } else if entry.panel.is_zoomed(cx) {
310                entry.panel.set_zoomed(false, cx);
311            }
312        }
313
314        cx.notify();
315    }
316
317    pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
318        for entry in &mut self.panel_entries {
319            if entry.panel.is_zoomed(cx) {
320                entry.panel.set_zoomed(false, cx);
321            }
322        }
323    }
324
325    pub(crate) fn add_panel<T: Panel>(
326        &mut self,
327        panel: View<T>,
328        workspace: WeakView<Workspace>,
329        cx: &mut ViewContext<Self>,
330    ) {
331        let subscriptions = [
332            cx.observe(&panel, |_, _, cx| cx.notify()),
333            cx.observe_global::<SettingsStore>({
334                let workspace = workspace.clone();
335                let panel = panel.clone();
336
337                move |this, cx| {
338                    let new_position = panel.read(cx).position(cx);
339                    if new_position == this.position {
340                        return;
341                    }
342
343                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
344                        if panel.is_zoomed(cx) {
345                            workspace.zoomed_position = Some(new_position);
346                        }
347                        match new_position {
348                            DockPosition::Left => &workspace.left_dock,
349                            DockPosition::Bottom => &workspace.bottom_dock,
350                            DockPosition::Right => &workspace.right_dock,
351                        }
352                        .clone()
353                    }) else {
354                        return;
355                    };
356
357                    let was_visible = this.is_open()
358                        && this.visible_panel().map_or(false, |active_panel| {
359                            active_panel.panel_id() == Entity::entity_id(&panel)
360                        });
361
362                    this.remove_panel(&panel, cx);
363
364                    new_dock.update(cx, |new_dock, cx| {
365                        new_dock.add_panel(panel.clone(), workspace.clone(), cx);
366                        if was_visible {
367                            new_dock.set_open(true, cx);
368                            new_dock.activate_panel(new_dock.panels_len() - 1, cx);
369                        }
370                    });
371                }
372            }),
373            cx.subscribe(&panel, move |this, panel, event, cx| match event {
374                PanelEvent::ZoomIn => {
375                    this.set_panel_zoomed(&panel.to_any(), true, cx);
376                    if !panel.focus_handle(cx).contains_focused(cx) {
377                        cx.focus_view(&panel);
378                    }
379                    workspace
380                        .update(cx, |workspace, cx| {
381                            workspace.zoomed = Some(panel.downgrade().into());
382                            workspace.zoomed_position = Some(panel.read(cx).position(cx));
383                        })
384                        .ok();
385                }
386                PanelEvent::ZoomOut => {
387                    this.set_panel_zoomed(&panel.to_any(), false, cx);
388                    workspace
389                        .update(cx, |workspace, cx| {
390                            if workspace.zoomed_position == Some(this.position) {
391                                workspace.zoomed = None;
392                                workspace.zoomed_position = None;
393                            }
394                            cx.notify();
395                        })
396                        .ok();
397                }
398                PanelEvent::Activate => {
399                    if let Some(ix) = this
400                        .panel_entries
401                        .iter()
402                        .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
403                    {
404                        this.set_open(true, cx);
405                        this.activate_panel(ix, cx);
406                        cx.focus_view(&panel);
407                    }
408                }
409                PanelEvent::Close => {
410                    if this
411                        .visible_panel()
412                        .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
413                    {
414                        this.set_open(false, cx);
415                    }
416                }
417            }),
418        ];
419
420        let name = panel.persistent_name().to_string();
421
422        self.panel_entries.push(PanelEntry {
423            panel: Arc::new(panel.clone()),
424            _subscriptions: subscriptions,
425        });
426        if let Some(serialized) = self.serialized_dock.clone() {
427            if serialized.active_panel == Some(name) {
428                self.activate_panel(self.panel_entries.len() - 1, cx);
429                if serialized.visible {
430                    self.set_open(true, cx);
431                }
432                if serialized.zoom {
433                    if let Some(panel) = self.active_panel() {
434                        panel.set_zoomed(true, cx)
435                    };
436                }
437            }
438        } else if panel.read(cx).starts_open(cx) {
439            self.activate_panel(self.panel_entries.len() - 1, cx);
440            self.set_open(true, cx);
441        }
442
443        cx.notify()
444    }
445
446    pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
447        if let Some(panel_ix) = self
448            .panel_entries
449            .iter()
450            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
451        {
452            if panel_ix == self.active_panel_index {
453                self.active_panel_index = 0;
454                self.set_open(false, cx);
455            } else if panel_ix < self.active_panel_index {
456                self.active_panel_index -= 1;
457            }
458            self.panel_entries.remove(panel_ix);
459            cx.notify();
460        }
461    }
462
463    pub fn panels_len(&self) -> usize {
464        self.panel_entries.len()
465    }
466
467    pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
468        if panel_ix != self.active_panel_index {
469            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
470                active_panel.panel.set_active(false, cx);
471            }
472
473            self.active_panel_index = panel_ix;
474            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
475                active_panel.panel.set_active(true, cx);
476            }
477
478            cx.notify();
479        }
480    }
481
482    pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
483        let entry = self.visible_entry()?;
484        Some(&entry.panel)
485    }
486
487    pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
488        Some(&self.panel_entries.get(self.active_panel_index)?.panel)
489    }
490
491    fn visible_entry(&self) -> Option<&PanelEntry> {
492        if self.is_open {
493            self.panel_entries.get(self.active_panel_index)
494        } else {
495            None
496        }
497    }
498
499    pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
500        let entry = self.visible_entry()?;
501        if entry.panel.is_zoomed(cx) {
502            Some(entry.panel.clone())
503        } else {
504            None
505        }
506    }
507
508    pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<Pixels> {
509        self.panel_entries
510            .iter()
511            .find(|entry| entry.panel.panel_id() == panel.panel_id())
512            .map(|entry| entry.panel.size(cx))
513    }
514
515    pub fn active_panel_size(&self, cx: &WindowContext) -> Option<Pixels> {
516        if self.is_open {
517            self.panel_entries
518                .get(self.active_panel_index)
519                .map(|entry| entry.panel.size(cx))
520        } else {
521            None
522        }
523    }
524
525    pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
526        if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
527            let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
528            entry.panel.set_size(size, cx);
529            cx.notify();
530        }
531    }
532
533    pub fn toggle_action(&self) -> Box<dyn Action> {
534        match self.position {
535            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
536            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
537            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
538        }
539    }
540
541    fn dispatch_context() -> KeyContext {
542        let mut dispatch_context = KeyContext::default();
543        dispatch_context.add("Dock");
544
545        dispatch_context
546    }
547}
548
549impl Render for Dock {
550    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
551        let dispatch_context = Self::dispatch_context();
552        if let Some(entry) = self.visible_entry() {
553            let size = entry.panel.size(cx);
554
555            let position = self.position;
556            let handle = div()
557                .id("resize-handle")
558                .on_drag(DraggedDock(position), |dock, cx| {
559                    cx.stop_propagation();
560                    cx.new_view(|_| dock.clone())
561                })
562                .on_click(cx.listener(|v, e: &ClickEvent, cx| {
563                    if e.down.button == MouseButton::Left && e.down.click_count == 2 {
564                        v.resize_active_panel(None, cx);
565                        cx.stop_propagation();
566                    }
567                }))
568                .occlude();
569            let handle = match self.position() {
570                DockPosition::Left => deferred(
571                    handle
572                        .absolute()
573                        .right(-RESIZE_HANDLE_SIZE / 2.)
574                        .top(px(0.))
575                        .h_full()
576                        .w(RESIZE_HANDLE_SIZE)
577                        .cursor_col_resize(),
578                ),
579                DockPosition::Bottom => deferred(
580                    handle
581                        .absolute()
582                        .top(-RESIZE_HANDLE_SIZE / 2.)
583                        .left(px(0.))
584                        .w_full()
585                        .h(RESIZE_HANDLE_SIZE)
586                        .cursor_row_resize(),
587                ),
588                DockPosition::Right => deferred(
589                    handle
590                        .absolute()
591                        .top(px(0.))
592                        .left(-RESIZE_HANDLE_SIZE / 2.)
593                        .h_full()
594                        .w(RESIZE_HANDLE_SIZE)
595                        .cursor_col_resize(),
596                ),
597            };
598
599            div()
600                .key_context(dispatch_context)
601                .track_focus(&self.focus_handle)
602                .flex()
603                .bg(cx.theme().colors().panel_background)
604                .border_color(cx.theme().colors().border)
605                .overflow_hidden()
606                .map(|this| match self.position().axis() {
607                    Axis::Horizontal => this.w(size).h_full().flex_row(),
608                    Axis::Vertical => this.h(size).w_full().flex_col(),
609                })
610                .map(|this| match self.position() {
611                    DockPosition::Left => this.border_r(),
612                    DockPosition::Right => this.border_l(),
613                    DockPosition::Bottom => this.border_t(),
614                })
615                .child(
616                    div()
617                        .map(|this| match self.position().axis() {
618                            Axis::Horizontal => this.min_w(size).h_full(),
619                            Axis::Vertical => this.min_h(size).w_full(),
620                        })
621                        .child(
622                            entry
623                                .panel
624                                .to_any()
625                                .cached(StyleRefinement::default().v_flex().size_full()),
626                        ),
627                )
628                .child(handle)
629        } else {
630            div()
631                .key_context(dispatch_context)
632                .track_focus(&self.focus_handle)
633        }
634    }
635}
636
637impl PanelButtons {
638    pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
639        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
640        Self { dock }
641    }
642}
643
644impl Render for PanelButtons {
645    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
646        let dock = self.dock.read(cx);
647        let active_index = dock.active_panel_index;
648        let is_open = dock.is_open;
649        let dock_position = dock.position;
650
651        let (menu_anchor, menu_attach) = match dock.position {
652            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
653            DockPosition::Bottom | DockPosition::Right => {
654                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
655            }
656        };
657
658        let buttons = dock
659            .panel_entries
660            .iter()
661            .enumerate()
662            .filter_map(|(i, entry)| {
663                let icon = entry.panel.icon(cx)?;
664                let icon_tooltip = entry.panel.icon_tooltip(cx)?;
665                let name = entry.panel.persistent_name();
666                let panel = entry.panel.clone();
667
668                let is_active_button = i == active_index && is_open;
669                let (action, tooltip) = if is_active_button {
670                    let action = dock.toggle_action();
671
672                    let tooltip: SharedString =
673                        format!("Close {} dock", dock.position.to_label()).into();
674
675                    (action, tooltip)
676                } else {
677                    let action = entry.panel.toggle_action(cx);
678
679                    (action, icon_tooltip.into())
680                };
681
682                Some(
683                    right_click_menu(name)
684                        .menu(move |cx| {
685                            const POSITIONS: [DockPosition; 3] = [
686                                DockPosition::Left,
687                                DockPosition::Right,
688                                DockPosition::Bottom,
689                            ];
690
691                            ContextMenu::build(cx, |mut menu, cx| {
692                                for position in POSITIONS {
693                                    if position != dock_position
694                                        && panel.position_is_valid(position, cx)
695                                    {
696                                        let panel = panel.clone();
697                                        menu = menu.entry(
698                                            format!("Dock {}", position.to_label()),
699                                            None,
700                                            move |cx| {
701                                                panel.set_position(position, cx);
702                                            },
703                                        )
704                                    }
705                                }
706                                menu
707                            })
708                        })
709                        .anchor(menu_anchor)
710                        .attach(menu_attach)
711                        .trigger(
712                            IconButton::new(name, icon)
713                                .icon_size(IconSize::Small)
714                                .selected(is_active_button)
715                                .on_click({
716                                    let action = action.boxed_clone();
717                                    move |_, cx| cx.dispatch_action(action.boxed_clone())
718                                })
719                                .tooltip(move |cx| {
720                                    Tooltip::for_action(tooltip.clone(), &*action, cx)
721                                }),
722                        ),
723                )
724            });
725
726        h_flex().gap_0p5().children(buttons)
727    }
728}
729
730impl StatusItemView for PanelButtons {
731    fn set_active_pane_item(
732        &mut self,
733        _active_pane_item: Option<&dyn crate::ItemHandle>,
734        _cx: &mut ViewContext<Self>,
735    ) {
736        // Nothing to do, panel buttons don't depend on the active center item
737    }
738}
739
740#[cfg(any(test, feature = "test-support"))]
741pub mod test {
742    use super::*;
743    use gpui::{actions, div, ViewContext, WindowContext};
744
745    pub struct TestPanel {
746        pub position: DockPosition,
747        pub zoomed: bool,
748        pub active: bool,
749        pub focus_handle: FocusHandle,
750        pub size: Pixels,
751    }
752    actions!(test, [ToggleTestPanel]);
753
754    impl EventEmitter<PanelEvent> for TestPanel {}
755
756    impl TestPanel {
757        pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
758            Self {
759                position,
760                zoomed: false,
761                active: false,
762                focus_handle: cx.focus_handle(),
763                size: px(300.),
764            }
765        }
766    }
767
768    impl Render for TestPanel {
769        fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
770            div().id("test").track_focus(&self.focus_handle)
771        }
772    }
773
774    impl Panel for TestPanel {
775        fn persistent_name() -> &'static str {
776            "TestPanel"
777        }
778
779        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
780            self.position
781        }
782
783        fn position_is_valid(&self, _: super::DockPosition) -> bool {
784            true
785        }
786
787        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
788            self.position = position;
789            cx.update_global::<SettingsStore, _>(|_, _| {});
790        }
791
792        fn size(&self, _: &WindowContext) -> Pixels {
793            self.size
794        }
795
796        fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
797            self.size = size.unwrap_or(px(300.));
798        }
799
800        fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
801            None
802        }
803
804        fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
805            None
806        }
807
808        fn toggle_action(&self) -> Box<dyn Action> {
809            ToggleTestPanel.boxed_clone()
810        }
811
812        fn is_zoomed(&self, _: &WindowContext) -> bool {
813            self.zoomed
814        }
815
816        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
817            self.zoomed = zoomed;
818        }
819
820        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
821            self.active = active;
822        }
823    }
824
825    impl FocusableView for TestPanel {
826        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
827            self.focus_handle.clone()
828        }
829    }
830}