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