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