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