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