1use std::sync::Arc;
2
3use gpui::{
4 div, prelude::FluentBuilder, px, relative, rems, App, AppContext, Context, Corner,
5 DismissEvent, Div, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
6 InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
7 SharedString, StatefulInteractiveElement, StyleRefinement, Styled, WeakEntity, Window,
8};
9use rust_i18n::t;
10
11use crate::{
12 button::{Button, ButtonVariants as _},
13 dock::PanelInfo,
14 h_flex,
15 menu::{DropdownMenu, PopupMenu},
16 tab::{Tab, TabBar},
17 v_flex, ActiveTheme, AxisExt, IconName, Placement, Selectable, Sizable,
18};
19
20use super::{
21 ClosePanel, DockArea, DockPlacement, Panel, PanelControl, PanelEvent, PanelState, PanelStyle,
22 PanelView, StackPanel, ToggleZoom,
23};
24
25#[derive(Clone)]
26struct TabState {
27 closable: bool,
28 zoomable: Option<PanelControl>,
29 draggable: bool,
30 droppable: bool,
31 active_panel: Option<Arc<dyn PanelView>>,
32}
33
34#[derive(Clone)]
35pub(crate) struct DragPanel {
36 pub(crate) panel: Arc<dyn PanelView>,
37 pub(crate) tab_panel: Entity<TabPanel>,
38}
39
40impl DragPanel {
41 pub(crate) fn new(panel: Arc<dyn PanelView>, tab_panel: Entity<TabPanel>) -> Self {
42 Self { panel, tab_panel }
43 }
44}
45
46impl Render for DragPanel {
47 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
48 div()
49 .id("drag-panel")
50 .cursor_grab()
51 .py_1()
52 .px_3()
53 .w_24()
54 .overflow_hidden()
55 .whitespace_nowrap()
56 .border_1()
57 .border_color(cx.theme().border)
58 .rounded(cx.theme().radius)
59 .text_color(cx.theme().tab_foreground)
60 .bg(cx.theme().tab_active)
61 .opacity(0.75)
62 .child(self.panel.title(window, cx))
63 }
64}
65
66pub struct TabPanel {
67 focus_handle: FocusHandle,
68 dock_area: WeakEntity<DockArea>,
69 stack_panel: Option<WeakEntity<StackPanel>>,
71 pub(crate) panels: Vec<Arc<dyn PanelView>>,
72 pub(crate) active_ix: usize,
73 pub(crate) closable: bool,
78
79 tab_bar_scroll_handle: ScrollHandle,
80 zoomed: bool,
81 collapsed: bool,
82 will_split_placement: Option<Placement>,
84 in_tiles: bool,
86}
87
88impl Panel for TabPanel {
89 fn panel_name(&self) -> &'static str {
90 "TabPanel"
91 }
92
93 fn title(&self, window: &Window, cx: &App) -> gpui::AnyElement {
94 self.active_panel(cx)
95 .map(|panel| panel.title(window, cx))
96 .unwrap_or("Empty Tab".into_any_element())
97 }
98
99 fn closable(&self, cx: &App) -> bool {
100 if !self.closable {
101 return false;
102 }
103
104 if !self.draggable(cx) && !self.in_tiles {
107 return false;
108 }
109
110 self.active_panel(cx)
111 .map(|panel| panel.closable(cx))
112 .unwrap_or(false)
113 }
114
115 fn zoomable(&self, cx: &App) -> Option<PanelControl> {
116 self.active_panel(cx).and_then(|panel| panel.zoomable(cx))
117 }
118
119 fn visible(&self, cx: &App) -> bool {
120 self.visible_panels(cx).next().is_some()
121 }
122
123 fn dropdown_menu(&self, menu: PopupMenu, window: &Window, cx: &App) -> PopupMenu {
124 if let Some(panel) = self.active_panel(cx) {
125 panel.dropdown_menu(menu, window, cx)
126 } else {
127 menu
128 }
129 }
130
131 fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option<Vec<Button>> {
132 self.active_panel(cx)
133 .and_then(|panel| panel.toolbar_buttons(window, cx))
134 }
135
136 fn dump(&self, cx: &App) -> PanelState {
137 let mut state = PanelState::new(self);
138 for panel in self.panels.iter() {
139 state.add_child(panel.dump(cx));
140 state.info = PanelInfo::tabs(self.active_ix);
141 }
142 state
143 }
144
145 fn inner_padding(&self, cx: &App) -> bool {
146 self.active_panel(cx)
147 .map_or(true, |panel| panel.inner_padding(cx))
148 }
149}
150
151impl TabPanel {
152 pub fn new(
153 stack_panel: Option<WeakEntity<StackPanel>>,
154 dock_area: WeakEntity<DockArea>,
155 _: &mut Window,
156 cx: &mut Context<Self>,
157 ) -> Self {
158 Self {
159 focus_handle: cx.focus_handle(),
160 dock_area,
161 stack_panel,
162 panels: Vec::new(),
163 active_ix: 0,
164 tab_bar_scroll_handle: ScrollHandle::new(),
165 will_split_placement: None,
166 zoomed: false,
167 collapsed: false,
168 closable: true,
169 in_tiles: false,
170 }
171 }
172
173 pub(super) fn set_in_tiles(&mut self, in_tiles: bool) {
175 self.in_tiles = in_tiles;
176 }
177
178 pub(super) fn set_parent(&mut self, view: WeakEntity<StackPanel>) {
179 self.stack_panel = Some(view);
180 }
181
182 pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
184 let panel = self.panels.get(self.active_ix);
185
186 if let Some(panel) = panel {
187 if panel.visible(cx) {
188 Some(panel.clone())
189 } else {
190 self.visible_panels(cx).next()
192 }
193 } else {
194 None
195 }
196 }
197
198 fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
199 if ix == self.active_ix {
200 return;
201 }
202
203 let last_active_ix = self.active_ix;
204
205 self.active_ix = ix;
206 self.tab_bar_scroll_handle.scroll_to_item(ix);
207 self.focus_active_panel(window, cx);
208
209 cx.spawn_in(window, async move |view, cx| {
211 _ = cx.update(|window, cx| {
212 _ = view.update(cx, |view, cx| {
213 if let Some(last_active) = view.panels.get(last_active_ix) {
214 last_active.set_active(false, window, cx);
215 }
216 if let Some(active) = view.panels.get(view.active_ix) {
217 active.set_active(true, window, cx);
218 }
219 });
220 });
221 })
222 .detach();
223
224 cx.emit(PanelEvent::LayoutChanged);
225 cx.notify();
226 }
227
228 pub fn add_panel(
230 &mut self,
231 panel: Arc<dyn PanelView>,
232 window: &mut Window,
233 cx: &mut Context<Self>,
234 ) {
235 self.add_panel_with_active(panel, true, window, cx);
236 }
237
238 fn add_panel_with_active(
239 &mut self,
240 panel: Arc<dyn PanelView>,
241 active: bool,
242 window: &mut Window,
243 cx: &mut Context<Self>,
244 ) {
245 assert_ne!(
246 panel.panel_name(cx),
247 "StackPanel",
248 "can not allows add `StackPanel` to `TabPanel`"
249 );
250
251 if self
252 .panels
253 .iter()
254 .any(|p| p.view().entity_id() == panel.view().entity_id())
255 {
256 return;
257 }
258
259 panel.on_added_to(cx.entity().downgrade(), window, cx);
260 self.panels.push(panel);
261 if active {
263 self.set_active_ix(self.panels.len() - 1, window, cx);
264 }
265 cx.emit(PanelEvent::LayoutChanged);
266 cx.notify();
267 }
268
269 pub fn add_panel_at(
271 &mut self,
272 panel: Arc<dyn PanelView>,
273 placement: Placement,
274 size: Option<Pixels>,
275 window: &mut Window,
276 cx: &mut Context<Self>,
277 ) {
278 cx.spawn_in(window, async move |view, cx| {
279 cx.update(|window, cx| {
280 view.update(cx, |view, cx| {
281 view.will_split_placement = Some(placement);
282 view.split_panel(panel, placement, size, window, cx)
283 })
284 .ok()
285 })
286 .ok()
287 })
288 .detach();
289 cx.emit(PanelEvent::LayoutChanged);
290 cx.notify();
291 }
292
293 fn insert_panel_at(
294 &mut self,
295 panel: Arc<dyn PanelView>,
296 ix: usize,
297 window: &mut Window,
298 cx: &mut Context<Self>,
299 ) {
300 if self
301 .panels
302 .iter()
303 .any(|p| p.view().entity_id() == panel.view().entity_id())
304 {
305 return;
306 }
307
308 panel.on_added_to(cx.entity().downgrade(), window, cx);
309 self.panels.insert(ix, panel);
310 self.set_active_ix(ix, window, cx);
311 cx.emit(PanelEvent::LayoutChanged);
312 cx.notify();
313 }
314
315 pub fn remove_panel(
317 &mut self,
318 panel: Arc<dyn PanelView>,
319 window: &mut Window,
320 cx: &mut Context<Self>,
321 ) {
322 self.detach_panel(panel, window, cx);
323 self.remove_self_if_empty(window, cx);
324 cx.emit(PanelEvent::ZoomOut);
325 cx.emit(PanelEvent::LayoutChanged);
326 }
327
328 fn detach_panel(
329 &mut self,
330 panel: Arc<dyn PanelView>,
331 window: &mut Window,
332 cx: &mut Context<Self>,
333 ) {
334 panel.on_removed(window, cx);
335 let panel_view = panel.view();
336 self.panels.retain(|p| p.view() != panel_view);
337 if self.active_ix >= self.panels.len() {
338 self.set_active_ix(self.panels.len().saturating_sub(1), window, cx)
339 }
340 }
341
342 fn remove_self_if_empty(&self, window: &mut Window, cx: &mut Context<Self>) {
344 if !self.panels.is_empty() {
345 return;
346 }
347
348 let tab_view = cx.entity().clone();
349 if let Some(stack_panel) = self.stack_panel.as_ref() {
350 _ = stack_panel.update(cx, |view, cx| {
351 view.remove_panel(Arc::new(tab_view), window, cx);
352 });
353 }
354 }
355
356 pub(super) fn set_collapsed(
357 &mut self,
358 collapsed: bool,
359 window: &mut Window,
360 cx: &mut Context<Self>,
361 ) {
362 self.collapsed = collapsed;
363 if let Some(panel) = self.panels.get(self.active_ix) {
364 panel.set_active(!collapsed, window, cx);
365 }
366 cx.notify();
367 }
368
369 fn is_locked(&self, cx: &App) -> bool {
370 let Some(dock_area) = self.dock_area.upgrade() else {
371 return true;
372 };
373
374 if dock_area.read(cx).is_locked() {
375 return true;
376 }
377
378 if self.zoomed {
379 return true;
380 }
381
382 self.stack_panel.is_none()
383 }
384
385 fn is_last_panel(&self, cx: &App) -> bool {
387 if let Some(parent) = &self.stack_panel {
388 if let Some(stack_panel) = parent.upgrade() {
389 if !stack_panel.read(cx).is_last_panel(cx) {
390 return false;
391 }
392 }
393 }
394
395 self.panels.len() <= 1
396 }
397
398 fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
400 self.panels.iter().filter_map(|panel| {
401 if panel.visible(cx) {
402 Some(panel.clone())
403 } else {
404 None
405 }
406 })
407 }
408
409 fn draggable(&self, cx: &App) -> bool {
413 !self.is_locked(cx) && !self.is_last_panel(cx)
414 }
415
416 fn droppable(&self, cx: &App) -> bool {
420 !self.is_locked(cx)
421 }
422
423 fn render_toolbar(
424 &self,
425 state: &TabState,
426 window: &mut Window,
427 cx: &mut Context<Self>,
428 ) -> impl IntoElement {
429 if self.collapsed {
430 return div();
431 }
432
433 let zoomed = self.zoomed;
434 let view = cx.entity().clone();
435 let zoomable_toolbar_visible = state.zoomable.map_or(false, |v| v.toolbar_visible());
436
437 h_flex()
438 .gap_1()
439 .occlude()
440 .when_some(self.toolbar_buttons(window, cx), |this, buttons| {
441 this.children(
442 buttons
443 .into_iter()
444 .map(|btn| btn.xsmall().ghost().tab_stop(false)),
445 )
446 })
447 .map(|this| {
448 let value = if zoomed {
449 Some(("zoom-out", IconName::Minimize, t!("Dock.Zoom Out")))
450 } else if zoomable_toolbar_visible {
451 Some(("zoom-in", IconName::Maximize, t!("Dock.Zoom In")))
452 } else {
453 None
454 };
455
456 if let Some((id, icon, tooltip)) = value {
457 this.child(
458 Button::new(id)
459 .icon(icon)
460 .xsmall()
461 .ghost()
462 .tab_stop(false)
463 .tooltip_with_action(tooltip, &ToggleZoom, None)
464 .when(zoomed, |this| this.selected(true))
465 .on_click(cx.listener(|view, _, window, cx| {
466 view.on_action_toggle_zoom(&ToggleZoom, window, cx)
467 })),
468 )
469 } else {
470 this
471 }
472 })
473 .child(
474 Button::new("menu")
475 .icon(IconName::Ellipsis)
476 .xsmall()
477 .ghost()
478 .tab_stop(false)
479 .dropdown_menu({
480 let zoomable = state.zoomable.map_or(false, |v| v.menu_visible());
481 let closable = state.closable;
482
483 move |this, window, cx| {
484 view.read(cx)
485 .dropdown_menu(this, window, cx)
486 .separator()
487 .menu_with_disabled(
488 if zoomed {
489 t!("Dock.Zoom Out")
490 } else {
491 t!("Dock.Zoom In")
492 },
493 Box::new(ToggleZoom),
494 !zoomable,
495 )
496 .when(closable, |this| {
497 this.separator()
498 .menu(t!("Dock.Close"), Box::new(ClosePanel))
499 })
500 }
501 })
502 .anchor(Corner::TopRight),
503 )
504 }
505
506 fn render_dock_toggle_button(
507 &self,
508 placement: DockPlacement,
509 _: &mut Window,
510 cx: &mut Context<Self>,
511 ) -> Option<impl IntoElement> {
512 if self.zoomed {
513 return None;
514 }
515
516 let dock_area = self.dock_area.upgrade()?.read(cx);
517 if !dock_area.toggle_button_visible {
518 return None;
519 }
520 if !dock_area.is_dock_collapsible(placement, cx) {
521 return None;
522 }
523
524 let view_entity_id = cx.entity().entity_id();
525 let toggle_button_panels = dock_area.toggle_button_panels;
526
527 if !match placement {
529 DockPlacement::Left => {
530 dock_area.left_dock.is_some() && toggle_button_panels.left == Some(view_entity_id)
531 }
532 DockPlacement::Right => {
533 dock_area.right_dock.is_some() && toggle_button_panels.right == Some(view_entity_id)
534 }
535 DockPlacement::Bottom => {
536 dock_area.bottom_dock.is_some()
537 && toggle_button_panels.bottom == Some(view_entity_id)
538 }
539 DockPlacement::Center => unreachable!(),
540 } {
541 return None;
542 }
543
544 let is_open = dock_area.is_dock_open(placement, cx);
545
546 let icon = match placement {
547 DockPlacement::Left => {
548 if is_open {
549 IconName::PanelLeft
550 } else {
551 IconName::PanelLeftOpen
552 }
553 }
554 DockPlacement::Right => {
555 if is_open {
556 IconName::PanelRight
557 } else {
558 IconName::PanelRightOpen
559 }
560 }
561 DockPlacement::Bottom => {
562 if is_open {
563 IconName::PanelBottom
564 } else {
565 IconName::PanelBottomOpen
566 }
567 }
568 DockPlacement::Center => unreachable!(),
569 };
570
571 Some(
572 Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
573 .icon(icon)
574 .xsmall()
575 .ghost()
576 .tab_stop(false)
577 .tooltip(match is_open {
578 true => t!("Dock.Collapse"),
579 false => t!("Dock.Expand"),
580 })
581 .on_click(cx.listener({
582 let dock_area = self.dock_area.clone();
583 move |_, _, window, cx| {
584 _ = dock_area.update(cx, |dock_area, cx| {
585 dock_area.toggle_dock(placement, window, cx);
586 });
587 }
588 })),
589 )
590 }
591
592 fn render_title_bar(
593 &self,
594 state: &TabState,
595 window: &mut Window,
596 cx: &mut Context<Self>,
597 ) -> impl IntoElement {
598 let view = cx.entity().clone();
599
600 let Some(dock_area) = self.dock_area.upgrade() else {
601 return div().into_any_element();
602 };
603 let panel_style = dock_area.read(cx).panel_style;
604
605 let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
606 let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
607 let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
608
609 let is_bottom_dock = bottom_dock_button.is_some();
610
611 if self.panels.len() == 1 && panel_style == PanelStyle::Default {
612 let panel = self.panels.get(0).unwrap();
613
614 if !panel.visible(cx) {
615 return div().into_any_element();
616 }
617
618 let title_style = panel.title_style(cx);
619
620 return h_flex()
621 .justify_between()
622 .line_height(rems(1.0))
623 .h(px(30.))
624 .py_2()
625 .pl_3()
626 .pr_2()
627 .when(left_dock_button.is_some(), |this| this.pl_2())
628 .when(right_dock_button.is_some(), |this| this.pr_2())
629 .when_some(title_style, |this, theme| {
630 this.bg(theme.background).text_color(theme.foreground)
631 })
632 .when(
633 left_dock_button.is_some() || bottom_dock_button.is_some(),
634 |this| {
635 this.child(
636 h_flex()
637 .flex_shrink_0()
638 .mr_1()
639 .gap_1()
640 .children(left_dock_button)
641 .children(bottom_dock_button),
642 )
643 },
644 )
645 .child(
646 div()
647 .id("tab")
648 .flex_1()
649 .min_w_16()
650 .overflow_hidden()
651 .text_ellipsis()
652 .whitespace_nowrap()
653 .child(panel.title(window, cx))
654 .when(state.draggable, |this| {
655 this.on_drag(
656 DragPanel {
657 panel: panel.clone(),
658 tab_panel: view,
659 },
660 |drag, _, _, cx| {
661 cx.stop_propagation();
662 cx.new(|_| drag.clone())
663 },
664 )
665 }),
666 )
667 .children(panel.title_suffix(window, cx))
668 .child(
669 h_flex()
670 .flex_shrink_0()
671 .ml_1()
672 .gap_1()
673 .child(self.render_toolbar(&state, window, cx))
674 .children(right_dock_button),
675 )
676 .into_any_element();
677 }
678
679 let tabs_count = self.panels.len();
680
681 TabBar::new("tab-bar")
682 .tab_item_top_offset(-px(1.))
683 .track_scroll(&self.tab_bar_scroll_handle)
684 .when(
685 left_dock_button.is_some() || bottom_dock_button.is_some(),
686 |this| {
687 this.prefix(
688 h_flex()
689 .items_center()
690 .top_0()
691 .right(-px(1.))
693 .border_r_1()
694 .border_b_1()
695 .h_full()
696 .border_color(cx.theme().border)
697 .bg(cx.theme().tab_bar)
698 .px_2()
699 .children(left_dock_button)
700 .children(bottom_dock_button),
701 )
702 },
703 )
704 .children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
705 let mut active = state.active_panel.as_ref() == Some(panel);
706 let droppable = self.collapsed;
707
708 if !panel.visible(cx) {
709 return None;
710 }
711
712 if self.collapsed {
714 active = false;
715 }
716
717 Some(
718 Tab::default()
719 .map(|this| {
720 if let Some(tab_name) = panel.tab_name(cx) {
721 this.child(tab_name)
722 } else {
723 this.child(panel.title(window, cx))
724 }
725 })
726 .selected(active)
727 .on_click(cx.listener({
728 let is_collapsed = self.collapsed;
729 let dock_area = self.dock_area.clone();
730 move |view, _, window, cx| {
731 view.set_active_ix(ix, window, cx);
732
733 if is_bottom_dock && is_collapsed {
735 _ = dock_area.update(cx, |dock_area, cx| {
736 dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
737 });
738 }
739 }
740 }))
741 .when(!droppable, |this| {
742 this.when(state.draggable, |this| {
743 this.on_drag(
744 DragPanel::new(panel.clone(), view.clone()),
745 |drag, _, _, cx| {
746 cx.stop_propagation();
747 cx.new(|_| drag.clone())
748 },
749 )
750 })
751 .when(state.droppable, |this| {
752 this.drag_over::<DragPanel>(|this, _, _, cx| {
753 this.rounded_l_none()
754 .border_l_2()
755 .border_r_0()
756 .border_color(cx.theme().drag_border)
757 })
758 .on_drop(cx.listener(
759 move |this, drag: &DragPanel, window, cx| {
760 this.will_split_placement = None;
761 this.on_drop(drag, Some(ix), true, window, cx)
762 },
763 ))
764 })
765 }),
766 )
767 }))
768 .last_empty_space(
769 div()
771 .id("tab-bar-empty-space")
772 .h_full()
773 .flex_grow()
774 .min_w_16()
775 .when(state.droppable, |this| {
776 this.drag_over::<DragPanel>(|this, _, _, cx| {
777 this.bg(cx.theme().drop_target)
778 })
779 .on_drop(cx.listener(
780 move |this, drag: &DragPanel, window, cx| {
781 this.will_split_placement = None;
782
783 let ix = if drag.tab_panel == view {
784 Some(tabs_count - 1)
785 } else {
786 None
787 };
788
789 this.on_drop(drag, ix, false, window, cx)
790 },
791 ))
792 }),
793 )
794 .when(!self.collapsed, |this| {
795 this.suffix(
796 h_flex()
797 .items_center()
798 .top_0()
799 .right_0()
800 .border_l_1()
801 .border_b_1()
802 .h_full()
803 .border_color(cx.theme().border)
804 .bg(cx.theme().tab_bar)
805 .px_2()
806 .gap_1()
807 .children(
808 self.active_panel(cx)
809 .and_then(|panel| panel.title_suffix(window, cx)),
810 )
811 .child(self.render_toolbar(state, window, cx))
812 .when_some(right_dock_button, |this, btn| this.child(btn)),
813 )
814 })
815 .into_any_element()
816 }
817
818 fn render_active_panel(
819 &self,
820 state: &TabState,
821 _: &mut Window,
822 cx: &mut Context<Self>,
823 ) -> impl IntoElement {
824 if self.collapsed {
825 return Empty {}.into_any_element();
826 }
827
828 let Some(active_panel) = state.active_panel.as_ref() else {
829 return Empty {}.into_any_element();
830 };
831
832 let is_render_in_tabs = self.panels.len() > 1 && self.inner_padding(cx);
833
834 v_flex()
835 .id("active-panel")
836 .group("")
837 .flex_1()
838 .when(is_render_in_tabs, |this| this.pt_2())
839 .child(
840 div()
841 .id("tab-content")
842 .overflow_y_scroll()
843 .overflow_x_hidden()
844 .flex_1()
845 .child(
846 active_panel
847 .view()
848 .cached(StyleRefinement::default().absolute().size_full()),
849 ),
850 )
851 .when(state.droppable, |this| {
852 this.on_drag_move(cx.listener(Self::on_panel_drag_move))
853 .child(
854 div()
855 .invisible()
856 .absolute()
857 .bg(cx.theme().drop_target)
858 .map(|this| match self.will_split_placement {
859 Some(placement) => {
860 let size = relative(0.5);
861 match placement {
862 Placement::Left => this.left_0().top_0().bottom_0().w(size),
863 Placement::Right => {
864 this.right_0().top_0().bottom_0().w(size)
865 }
866 Placement::Top => this.top_0().left_0().right_0().h(size),
867 Placement::Bottom => {
868 this.bottom_0().left_0().right_0().h(size)
869 }
870 }
871 }
872 None => this.top_0().left_0().size_full(),
873 })
874 .group_drag_over::<DragPanel>("", |this| this.visible())
875 .on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
876 this.on_drop(drag, None, true, window, cx)
877 })),
878 )
879 })
880 .into_any_element()
881 }
882
883 fn on_panel_drag_move(
885 &mut self,
886 drag: &DragMoveEvent<DragPanel>,
887 _: &mut Window,
888 cx: &mut Context<Self>,
889 ) {
890 let bounds = drag.bounds;
891 let position = drag.event.position;
892
893 if position.x < bounds.left() + bounds.size.width * 0.35 {
895 self.will_split_placement = Some(Placement::Left);
896 } else if position.x > bounds.left() + bounds.size.width * 0.65 {
897 self.will_split_placement = Some(Placement::Right);
898 } else if position.y < bounds.top() + bounds.size.height * 0.35 {
899 self.will_split_placement = Some(Placement::Top);
900 } else if position.y > bounds.top() + bounds.size.height * 0.65 {
901 self.will_split_placement = Some(Placement::Bottom);
902 } else {
903 self.will_split_placement = None;
905 }
906 cx.notify()
907 }
908
909 fn on_drop(
913 &mut self,
914 drag: &DragPanel,
915 ix: Option<usize>,
916 active: bool,
917 window: &mut Window,
918 cx: &mut Context<Self>,
919 ) {
920 let panel = drag.panel.clone();
921 let is_same_tab = drag.tab_panel == cx.entity();
922
923 if is_same_tab && ix.is_none() {
925 if self.will_split_placement.is_none() {
926 return;
927 } else {
928 if self.panels.len() == 1 {
929 return;
930 }
931 }
932 }
933
934 if is_same_tab {
939 self.detach_panel(panel.clone(), window, cx);
940 } else {
941 let _ = drag.tab_panel.update(cx, |view, cx| {
942 view.detach_panel(panel.clone(), window, cx);
943 view.remove_self_if_empty(window, cx);
944 });
945 }
946
947 if let Some(placement) = self.will_split_placement {
949 self.split_panel(panel, placement, None, window, cx);
950 } else {
951 if let Some(ix) = ix {
952 self.insert_panel_at(panel, ix, window, cx)
953 } else {
954 self.add_panel_with_active(panel, active, window, cx)
955 }
956 }
957
958 self.remove_self_if_empty(window, cx);
959 cx.emit(PanelEvent::LayoutChanged);
960 }
961
962 fn split_panel(
964 &self,
965 panel: Arc<dyn PanelView>,
966 placement: Placement,
967 size: Option<Pixels>,
968 window: &mut Window,
969 cx: &mut Context<Self>,
970 ) {
971 let dock_area = self.dock_area.clone();
972 let new_tab_panel = cx.new(|cx| Self::new(None, dock_area.clone(), window, cx));
974 new_tab_panel.update(cx, |view, cx| {
975 view.add_panel(panel, window, cx);
976 });
977
978 let stack_panel = match self.stack_panel.as_ref().and_then(|panel| panel.upgrade()) {
979 Some(panel) => panel,
980 None => return,
981 };
982
983 let parent_axis = stack_panel.read(cx).axis;
984
985 let ix = stack_panel
986 .read(cx)
987 .index_of_panel(Arc::new(cx.entity().clone()))
988 .unwrap_or_default();
989
990 if parent_axis.is_vertical() && placement.is_vertical() {
991 stack_panel.update(cx, |view, cx| {
992 view.insert_panel_at(
993 Arc::new(new_tab_panel),
994 ix,
995 placement,
996 size,
997 dock_area.clone(),
998 window,
999 cx,
1000 );
1001 });
1002 } else if parent_axis.is_horizontal() && placement.is_horizontal() {
1003 stack_panel.update(cx, |view, cx| {
1004 view.insert_panel_at(
1005 Arc::new(new_tab_panel),
1006 ix,
1007 placement,
1008 size,
1009 dock_area.clone(),
1010 window,
1011 cx,
1012 );
1013 });
1014 } else {
1015 let tab_panel = cx.entity().clone();
1020
1021 let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 {
1023 stack_panel.update(cx, |view, cx| {
1024 view.remove_all_panels(window, cx);
1025 view.set_axis(placement.axis(), window, cx);
1026 });
1027 stack_panel.clone()
1028 } else {
1029 cx.new(|cx| {
1030 let mut panel = StackPanel::new(placement.axis(), window, cx);
1031 panel.parent = Some(stack_panel.downgrade());
1032 panel
1033 })
1034 };
1035
1036 new_stack_panel.update(cx, |view, cx| match placement {
1037 Placement::Left | Placement::Top => {
1038 view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
1039 view.add_panel(
1040 Arc::new(tab_panel.clone()),
1041 None,
1042 dock_area.clone(),
1043 window,
1044 cx,
1045 );
1046 }
1047 Placement::Right | Placement::Bottom => {
1048 view.add_panel(
1049 Arc::new(tab_panel.clone()),
1050 None,
1051 dock_area.clone(),
1052 window,
1053 cx,
1054 );
1055 view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
1056 }
1057 });
1058
1059 if stack_panel != new_stack_panel {
1060 stack_panel.update(cx, |view, cx| {
1061 view.replace_panel(
1062 Arc::new(tab_panel.clone()),
1063 new_stack_panel.clone(),
1064 window,
1065 cx,
1066 );
1067 });
1068 }
1069
1070 cx.spawn_in(window, async move |_, cx| {
1071 cx.update(|window, cx| {
1072 tab_panel.update(cx, |view, cx| view.remove_self_if_empty(window, cx))
1073 })
1074 })
1075 .detach()
1076 }
1077
1078 cx.emit(PanelEvent::LayoutChanged);
1079 }
1080
1081 fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
1082 if let Some(active_panel) = self.active_panel(cx) {
1083 active_panel.focus_handle(cx).focus(window);
1084 }
1085 }
1086
1087 fn on_action_toggle_zoom(
1088 &mut self,
1089 _: &ToggleZoom,
1090 window: &mut Window,
1091 cx: &mut Context<Self>,
1092 ) {
1093 if self.zoomable(cx).is_none() {
1094 return;
1095 }
1096
1097 if !self.zoomed {
1098 cx.emit(PanelEvent::ZoomIn)
1099 } else {
1100 cx.emit(PanelEvent::ZoomOut)
1101 }
1102 self.zoomed = !self.zoomed;
1103
1104 cx.spawn_in(window, {
1105 let zoomed = self.zoomed;
1106 async move |view, cx| {
1107 _ = cx.update(|window, cx| {
1108 _ = view.update(cx, |view, cx| {
1109 view.set_zoomed(zoomed, window, cx);
1110 });
1111 });
1112 }
1113 })
1114 .detach();
1115 }
1116
1117 fn on_action_close_panel(
1118 &mut self,
1119 _: &ClosePanel,
1120 window: &mut Window,
1121 cx: &mut Context<Self>,
1122 ) {
1123 if !self.closable(cx) {
1124 return;
1125 }
1126 if let Some(panel) = self.active_panel(cx) {
1127 self.remove_panel(panel, window, cx);
1128 }
1129
1130 if self.panels.is_empty() && self.in_tiles {
1133 let tab_panel = Arc::new(cx.entity());
1134 window.defer(cx, {
1135 let dock_area = self.dock_area.clone();
1136 move |window, cx| {
1137 _ = dock_area.update(cx, |this, cx| {
1138 this.remove_panel_from_all_docks(tab_panel, window, cx);
1139 });
1140 }
1141 });
1142 }
1143 }
1144
1145 fn bind_actions(&self, cx: &mut Context<Self>) -> Div {
1147 v_flex().when(!self.collapsed, |this| {
1148 this.on_action(cx.listener(Self::on_action_toggle_zoom))
1149 .on_action(cx.listener(Self::on_action_close_panel))
1150 })
1151 }
1152}
1153
1154impl Focusable for TabPanel {
1155 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1156 if let Some(active_panel) = self.active_panel(cx) {
1157 active_panel.focus_handle(cx)
1158 } else {
1159 self.focus_handle.clone()
1160 }
1161 }
1162}
1163impl EventEmitter<DismissEvent> for TabPanel {}
1164impl EventEmitter<PanelEvent> for TabPanel {}
1165impl Render for TabPanel {
1166 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
1167 let focus_handle = self.focus_handle(cx);
1168 let active_panel = self.active_panel(cx);
1169 let state = TabState {
1170 closable: self.closable(cx),
1171 draggable: self.draggable(cx),
1172 droppable: self.droppable(cx),
1173 zoomable: self.zoomable(cx),
1174 active_panel,
1175 };
1176
1177 self.bind_actions(cx)
1178 .id("tab-panel")
1179 .track_focus(&focus_handle)
1180 .tab_group()
1181 .size_full()
1182 .overflow_hidden()
1183 .bg(cx.theme().background)
1184 .child(self.render_title_bar(&state, window, cx))
1185 .child(self.render_active_panel(&state, window, cx))
1186 }
1187}