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