Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:49:48.645Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

dock.rs

1560 lines · 56.4 KB · rust
1use crate::focus_follows_mouse::FocusFollowsMouse as _;
2use crate::persistence::model::DockData;
3use crate::status_bar::HideStatusItem;
4use crate::{DraggedDock, Event, FocusFollowsMouse, ModalLayer, Pane, WorkspaceSettings};
5use crate::{Workspace, status_bar::StatusItemView};
6use anyhow::Context as _;
7use client::proto;
8use db::kvp::KeyValueStore;
9
10use gpui::{
11    Action, Anchor, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle,
12    Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
13    Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
14    px,
15};
16use serde::{Deserialize, Serialize};
17use settings::{Settings, SettingsStore, TerminalDockPosition};
18use std::sync::Arc;
19use ui::{
20    ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
21    right_click_menu,
22};
23use util::ResultExt as _;
24
25pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
26
27pub enum PanelEvent {
28    ZoomIn,
29    ZoomOut,
30    Activate,
31    Close,
32}
33
34pub use proto::PanelId;
35
36pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
37    fn persistent_name() -> &'static str;
38    fn panel_key() -> &'static str;
39    fn position(&self, window: &Window, cx: &App) -> DockPosition;
40    fn position_is_valid(&self, position: DockPosition) -> bool;
41    fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
42    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
43    fn min_size(&self, _window: &Window, _cx: &App) -> Option<Pixels> {
44        None
45    }
46    fn initial_size_state(&self, _window: &Window, _cx: &App) -> PanelSizeState {
47        PanelSizeState::default()
48    }
49    fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
50    fn supports_flexible_size(&self) -> bool {
51        false
52    }
53    fn has_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
54        false
55    }
56    fn set_flexible_size(
57        &mut self,
58        _flexible: bool,
59        _window: &mut Window,
60        _cx: &mut Context<Self>,
61    ) {
62    }
63    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
64    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
65    fn toggle_action(&self) -> Box<dyn Action>;
66    fn icon_label(&self, _window: &Window, _: &App) -> Option<String> {
67        None
68    }
69    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
70        false
71    }
72    fn starts_open(&self, _window: &Window, _cx: &App) -> bool {
73        false
74    }
75    fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
76    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
77    fn pane(&self) -> Option<Entity<Pane>> {
78        None
79    }
80    fn remote_id() -> Option<proto::PanelId> {
81        None
82    }
83    fn activation_priority(&self) -> u32;
84    fn enabled(&self, _cx: &App) -> bool {
85        true
86    }
87    fn is_agent_panel(&self) -> bool {
88        false
89    }
90    /// Returns metadata describing how to hide this panel's button from the
91    /// status bar by writing to user settings. Implementors should return
92    /// `None` if the panel button cannot be hidden through settings.
93    fn hide_button_setting(&self, _: &App) -> Option<HideStatusItem> {
94        None
95    }
96}
97
98pub trait PanelHandle: Send + Sync {
99    fn panel_id(&self) -> EntityId;
100    fn persistent_name(&self) -> &'static str;
101    fn panel_key(&self) -> &'static str;
102    fn position(&self, window: &Window, cx: &App) -> DockPosition;
103    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool;
104    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App);
105    fn is_zoomed(&self, window: &Window, cx: &App) -> bool;
106    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App);
107    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
108    fn remote_id(&self) -> Option<proto::PanelId>;
109    fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
110    fn default_size(&self, window: &Window, cx: &App) -> Pixels;
111    fn min_size(&self, window: &Window, cx: &App) -> Option<Pixels>;
112    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState;
113    fn size_state_changed(&self, window: &mut Window, cx: &mut App);
114    fn supports_flexible_size(&self, cx: &App) -> bool;
115    fn has_flexible_size(&self, window: &Window, cx: &App) -> bool;
116    fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App);
117    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
118    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
119    fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
120    fn icon_label(&self, window: &Window, cx: &App) -> Option<String>;
121    fn panel_focus_handle(&self, cx: &App) -> FocusHandle;
122    fn to_any(&self) -> AnyView;
123    fn activation_priority(&self, cx: &App) -> u32;
124    fn enabled(&self, cx: &App) -> bool;
125    fn is_agent_panel(&self, cx: &App) -> bool;
126    fn hide_button_setting(&self, cx: &App) -> Option<HideStatusItem>;
127    fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
128        let current_position = self.position(window, cx);
129        let next_position = [
130            DockPosition::Left,
131            DockPosition::Bottom,
132            DockPosition::Right,
133        ]
134        .into_iter()
135        .filter(|position| self.position_is_valid(*position, cx))
136        .skip_while(|valid_position| *valid_position != current_position)
137        .nth(1)
138        .unwrap_or(DockPosition::Left);
139
140        self.set_position(next_position, window, cx);
141    }
142}
143
144impl<T> PanelHandle for Entity<T>
145where
146    T: Panel,
147{
148    fn panel_id(&self) -> EntityId {
149        Entity::entity_id(self)
150    }
151
152    fn persistent_name(&self) -> &'static str {
153        T::persistent_name()
154    }
155
156    fn panel_key(&self) -> &'static str {
157        T::panel_key()
158    }
159
160    fn position(&self, window: &Window, cx: &App) -> DockPosition {
161        self.read(cx).position(window, cx)
162    }
163
164    fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool {
165        self.read(cx).position_is_valid(position)
166    }
167
168    fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) {
169        self.update(cx, |this, cx| this.set_position(position, window, cx))
170    }
171
172    fn is_zoomed(&self, window: &Window, cx: &App) -> bool {
173        self.read(cx).is_zoomed(window, cx)
174    }
175
176    fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) {
177        self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx))
178    }
179
180    fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
181        self.update(cx, |this, cx| this.set_active(active, window, cx))
182    }
183
184    fn pane(&self, cx: &App) -> Option<Entity<Pane>> {
185        self.read(cx).pane()
186    }
187
188    fn remote_id(&self) -> Option<PanelId> {
189        T::remote_id()
190    }
191
192    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
193        self.read(cx).default_size(window, cx)
194    }
195
196    fn min_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
197        self.read(cx).min_size(window, cx)
198    }
199
200    fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState {
201        self.read(cx).initial_size_state(window, cx)
202    }
203
204    fn size_state_changed(&self, window: &mut Window, cx: &mut App) {
205        self.update(cx, |this, cx| this.size_state_changed(window, cx))
206    }
207
208    fn supports_flexible_size(&self, cx: &App) -> bool {
209        self.read(cx).supports_flexible_size()
210    }
211
212    fn has_flexible_size(&self, window: &Window, cx: &App) -> bool {
213        self.read(cx).has_flexible_size(window, cx)
214    }
215
216    fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App) {
217        self.update(cx, |this, cx| this.set_flexible_size(flexible, window, cx))
218    }
219
220    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
221        self.read(cx).icon(window, cx)
222    }
223
224    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
225        self.read(cx).icon_tooltip(window, cx)
226    }
227
228    fn toggle_action(&self, _: &Window, cx: &App) -> Box<dyn Action> {
229        self.read(cx).toggle_action()
230    }
231
232    fn icon_label(&self, window: &Window, cx: &App) -> Option<String> {
233        self.read(cx).icon_label(window, cx)
234    }
235
236    fn to_any(&self) -> AnyView {
237        self.clone().into()
238    }
239
240    fn panel_focus_handle(&self, cx: &App) -> FocusHandle {
241        self.read(cx).focus_handle(cx)
242    }
243
244    fn activation_priority(&self, cx: &App) -> u32 {
245        self.read(cx).activation_priority()
246    }
247
248    fn enabled(&self, cx: &App) -> bool {
249        self.read(cx).enabled(cx)
250    }
251
252    fn is_agent_panel(&self, cx: &App) -> bool {
253        self.read(cx).is_agent_panel()
254    }
255
256    fn hide_button_setting(&self, cx: &App) -> Option<HideStatusItem> {
257        self.read(cx).hide_button_setting(cx)
258    }
259}
260
261impl From<&dyn PanelHandle> for AnyView {
262    fn from(val: &dyn PanelHandle) -> Self {
263        val.to_any()
264    }
265}
266
267/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge.
268/// Can contain multiple panels and show/hide itself with all contents.
269pub struct Dock {
270    position: DockPosition,
271    panel_entries: Vec<PanelEntry>,
272    workspace: WeakEntity<Workspace>,
273    is_open: bool,
274    active_panel_index: Option<usize>,
275    focus_handle: FocusHandle,
276    focus_follows_mouse: FocusFollowsMouse,
277    pub(crate) serialized_dock: Option<DockData>,
278    zoom_layer_open: bool,
279    modal_layer: Entity<ModalLayer>,
280    _subscriptions: [Subscription; 2],
281}
282
283impl Focusable for Dock {
284    fn focus_handle(&self, _: &App) -> FocusHandle {
285        self.focus_handle.clone()
286    }
287}
288
289#[derive(Copy, Clone, Debug, PartialEq, Eq)]
290pub enum DockPosition {
291    Left,
292    Bottom,
293    Right,
294}
295
296impl From<settings::DockPosition> for DockPosition {
297    fn from(value: settings::DockPosition) -> Self {
298        match value {
299            settings::DockPosition::Left => Self::Left,
300            settings::DockPosition::Bottom => Self::Bottom,
301            settings::DockPosition::Right => Self::Right,
302        }
303    }
304}
305
306impl Into<settings::DockPosition> for DockPosition {
307    fn into(self) -> settings::DockPosition {
308        match self {
309            Self::Left => settings::DockPosition::Left,
310            Self::Bottom => settings::DockPosition::Bottom,
311            Self::Right => settings::DockPosition::Right,
312        }
313    }
314}
315
316impl From<TerminalDockPosition> for DockPosition {
317    fn from(value: TerminalDockPosition) -> Self {
318        match value {
319            TerminalDockPosition::Left => DockPosition::Left,
320            TerminalDockPosition::Bottom => DockPosition::Bottom,
321            TerminalDockPosition::Right => DockPosition::Right,
322        }
323    }
324}
325
326impl DockPosition {
327    fn label(&self) -> &'static str {
328        match self {
329            Self::Left => "Left",
330            Self::Bottom => "Bottom",
331            Self::Right => "Right",
332        }
333    }
334
335    pub fn axis(&self) -> Axis {
336        match self {
337            Self::Left | Self::Right => Axis::Horizontal,
338            Self::Bottom => Axis::Vertical,
339        }
340    }
341}
342
343#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
344pub struct PanelSizeState {
345    pub size: Option<Pixels>,
346    #[serde(default)]
347    pub flex: Option<f32>,
348}
349
350struct PanelEntry {
351    panel: Arc<dyn PanelHandle>,
352    size_state: PanelSizeState,
353    _subscriptions: [Subscription; 3],
354}
355
356pub struct PanelButtons {
357    dock: Entity<Dock>,
358    _settings_subscription: Subscription,
359}
360
361pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
362
363fn panel_uses_flexible_width(
364    position: DockPosition,
365    panel: &dyn PanelHandle,
366    window: &Window,
367    cx: &App,
368) -> bool {
369    position.axis() == Axis::Horizontal && panel.has_flexible_size(window, cx)
370}
371
372fn resize_panel_entry(
373    position: DockPosition,
374    entry: &mut PanelEntry,
375    size: Option<Pixels>,
376    flex: Option<f32>,
377    window: &mut Window,
378    cx: &mut App,
379) -> (&'static str, PanelSizeState) {
380    let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
381    let uses_flexible_width = panel_uses_flexible_width(position, entry.panel.as_ref(), window, cx);
382    if uses_flexible_width {
383        entry.size_state.flex = flex;
384    } else {
385        entry.size_state.size = size;
386    }
387    entry.panel.size_state_changed(window, cx);
388    (entry.panel.panel_key(), entry.size_state)
389}
390
391impl Dock {
392    pub fn new(
393        position: DockPosition,
394        modal_layer: Entity<ModalLayer>,
395        window: &mut Window,
396        cx: &mut Context<Workspace>,
397    ) -> Entity<Self> {
398        let focus_handle = cx.focus_handle();
399        let workspace = cx.entity();
400        let dock = cx.new(|cx| {
401            let focus_subscription =
402                cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
403                    if let Some(active_entry) = dock.active_panel_entry() {
404                        active_entry.panel.panel_focus_handle(cx).focus(window, cx)
405                    }
406                });
407            let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
408                if matches!(e, Event::ZoomChanged) {
409                    let is_zoomed = workspace.read(cx).zoomed.is_some();
410                    dock.zoom_layer_open = is_zoomed;
411                }
412            });
413            Self {
414                position,
415                workspace: workspace.downgrade(),
416                panel_entries: Default::default(),
417                active_panel_index: None,
418                is_open: false,
419                focus_handle: focus_handle.clone(),
420                focus_follows_mouse: WorkspaceSettings::get_global(cx).focus_follows_mouse,
421                _subscriptions: [focus_subscription, zoom_subscription],
422                serialized_dock: None,
423                zoom_layer_open: false,
424                modal_layer,
425            }
426        });
427
428        cx.on_focus_in(&focus_handle, window, {
429            let dock = dock.downgrade();
430            move |workspace, window, cx| {
431                let Some(dock) = dock.upgrade() else {
432                    return;
433                };
434                let Some(panel) = dock.read(cx).active_panel() else {
435                    return;
436                };
437                if panel.is_zoomed(window, cx) {
438                    workspace.zoomed = Some(panel.to_any().downgrade());
439                    workspace.zoomed_position = Some(position);
440                } else {
441                    workspace.zoomed = None;
442                    workspace.zoomed_position = None;
443                }
444                cx.emit(Event::ZoomChanged);
445                workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx);
446                workspace.update_active_view_for_followers(window, cx)
447            }
448        })
449        .detach();
450
451        cx.observe_in(&dock, window, move |workspace, dock, window, cx| {
452            if dock.read(cx).is_open()
453                && let Some(panel) = dock.read(cx).active_panel()
454                && panel.is_zoomed(window, cx)
455            {
456                workspace.zoomed = Some(panel.to_any().downgrade());
457                workspace.zoomed_position = Some(position);
458                cx.emit(Event::ZoomChanged);
459                return;
460            }
461            if workspace.zoomed_position == Some(position) {
462                workspace.zoomed = None;
463                workspace.zoomed_position = None;
464                cx.emit(Event::ZoomChanged);
465            }
466        })
467        .detach();
468
469        dock
470    }
471
472    pub fn position(&self) -> DockPosition {
473        self.position
474    }
475
476    pub fn is_open(&self) -> bool {
477        self.is_open
478    }
479
480    fn resizable(&self, cx: &App) -> bool {
481        !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal())
482    }
483
484    pub fn panel<T: Panel>(&self) -> Option<Entity<T>> {
485        self.panel_entries
486            .iter()
487            .find_map(|entry| entry.panel.to_any().downcast().ok())
488    }
489
490    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
491        self.panel_entries
492            .iter()
493            .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
494    }
495
496    pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option<usize> {
497        self.panel_entries
498            .iter()
499            .position(|entry| entry.panel.persistent_name() == ui_name)
500    }
501
502    pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
503        self.panel_entries
504            .iter()
505            .position(|entry| entry.panel.remote_id() == Some(panel_id))
506    }
507
508    pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
509        self.panel_entries
510            .iter()
511            .find(|entry| entry.panel.panel_id() == panel_id)
512            .map(|entry| &entry.panel)
513    }
514
515    pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
516        self.panel_entries
517            .iter()
518            .position(|entry| entry.panel.enabled(cx))
519            .with_context(|| {
520                format!(
521                    "Couldn't find any enabled panel for the {} dock.",
522                    self.position.label()
523                )
524            })
525    }
526
527    fn active_panel_entry(&self) -> Option<&PanelEntry> {
528        self.active_panel_index
529            .and_then(|index| self.panel_entries.get(index))
530    }
531
532    pub fn active_panel_index(&self) -> Option<usize> {
533        self.active_panel_index
534    }
535
536    pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
537        if open != self.is_open {
538            self.is_open = open;
539            if let Some(active_panel) = self.active_panel_entry() {
540                active_panel.panel.set_active(open, window, cx);
541            }
542
543            cx.notify();
544        }
545    }
546
547    pub fn set_panel_zoomed(
548        &mut self,
549        panel: &AnyView,
550        zoomed: bool,
551        window: &mut Window,
552        cx: &mut Context<Self>,
553    ) {
554        for entry in &mut self.panel_entries {
555            if entry.panel.panel_id() == panel.entity_id() {
556                if zoomed != entry.panel.is_zoomed(window, cx) {
557                    entry.panel.set_zoomed(zoomed, window, cx);
558                }
559            } else if entry.panel.is_zoomed(window, cx) {
560                entry.panel.set_zoomed(false, window, cx);
561            }
562        }
563
564        self.workspace
565            .update(cx, |workspace, cx| {
566                workspace.serialize_workspace(window, cx);
567            })
568            .ok();
569        cx.notify();
570    }
571
572    pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
573        for entry in &mut self.panel_entries {
574            if entry.panel.is_zoomed(window, cx) {
575                entry.panel.set_zoomed(false, window, cx);
576            }
577        }
578    }
579
580    pub(crate) fn add_panel<T: Panel>(
581        &mut self,
582        panel: Entity<T>,
583        workspace: WeakEntity<Workspace>,
584        window: &mut Window,
585        cx: &mut Context<Self>,
586    ) -> usize {
587        let subscriptions = [
588            cx.observe(&panel, |_, _, cx| cx.notify()),
589            cx.observe_global_in::<SettingsStore>(window, {
590                let workspace = workspace.clone();
591                let panel = panel.clone();
592
593                move |this, window, cx| {
594                    let new_position = panel.read(cx).position(window, cx);
595                    if new_position == this.position {
596                        return;
597                    }
598
599                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
600                        if panel.is_zoomed(window, cx) {
601                            workspace.zoomed_position = Some(new_position);
602                        }
603                        match new_position {
604                            DockPosition::Left => &workspace.left_dock,
605                            DockPosition::Bottom => &workspace.bottom_dock,
606                            DockPosition::Right => &workspace.right_dock,
607                        }
608                        .clone()
609                    }) else {
610                        return;
611                    };
612
613                    let panel_id = Entity::entity_id(&panel);
614                    let was_visible = this.is_open()
615                        && this
616                            .visible_panel()
617                            .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
618                    let size_state = this
619                        .panel_entries
620                        .iter()
621                        .find(|entry| entry.panel.panel_id() == panel_id)
622                        .map(|entry| entry.size_state)
623                        .unwrap_or_default();
624
625                    let previous_axis = this.position.axis();
626                    let next_axis = new_position.axis();
627                    let size_state = if previous_axis == next_axis {
628                        size_state
629                    } else {
630                        PanelSizeState::default()
631                    };
632
633                    if !this.remove_panel(&panel, window, cx) {
634                        // Panel was already moved from this dock
635                        return;
636                    }
637
638                    new_dock.update(cx, |new_dock, cx| {
639                        let index =
640                            new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
641                        if let Some(added_panel) = new_dock.panel_for_id(panel_id).cloned() {
642                            new_dock.set_panel_size_state(added_panel.as_ref(), size_state, cx);
643                        }
644                        if was_visible {
645                            new_dock.set_open(true, window, cx);
646                            new_dock.activate_panel(index, window, cx);
647                        }
648                    });
649
650                    workspace
651                        .update(cx, |workspace, cx| {
652                            workspace.serialize_workspace(window, cx);
653                        })
654                        .ok();
655                }
656            }),
657            cx.subscribe_in(
658                &panel,
659                window,
660                move |this, panel, event, window, cx| match event {
661                    PanelEvent::ZoomIn => {
662                        this.set_panel_zoomed(&panel.to_any(), true, window, cx);
663                        if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx)
664                        {
665                            window.focus(&panel.focus_handle(cx), cx);
666                        }
667                        workspace
668                            .update(cx, |workspace, cx| {
669                                workspace.zoomed = Some(panel.downgrade().into());
670                                workspace.zoomed_position =
671                                    Some(panel.read(cx).position(window, cx));
672                                cx.emit(Event::ZoomChanged);
673                            })
674                            .ok();
675                    }
676                    PanelEvent::ZoomOut => {
677                        this.set_panel_zoomed(&panel.to_any(), false, window, cx);
678                        workspace
679                            .update(cx, |workspace, cx| {
680                                if workspace.zoomed_position == Some(this.position) {
681                                    workspace.zoomed = None;
682                                    workspace.zoomed_position = None;
683                                    cx.emit(Event::ZoomChanged);
684                                }
685                                cx.notify();
686                            })
687                            .ok();
688                    }
689                    PanelEvent::Activate => {
690                        if let Some(ix) = this
691                            .panel_entries
692                            .iter()
693                            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
694                        {
695                            this.set_open(true, window, cx);
696                            this.activate_panel(ix, window, cx);
697                            window.focus(&panel.read(cx).focus_handle(cx), cx);
698                        }
699                    }
700                    PanelEvent::Close => {
701                        if this
702                            .visible_panel()
703                            .is_some_and(|p| p.panel_id() == Entity::entity_id(panel))
704                        {
705                            this.set_open(false, window, cx);
706                        }
707                    }
708                },
709            ),
710        ];
711
712        let index = match self
713            .panel_entries
714            .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| {
715                entry.panel.activation_priority(cx)
716            }) {
717            Ok(ix) => {
718                if cfg!(debug_assertions) {
719                    panic!(
720                        "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.",
721                        T::panel_key(),
722                        self.panel_entries[ix].panel.panel_key()
723                    );
724                }
725                ix
726            }
727            Err(ix) => ix,
728        };
729        if let Some(active_index) = self.active_panel_index.as_mut()
730            && *active_index >= index
731        {
732            *active_index += 1;
733        }
734        let size_state = panel.read(cx).initial_size_state(window, cx);
735
736        self.panel_entries.insert(
737            index,
738            PanelEntry {
739                panel: Arc::new(panel.clone()),
740                size_state,
741                _subscriptions: subscriptions,
742            },
743        );
744
745        self.restore_state(window, cx);
746
747        if panel.read(cx).starts_open(window, cx) {
748            self.activate_panel(index, window, cx);
749            self.set_open(true, window, cx);
750        }
751
752        cx.notify();
753        index
754    }
755
756    pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
757        if let Some(serialized) = self.serialized_dock.clone() {
758            if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible)
759                && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx)
760            {
761                self.activate_panel(idx, window, cx);
762            }
763
764            if serialized.zoom
765                && let Some(panel) = self.active_panel()
766            {
767                panel.set_zoomed(true, window, cx)
768            }
769            self.set_open(serialized.visible, window, cx);
770            return true;
771        }
772        false
773    }
774
775    pub fn remove_panel<T: Panel>(
776        &mut self,
777        panel: &Entity<T>,
778        window: &mut Window,
779        cx: &mut Context<Self>,
780    ) -> bool {
781        if let Some(panel_ix) = self
782            .panel_entries
783            .iter()
784            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
785        {
786            if let Some(active_panel_index) = self.active_panel_index.as_mut() {
787                match panel_ix.cmp(active_panel_index) {
788                    std::cmp::Ordering::Less => {
789                        *active_panel_index -= 1;
790                    }
791                    std::cmp::Ordering::Equal => {
792                        self.active_panel_index = None;
793                        self.set_open(false, window, cx);
794                    }
795                    std::cmp::Ordering::Greater => {}
796                }
797            }
798
799            self.panel_entries.remove(panel_ix);
800            cx.notify();
801
802            true
803        } else {
804            false
805        }
806    }
807
808    pub fn panels_len(&self) -> usize {
809        self.panel_entries.len()
810    }
811
812    pub fn has_agent_panel(&self, cx: &App) -> bool {
813        self.panel_entries
814            .iter()
815            .any(|entry| entry.panel.is_agent_panel(cx))
816    }
817
818    pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
819        if Some(panel_ix) != self.active_panel_index {
820            if let Some(active_panel) = self.active_panel_entry() {
821                active_panel.panel.set_active(false, window, cx);
822            }
823
824            self.active_panel_index = Some(panel_ix);
825            if let Some(active_panel) = self.active_panel_entry() {
826                active_panel.panel.set_active(true, window, cx);
827            }
828
829            cx.notify();
830        }
831    }
832
833    pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
834        let entry = self.visible_entry()?;
835        Some(&entry.panel)
836    }
837
838    pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
839        let panel_entry = self.active_panel_entry()?;
840        Some(&panel_entry.panel)
841    }
842
843    fn visible_entry(&self) -> Option<&PanelEntry> {
844        if self.is_open {
845            self.active_panel_entry()
846        } else {
847            None
848        }
849    }
850
851    pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option<Arc<dyn PanelHandle>> {
852        let entry = self.visible_entry()?;
853        if entry.panel.is_zoomed(window, cx) {
854            Some(entry.panel.clone())
855        } else {
856            None
857        }
858    }
859
860    pub fn active_panel_size(&self) -> Option<PanelSizeState> {
861        if self.is_open {
862            self.active_panel_entry().map(|entry| entry.size_state)
863        } else {
864            None
865        }
866    }
867
868    pub fn stored_panel_size(
869        &self,
870        panel: &dyn PanelHandle,
871        window: &Window,
872        cx: &App,
873    ) -> Option<Pixels> {
874        self.panel_entries
875            .iter()
876            .find(|entry| entry.panel.panel_id() == panel.panel_id())
877            .map(|entry| {
878                entry
879                    .size_state
880                    .size
881                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
882            })
883    }
884
885    pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
886        self.panel_entries
887            .iter()
888            .find(|entry| entry.panel.panel_id() == panel.panel_id())
889            .map(|entry| entry.size_state)
890    }
891
892    pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
893        if self.is_open {
894            self.active_panel_entry().map(|entry| {
895                entry
896                    .size_state
897                    .size
898                    .unwrap_or_else(|| entry.panel.default_size(window, cx))
899            })
900        } else {
901            None
902        }
903    }
904
905    pub fn set_panel_size_state(
906        &mut self,
907        panel: &dyn PanelHandle,
908        size_state: PanelSizeState,
909        cx: &mut Context<Self>,
910    ) -> bool {
911        if let Some(entry) = self
912            .panel_entries
913            .iter_mut()
914            .find(|entry| entry.panel.panel_id() == panel.panel_id())
915        {
916            entry.size_state = size_state;
917            cx.notify();
918            true
919        } else {
920            false
921        }
922    }
923
924    pub fn toggle_panel_flexible_size(
925        &mut self,
926        panel: &dyn PanelHandle,
927        current_size: Option<Pixels>,
928        current_flex: Option<f32>,
929        window: &mut Window,
930        cx: &mut Context<Self>,
931    ) {
932        let Some(entry) = self
933            .panel_entries
934            .iter_mut()
935            .find(|entry| entry.panel.panel_id() == panel.panel_id())
936        else {
937            return;
938        };
939        let currently_flexible = entry.panel.has_flexible_size(window, cx);
940        if currently_flexible {
941            entry.size_state.size = current_size;
942        } else {
943            entry.size_state.flex = current_flex;
944        }
945        let panel_key = entry.panel.panel_key();
946        let size_state = entry.size_state;
947        let workspace = self.workspace.clone();
948        entry
949            .panel
950            .set_flexible_size(!currently_flexible, window, cx);
951        entry.panel.size_state_changed(window, cx);
952        cx.defer(move |cx| {
953            if let Some(workspace) = workspace.upgrade() {
954                workspace.update(cx, |workspace, cx| {
955                    workspace.persist_panel_size_state(panel_key, size_state, cx);
956                });
957            }
958        });
959        cx.notify();
960    }
961
962    pub fn resize_active_panel(
963        &mut self,
964        size: Option<Pixels>,
965        flex: Option<f32>,
966        window: &mut Window,
967        cx: &mut Context<Self>,
968    ) {
969        if let Some(index) = self.active_panel_index
970            && let Some(entry) = self.panel_entries.get_mut(index)
971        {
972            let (panel_key, size_state) =
973                resize_panel_entry(self.position, entry, size, flex, window, cx);
974
975            let workspace = self.workspace.clone();
976            cx.defer(move |cx| {
977                if let Some(workspace) = workspace.upgrade() {
978                    workspace.update(cx, |workspace, cx| {
979                        workspace.persist_panel_size_state(panel_key, size_state, cx);
980                    });
981                }
982            });
983            cx.notify();
984        }
985    }
986
987    pub fn resize_all_panels(
988        &mut self,
989        size: Option<Pixels>,
990        flex: Option<f32>,
991        window: &mut Window,
992        cx: &mut Context<Self>,
993    ) {
994        let Some(active_panel_index) = self.active_panel_index else {
995            return;
996        };
997
998        let active_panel_uses_flexible_width = {
999            let Some(active_entry) = self.panel_entries.get(active_panel_index) else {
1000                return;
1001            };
1002            panel_uses_flexible_width(self.position, active_entry.panel.as_ref(), window, cx)
1003        };
1004        let mut size_states_to_persist = Vec::new();
1005        for entry in &mut self.panel_entries {
1006            if panel_uses_flexible_width(self.position, entry.panel.as_ref(), window, cx)
1007                == active_panel_uses_flexible_width
1008            {
1009                size_states_to_persist.push(resize_panel_entry(
1010                    self.position,
1011                    entry,
1012                    size,
1013                    flex,
1014                    window,
1015                    cx,
1016                ));
1017            }
1018        }
1019
1020        let workspace = self.workspace.clone();
1021        cx.defer(move |cx| {
1022            if let Some(workspace) = workspace.upgrade() {
1023                workspace.update(cx, |workspace, cx| {
1024                    for (panel_key, size_state) in size_states_to_persist {
1025                        workspace.persist_panel_size_state(panel_key, size_state, cx);
1026                    }
1027                });
1028            }
1029        });
1030
1031        cx.notify();
1032    }
1033
1034    pub fn toggle_action(&self) -> Box<dyn Action> {
1035        match self.position {
1036            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
1037            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
1038            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
1039        }
1040    }
1041
1042    fn dispatch_context() -> KeyContext {
1043        let mut dispatch_context = KeyContext::new_with_defaults();
1044        dispatch_context.add("Dock");
1045
1046        dispatch_context
1047    }
1048
1049    pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut Context<Self>) {
1050        let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
1051        let mut clamped = false;
1052        for entry in &mut self.panel_entries {
1053            let uses_flexible_width =
1054                panel_uses_flexible_width(self.position, entry.panel.as_ref(), window, cx);
1055            if uses_flexible_width {
1056                continue;
1057            }
1058
1059            let size = entry
1060                .size_state
1061                .size
1062                .unwrap_or_else(|| entry.panel.default_size(window, cx));
1063            if size > max_size {
1064                entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
1065                clamped = true;
1066            }
1067        }
1068        if clamped {
1069            cx.notify();
1070        }
1071    }
1072
1073    pub(crate) fn load_persisted_size_state(
1074        workspace: &Workspace,
1075        panel_key: &'static str,
1076        cx: &App,
1077    ) -> Option<PanelSizeState> {
1078        let workspace_id = workspace
1079            .database_id()
1080            .map(|id| i64::from(id).to_string())
1081            .or(workspace.session_id())?;
1082        let kvp = KeyValueStore::global(cx);
1083        let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
1084        scope
1085            .read(&format!("{workspace_id}:{panel_key}"))
1086            .log_err()
1087            .flatten()
1088            .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
1089    }
1090}
1091
1092impl Render for Dock {
1093    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1094        let dispatch_context = Self::dispatch_context();
1095        if let Some(entry) = self.visible_entry() {
1096            let position = self.position;
1097            let create_resize_handle = || {
1098                let handle = div()
1099                    .id("resize-handle")
1100                    .on_drag(DraggedDock(position), |dock, _, _, cx| {
1101                        cx.stop_propagation();
1102                        cx.new(|_| dock.clone())
1103                    })
1104                    .on_mouse_down(
1105                        MouseButton::Left,
1106                        cx.listener(|_, _: &MouseDownEvent, _, cx| {
1107                            cx.stop_propagation();
1108                        }),
1109                    )
1110                    .on_mouse_up(
1111                        MouseButton::Left,
1112                        cx.listener(|dock, e: &MouseUpEvent, window, cx| {
1113                            if e.click_count == 2 {
1114                                dock.resize_active_panel(None, None, window, cx);
1115                                dock.workspace
1116                                    .update(cx, |workspace, cx| {
1117                                        workspace.serialize_workspace(window, cx);
1118                                    })
1119                                    .ok();
1120                                cx.stop_propagation();
1121                            }
1122                        }),
1123                    )
1124                    .occlude();
1125                match self.position() {
1126                    DockPosition::Left => deferred(
1127                        handle
1128                            .absolute()
1129                            .right(-RESIZE_HANDLE_SIZE / 2.)
1130                            .top(px(0.))
1131                            .h_full()
1132                            .w(RESIZE_HANDLE_SIZE)
1133                            .cursor_col_resize(),
1134                    ),
1135                    DockPosition::Bottom => deferred(
1136                        handle
1137                            .absolute()
1138                            .top(-RESIZE_HANDLE_SIZE / 2.)
1139                            .left(px(0.))
1140                            .w_full()
1141                            .h(RESIZE_HANDLE_SIZE)
1142                            .cursor_row_resize(),
1143                    ),
1144                    DockPosition::Right => deferred(
1145                        handle
1146                            .absolute()
1147                            .top(px(0.))
1148                            .left(-RESIZE_HANDLE_SIZE / 2.)
1149                            .h_full()
1150                            .w(RESIZE_HANDLE_SIZE)
1151                            .cursor_col_resize(),
1152                    ),
1153                }
1154            };
1155
1156            div()
1157                .id("dock-panel")
1158                .key_context(dispatch_context)
1159                .track_focus(&self.focus_handle(cx))
1160                .focus_follows_mouse(self.focus_follows_mouse, cx)
1161                .flex()
1162                .bg(cx.theme().colors().panel_background)
1163                .border_color(cx.theme().colors().border)
1164                .overflow_hidden()
1165                .map(|this| match self.position().axis() {
1166                    // Width and height are always set on the workspace wrapper in
1167                    // render_dock, so fill whatever space the wrapper provides.
1168                    Axis::Horizontal => this.w_full().h_full().flex_row(),
1169                    Axis::Vertical => this.h_full().w_full().flex_col(),
1170                })
1171                .map(|this| match self.position() {
1172                    DockPosition::Left => this.border_r_1(),
1173                    DockPosition::Right => this.border_l_1(),
1174                    DockPosition::Bottom => this.border_t_1(),
1175                })
1176                .child(
1177                    div()
1178                        .map(|this| match self.position().axis() {
1179                            Axis::Horizontal => this.w_full().h_full(),
1180                            Axis::Vertical => this.h_full().w_full(),
1181                        })
1182                        .child(
1183                            entry
1184                                .panel
1185                                .to_any()
1186                                .cached(StyleRefinement::default().v_flex().size_full()),
1187                        ),
1188                )
1189                .when(self.resizable(cx), |this| {
1190                    this.child(create_resize_handle())
1191                })
1192        } else {
1193            div()
1194                .id("dock-panel")
1195                .key_context(dispatch_context)
1196                .track_focus(&self.focus_handle(cx))
1197        }
1198    }
1199}
1200
1201impl PanelButtons {
1202    pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
1203        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
1204        let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
1205        Self {
1206            dock,
1207            _settings_subscription: settings_subscription,
1208        }
1209    }
1210}
1211
1212impl Render for PanelButtons {
1213    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1214        let dock = self.dock.read(cx);
1215        let active_index = dock.active_panel_index;
1216        let is_open = dock.is_open;
1217        let dock_position = dock.position;
1218
1219        let (menu_anchor, menu_attach) = match dock.position {
1220            DockPosition::Left => (Anchor::BottomLeft, Anchor::TopLeft),
1221            DockPosition::Bottom | DockPosition::Right => (Anchor::BottomRight, Anchor::TopRight),
1222        };
1223
1224        let dock_entity = self.dock.clone();
1225        let workspace = dock.workspace.clone();
1226        let mut buttons: Vec<_> = dock
1227            .panel_entries
1228            .iter()
1229            .enumerate()
1230            .filter_map(|(i, entry)| {
1231                let icon = entry.panel.icon(window, cx)?;
1232                let icon_tooltip = entry
1233                    .panel
1234                    .icon_tooltip(window, cx)
1235                    .ok_or_else(|| {
1236                        anyhow::anyhow!("can't render a panel button without an icon tooltip")
1237                    })
1238                    .log_err()?;
1239                let name = entry.panel.persistent_name();
1240                let panel = entry.panel.clone();
1241                let supports_flexible = panel.supports_flexible_size(cx);
1242                let currently_flexible = panel.has_flexible_size(window, cx);
1243                let dock_for_menu = dock_entity.clone();
1244                let workspace_for_menu = workspace.clone();
1245
1246                let is_active_button = Some(i) == active_index && is_open;
1247                let (action, tooltip) = if is_active_button {
1248                    let action = dock.toggle_action();
1249
1250                    let tooltip: SharedString =
1251                        format!("Close {} Dock", dock.position.label()).into();
1252
1253                    (action, tooltip)
1254                } else {
1255                    let action = entry.panel.toggle_action(window, cx);
1256
1257                    (action, icon_tooltip.into())
1258                };
1259
1260                let focus_handle = dock.focus_handle(cx);
1261                let icon_label = entry.panel.icon_label(window, cx);
1262
1263                Some(
1264                    right_click_menu(name)
1265                        .menu(move |window, cx| {
1266                            const POSITIONS: [DockPosition; 3] = [
1267                                DockPosition::Left,
1268                                DockPosition::Right,
1269                                DockPosition::Bottom,
1270                            ];
1271
1272                            let panel_hide = panel.hide_button_setting(cx);
1273                            ContextMenu::build(window, cx, |mut menu, _, cx| {
1274                                let mut has_position_entries = false;
1275                                for position in POSITIONS {
1276                                    if panel.position_is_valid(position, cx) {
1277                                        let is_current = position == dock_position;
1278                                        let panel = panel.clone();
1279                                        menu = menu.toggleable_entry(
1280                                            format!("Dock {}", position.label()),
1281                                            is_current,
1282                                            IconPosition::Start,
1283                                            None,
1284                                            move |window, cx| {
1285                                                if !is_current {
1286                                                    panel.set_position(position, window, cx);
1287                                                }
1288                                            },
1289                                        );
1290                                        has_position_entries = true;
1291                                    }
1292                                }
1293                                if supports_flexible {
1294                                    if has_position_entries {
1295                                        menu = menu.separator();
1296                                    }
1297                                    let panel_for_flex = panel.clone();
1298                                    let dock_for_flex = dock_for_menu.clone();
1299                                    let workspace_for_flex = workspace_for_menu.clone();
1300                                    menu = menu.toggleable_entry(
1301                                        "Flex Width",
1302                                        currently_flexible,
1303                                        IconPosition::Start,
1304                                        None,
1305                                        move |window, cx| {
1306                                            if !currently_flexible {
1307                                                if let Some(ws) = workspace_for_flex.upgrade() {
1308                                                    ws.update(cx, |workspace, cx| {
1309                                                        workspace.toggle_dock_panel_flexible_size(
1310                                                            &dock_for_flex,
1311                                                            panel_for_flex.as_ref(),
1312                                                            window,
1313                                                            cx,
1314                                                        );
1315                                                    });
1316                                                }
1317                                            }
1318                                        },
1319                                    );
1320                                    let panel_for_fixed = panel.clone();
1321                                    let dock_for_fixed = dock_for_menu.clone();
1322                                    let workspace_for_fixed = workspace_for_menu.clone();
1323                                    menu = menu.toggleable_entry(
1324                                        "Fixed Width",
1325                                        !currently_flexible,
1326                                        IconPosition::Start,
1327                                        None,
1328                                        move |window, cx| {
1329                                            if currently_flexible {
1330                                                if let Some(ws) = workspace_for_fixed.upgrade() {
1331                                                    ws.update(cx, |workspace, cx| {
1332                                                        workspace.toggle_dock_panel_flexible_size(
1333                                                            &dock_for_fixed,
1334                                                            panel_for_fixed.as_ref(),
1335                                                            window,
1336                                                            cx,
1337                                                        );
1338                                                    });
1339                                                }
1340                                            }
1341                                        },
1342                                    );
1343                                }
1344                                if let Some(hide) = panel_hide {
1345                                    menu = crate::status_bar::add_hide_button_entry(
1346                                        menu.separator(),
1347                                        hide,
1348                                    );
1349                                }
1350                                menu
1351                            })
1352                        })
1353                        .anchor(menu_anchor)
1354                        .attach(menu_attach)
1355                        .trigger(move |is_active, _window, _cx| {
1356                            // Include active state in element ID to invalidate the cached
1357                            // tooltip when panel state changes (e.g., via keyboard shortcut)
1358                            let button = IconButton::new((name, is_active_button as u64), icon)
1359                                .icon_size(IconSize::Small)
1360                                .toggle_state(is_active_button)
1361                                .tab_index(0isize)
1362                                .aria_label(icon_tooltip)
1363                                .on_click({
1364                                    let action = action.boxed_clone();
1365                                    move |_, window, cx| {
1366                                        window.focus(&focus_handle, cx);
1367                                        window.dispatch_action(action.boxed_clone(), cx)
1368                                    }
1369                                })
1370                                .when(!is_active, |this| {
1371                                    this.tooltip(move |_window, cx| {
1372                                        Tooltip::for_action(tooltip.clone(), &*action, cx)
1373                                    })
1374                                });
1375
1376                            div().relative().child(button).when_some(
1377                                icon_label
1378                                    .clone()
1379                                    .filter(|_| !is_active_button)
1380                                    .and_then(|label| label.parse::<usize>().ok()),
1381                                |this, count| this.child(CountBadge::new(count)),
1382                            )
1383                        }),
1384                )
1385            })
1386            .collect();
1387
1388        if dock_position == DockPosition::Right {
1389            buttons.reverse();
1390        }
1391
1392        let has_buttons = !buttons.is_empty();
1393
1394        h_flex()
1395            .gap_1()
1396            .when(
1397                has_buttons
1398                    && (dock.position == DockPosition::Bottom
1399                        || dock.position == DockPosition::Right),
1400                |this| this.child(Divider::vertical().color(DividerColor::Border)),
1401            )
1402            .children(buttons)
1403            .when(has_buttons && dock.position == DockPosition::Left, |this| {
1404                this.child(Divider::vertical().color(DividerColor::Border))
1405            })
1406    }
1407}
1408
1409impl StatusItemView for PanelButtons {
1410    fn set_active_pane_item(
1411        &mut self,
1412        _active_pane_item: Option<&dyn crate::ItemHandle>,
1413        _window: &mut Window,
1414        _cx: &mut Context<Self>,
1415    ) {
1416        // Nothing to do, panel buttons don't depend on the active center item
1417    }
1418
1419    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
1420        // Panel buttons are hidden on a per-panel basis through each panel
1421        // button's own context menu.
1422        None
1423    }
1424}
1425
1426#[cfg(any(test, feature = "test-support"))]
1427pub mod test {
1428    use super::*;
1429    use gpui::{App, Context, Window, actions, div};
1430
1431    pub struct TestPanel {
1432        pub position: DockPosition,
1433        pub zoomed: bool,
1434        pub active: bool,
1435        pub focus_handle: FocusHandle,
1436        pub default_size: Pixels,
1437        pub flexible: bool,
1438        pub activation_priority: u32,
1439    }
1440    actions!(test_only, [ToggleTestPanel]);
1441
1442    impl EventEmitter<PanelEvent> for TestPanel {}
1443
1444    impl TestPanel {
1445        pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self {
1446            Self {
1447                position,
1448                zoomed: false,
1449                active: false,
1450                focus_handle: cx.focus_handle(),
1451                default_size: px(300.),
1452                flexible: false,
1453                activation_priority,
1454            }
1455        }
1456
1457        pub fn new_flexible(
1458            position: DockPosition,
1459            activation_priority: u32,
1460            cx: &mut App,
1461        ) -> Self {
1462            Self {
1463                flexible: true,
1464                ..Self::new(position, activation_priority, cx)
1465            }
1466        }
1467    }
1468
1469    impl Render for TestPanel {
1470        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1471            div().id("test").track_focus(&self.focus_handle(cx))
1472        }
1473    }
1474
1475    impl Panel for TestPanel {
1476        fn persistent_name() -> &'static str {
1477            "TestPanel"
1478        }
1479
1480        fn panel_key() -> &'static str {
1481            "TestPanel"
1482        }
1483
1484        fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
1485            self.position
1486        }
1487
1488        fn position_is_valid(&self, _: super::DockPosition) -> bool {
1489            true
1490        }
1491
1492        fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1493            self.position = position;
1494            cx.update_global::<SettingsStore, _>(|_, _| {});
1495        }
1496
1497        fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1498            self.default_size
1499        }
1500
1501        fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
1502            PanelSizeState {
1503                size: None,
1504                flex: None,
1505            }
1506        }
1507
1508        fn supports_flexible_size(&self) -> bool {
1509            self.flexible
1510        }
1511
1512        fn has_flexible_size(&self, _window: &Window, _: &App) -> bool {
1513            self.flexible
1514        }
1515
1516        fn set_flexible_size(
1517            &mut self,
1518            flexible: bool,
1519            _window: &mut Window,
1520            _cx: &mut Context<Self>,
1521        ) {
1522            self.flexible = flexible;
1523        }
1524
1525        fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
1526            None
1527        }
1528
1529        fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1530            None
1531        }
1532
1533        fn toggle_action(&self) -> Box<dyn Action> {
1534            ToggleTestPanel.boxed_clone()
1535        }
1536
1537        fn is_zoomed(&self, _window: &Window, _: &App) -> bool {
1538            self.zoomed
1539        }
1540
1541        fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1542            self.zoomed = zoomed;
1543        }
1544
1545        fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1546            self.active = active;
1547        }
1548
1549        fn activation_priority(&self) -> u32 {
1550            self.activation_priority
1551        }
1552    }
1553
1554    impl Focusable for TestPanel {
1555        fn focus_handle(&self, _cx: &App) -> FocusHandle {
1556            self.focus_handle.clone()
1557        }
1558    }
1559}
1560
Served at tenant.openagents/omega Member data and write actions are omitted.