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