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