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