Skip to repository content1560 lines · 56.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:49:48.645Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
dock.rs
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