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