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