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
619 let is_bottom_dock = bottom_dock_button.is_some();
620
621 let panel_style = dock_area.read(cx).panel_style;
622 let visible_panels = self.visible_panels(cx).collect::<Vec<_>>();
623
624 if visible_panels.len() == 1 && panel_style == PanelStyle::default() {
625 let panel = visible_panels.get(0).unwrap();
626
627 if !panel.visible(cx) {
628 return div().into_any_element();
629 }
630
631 let title_style = panel.title_style(cx);
632
633 return h_flex()
634 .justify_between()
635 .line_height(rems(1.0))
636 .h(px(30.))
637 .py_2()
638 .pl_3()
639 .pr_2()
640 .when(left_dock_button.is_some(), |this| this.pl_2())
641 .when(right_dock_button.is_some(), |this| this.pr_2())
642 .when_some(title_style, |this, theme| {
643 this.bg(theme.background).text_color(theme.foreground)
644 })
645 .when(
646 left_dock_button.is_some() || bottom_dock_button.is_some(),
647 |this| {
648 this.child(
649 h_flex()
650 .flex_shrink_0()
651 .mr_1()
652 .gap_1()
653 .children(left_dock_button)
654 .children(bottom_dock_button),
655 )
656 },
657 )
658 .child(
659 div()
660 .id("tab")
661 .flex_1()
662 .min_w_16()
663 .overflow_hidden()
664 .text_ellipsis()
665 .whitespace_nowrap()
666 .child(panel.title(window, cx))
667 .when(state.draggable, |this| {
668 this.on_drag(
669 DragPanel {
670 panel: panel.clone(),
671 tab_panel: view,
672 },
673 |drag, _, _, cx| {
674 cx.stop_propagation();
675 cx.new(|_| drag.clone())
676 },
677 )
678 }),
679 )
680 .children(panel.title_suffix(window, cx))
681 .child(
682 h_flex()
683 .flex_shrink_0()
684 .ml_1()
685 .gap_1()
686 .child(self.render_toolbar(&state, window, cx))
687 .children(right_dock_button),
688 )
689 .into_any_element();
690 }
691
692 let tabs_count = self.panels.len();
693
694 TabBar::new("tab-bar")
695 .tab_item_top_offset(-px(1.))
696 .track_scroll(&self.tab_bar_scroll_handle)
697 .when(
698 left_dock_button.is_some() || bottom_dock_button.is_some(),
699 |this| {
700 this.prefix(
701 h_flex()
702 .items_center()
703 .top_0()
704 .right(-px(1.))
706 .border_r_1()
707 .border_b_1()
708 .h_full()
709 .border_color(cx.theme().border)
710 .bg(cx.theme().tab_bar)
711 .px_2()
712 .children(left_dock_button)
713 .children(bottom_dock_button),
714 )
715 },
716 )
717 .children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
718 let mut active = state.active_panel.as_ref() == Some(panel);
719 let droppable = self.collapsed;
720
721 if !panel.visible(cx) {
722 return None;
723 }
724
725 if self.collapsed {
727 active = false;
728 }
729
730 Some(
731 Tab::default()
732 .map(|this| {
733 if let Some(tab_name) = panel.tab_name(cx) {
734 this.child(tab_name)
735 } else {
736 this.child(panel.title(window, cx))
737 }
738 })
739 .selected(active)
740 .on_click(cx.listener({
741 let is_collapsed = self.collapsed;
742 let dock_area = self.dock_area.clone();
743 move |view, _, window, cx| {
744 view.set_active_ix(ix, window, cx);
745
746 if is_bottom_dock && is_collapsed {
748 _ = dock_area.update(cx, |dock_area, cx| {
749 dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
750 });
751 }
752 }
753 }))
754 .when(!droppable, |this| {
755 this.when(state.draggable, |this| {
756 this.on_drag(
757 DragPanel::new(panel.clone(), view.clone()),
758 |drag, _, _, cx| {
759 cx.stop_propagation();
760 cx.new(|_| drag.clone())
761 },
762 )
763 })
764 .when(state.droppable, |this| {
765 this.drag_over::<DragPanel>(|this, _, _, cx| {
766 this.rounded_l_none()
767 .border_l_2()
768 .border_r_0()
769 .border_color(cx.theme().drag_border)
770 })
771 .on_drop(cx.listener(
772 move |this, drag: &DragPanel, window, cx| {
773 this.will_split_placement = None;
774 this.on_drop(drag, Some(ix), true, window, cx)
775 },
776 ))
777 })
778 }),
779 )
780 }))
781 .last_empty_space(
782 div()
784 .id("tab-bar-empty-space")
785 .h_full()
786 .flex_grow()
787 .min_w_16()
788 .when(state.droppable, |this| {
789 this.drag_over::<DragPanel>(|this, _, _, cx| {
790 this.bg(cx.theme().drop_target)
791 })
792 .on_drop(cx.listener(
793 move |this, drag: &DragPanel, window, cx| {
794 this.will_split_placement = None;
795
796 let ix = if drag.tab_panel == view {
797 Some(tabs_count - 1)
798 } else {
799 None
800 };
801
802 this.on_drop(drag, ix, false, window, cx)
803 },
804 ))
805 }),
806 )
807 .when(!self.collapsed, |this| {
808 this.suffix(
809 h_flex()
810 .items_center()
811 .top_0()
812 .right_0()
813 .border_l_1()
814 .border_b_1()
815 .h_full()
816 .border_color(cx.theme().border)
817 .bg(cx.theme().tab_bar)
818 .px_2()
819 .gap_1()
820 .children(
821 self.active_panel(cx)
822 .and_then(|panel| panel.title_suffix(window, cx)),
823 )
824 .child(self.render_toolbar(state, window, cx))
825 .when_some(right_dock_button, |this, btn| this.child(btn)),
826 )
827 })
828 .into_any_element()
829 }
830
831 fn render_active_panel(
832 &self,
833 state: &TabState,
834 _: &mut Window,
835 cx: &mut Context<Self>,
836 ) -> impl IntoElement {
837 if self.collapsed {
838 return Empty {}.into_any_element();
839 }
840
841 let Some(active_panel) = state.active_panel.as_ref() else {
842 return Empty {}.into_any_element();
843 };
844
845 let is_render_in_tabs = self.panels.len() > 1 && self.inner_padding(cx);
846
847 v_flex()
848 .id("active-panel")
849 .group("")
850 .flex_1()
851 .when(is_render_in_tabs, |this| this.pt_2())
852 .child(
853 div()
854 .id("tab-content")
855 .overflow_y_scroll()
856 .overflow_x_hidden()
857 .flex_1()
858 .child(
859 active_panel
860 .view()
861 .cached(StyleRefinement::default().absolute().size_full()),
862 ),
863 )
864 .when(state.droppable, |this| {
865 this.on_drag_move(cx.listener(Self::on_panel_drag_move))
866 .child(
867 div()
868 .invisible()
869 .absolute()
870 .bg(cx.theme().drop_target)
871 .map(|this| match self.will_split_placement {
872 Some(placement) => {
873 let size = relative(0.5);
874 match placement {
875 Placement::Left => this.left_0().top_0().bottom_0().w(size),
876 Placement::Right => {
877 this.right_0().top_0().bottom_0().w(size)
878 }
879 Placement::Top => this.top_0().left_0().right_0().h(size),
880 Placement::Bottom => {
881 this.bottom_0().left_0().right_0().h(size)
882 }
883 }
884 }
885 None => this.top_0().left_0().size_full(),
886 })
887 .group_drag_over::<DragPanel>("", |this| this.visible())
888 .on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
889 this.on_drop(drag, None, true, window, cx)
890 })),
891 )
892 })
893 .into_any_element()
894 }
895
896 fn on_panel_drag_move(
898 &mut self,
899 drag: &DragMoveEvent<DragPanel>,
900 _: &mut Window,
901 cx: &mut Context<Self>,
902 ) {
903 let bounds = drag.bounds;
904 let position = drag.event.position;
905
906 if position.x < bounds.left() + bounds.size.width * 0.35 {
908 self.will_split_placement = Some(Placement::Left);
909 } else if position.x > bounds.left() + bounds.size.width * 0.65 {
910 self.will_split_placement = Some(Placement::Right);
911 } else if position.y < bounds.top() + bounds.size.height * 0.35 {
912 self.will_split_placement = Some(Placement::Top);
913 } else if position.y > bounds.top() + bounds.size.height * 0.65 {
914 self.will_split_placement = Some(Placement::Bottom);
915 } else {
916 self.will_split_placement = None;
918 }
919 cx.notify()
920 }
921
922 fn on_drop(
926 &mut self,
927 drag: &DragPanel,
928 ix: Option<usize>,
929 active: bool,
930 window: &mut Window,
931 cx: &mut Context<Self>,
932 ) {
933 let panel = drag.panel.clone();
934 let is_same_tab = drag.tab_panel == cx.entity();
935
936 if is_same_tab && ix.is_none() {
938 if self.will_split_placement.is_none() {
939 return;
940 } else {
941 if self.panels.len() == 1 {
942 return;
943 }
944 }
945 }
946
947 if is_same_tab {
952 self.detach_panel(panel.clone(), window, cx);
953 } else {
954 let _ = drag.tab_panel.update(cx, |view, cx| {
955 view.detach_panel(panel.clone(), window, cx);
956 view.remove_self_if_empty(window, cx);
957 });
958 }
959
960 if let Some(placement) = self.will_split_placement {
962 self.split_panel(panel, placement, None, window, cx);
963 } else {
964 if let Some(ix) = ix {
965 self.insert_panel_at(panel, ix, window, cx)
966 } else {
967 self.add_panel_with_active(panel, active, window, cx)
968 }
969 }
970
971 self.remove_self_if_empty(window, cx);
972 cx.emit(PanelEvent::LayoutChanged);
973 }
974
975 fn split_panel(
977 &self,
978 panel: Arc<dyn PanelView>,
979 placement: Placement,
980 size: Option<Pixels>,
981 window: &mut Window,
982 cx: &mut Context<Self>,
983 ) {
984 let dock_area = self.dock_area.clone();
985 let new_tab_panel = cx.new(|cx| Self::new(None, dock_area.clone(), window, cx));
987 new_tab_panel.update(cx, |view, cx| {
988 view.add_panel(panel, window, cx);
989 });
990
991 let stack_panel = match self.stack_panel.as_ref().and_then(|panel| panel.upgrade()) {
992 Some(panel) => panel,
993 None => return,
994 };
995
996 let parent_axis = stack_panel.read(cx).axis;
997
998 let ix = stack_panel
999 .read(cx)
1000 .index_of_panel(Arc::new(cx.entity().clone()))
1001 .unwrap_or_default();
1002
1003 if parent_axis.is_vertical() && placement.is_vertical() {
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 if parent_axis.is_horizontal() && placement.is_horizontal() {
1016 stack_panel.update(cx, |view, cx| {
1017 view.insert_panel_at(
1018 Arc::new(new_tab_panel),
1019 ix,
1020 placement,
1021 size,
1022 dock_area.clone(),
1023 window,
1024 cx,
1025 );
1026 });
1027 } else {
1028 let tab_panel = cx.entity().clone();
1033
1034 let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 {
1036 stack_panel.update(cx, |view, cx| {
1037 view.remove_all_panels(window, cx);
1038 view.set_axis(placement.axis(), window, cx);
1039 });
1040 stack_panel.clone()
1041 } else {
1042 cx.new(|cx| {
1043 let mut panel = StackPanel::new(placement.axis(), window, cx);
1044 panel.parent = Some(stack_panel.downgrade());
1045 panel
1046 })
1047 };
1048
1049 new_stack_panel.update(cx, |view, cx| match placement {
1050 Placement::Left | Placement::Top => {
1051 view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
1052 view.add_panel(
1053 Arc::new(tab_panel.clone()),
1054 None,
1055 dock_area.clone(),
1056 window,
1057 cx,
1058 );
1059 }
1060 Placement::Right | Placement::Bottom => {
1061 view.add_panel(
1062 Arc::new(tab_panel.clone()),
1063 None,
1064 dock_area.clone(),
1065 window,
1066 cx,
1067 );
1068 view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx);
1069 }
1070 });
1071
1072 if stack_panel != new_stack_panel {
1073 stack_panel.update(cx, |view, cx| {
1074 view.replace_panel(
1075 Arc::new(tab_panel.clone()),
1076 new_stack_panel.clone(),
1077 window,
1078 cx,
1079 );
1080 });
1081 }
1082
1083 cx.spawn_in(window, async move |_, cx| {
1084 cx.update(|window, cx| {
1085 tab_panel.update(cx, |view, cx| view.remove_self_if_empty(window, cx))
1086 })
1087 })
1088 .detach()
1089 }
1090
1091 cx.emit(PanelEvent::LayoutChanged);
1092 }
1093
1094 fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
1095 if let Some(active_panel) = self.active_panel(cx) {
1096 active_panel.focus_handle(cx).focus(window);
1097 }
1098 }
1099
1100 fn on_action_toggle_zoom(
1101 &mut self,
1102 _: &ToggleZoom,
1103 window: &mut Window,
1104 cx: &mut Context<Self>,
1105 ) {
1106 if self.zoomable(cx).is_none() {
1107 return;
1108 }
1109
1110 if !self.zoomed {
1111 cx.emit(PanelEvent::ZoomIn)
1112 } else {
1113 cx.emit(PanelEvent::ZoomOut)
1114 }
1115 self.zoomed = !self.zoomed;
1116
1117 cx.spawn_in(window, {
1118 let zoomed = self.zoomed;
1119 async move |view, cx| {
1120 _ = cx.update(|window, cx| {
1121 _ = view.update(cx, |view, cx| {
1122 view.set_zoomed(zoomed, window, cx);
1123 });
1124 });
1125 }
1126 })
1127 .detach();
1128 }
1129
1130 fn on_action_close_panel(
1131 &mut self,
1132 _: &ClosePanel,
1133 window: &mut Window,
1134 cx: &mut Context<Self>,
1135 ) {
1136 if !self.closable(cx) {
1137 return;
1138 }
1139 if let Some(panel) = self.active_panel(cx) {
1140 self.remove_panel(panel, window, cx);
1141 }
1142
1143 if self.panels.is_empty() && self.in_tiles {
1146 let tab_panel = Arc::new(cx.entity());
1147 window.defer(cx, {
1148 let dock_area = self.dock_area.clone();
1149 move |window, cx| {
1150 _ = dock_area.update(cx, |this, cx| {
1151 this.remove_panel_from_all_docks(tab_panel, window, cx);
1152 });
1153 }
1154 });
1155 }
1156 }
1157
1158 fn bind_actions(&self, cx: &mut Context<Self>) -> Div {
1160 v_flex().when(!self.collapsed, |this| {
1161 this.on_action(cx.listener(Self::on_action_toggle_zoom))
1162 .on_action(cx.listener(Self::on_action_close_panel))
1163 })
1164 }
1165}
1166
1167impl Focusable for TabPanel {
1168 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1169 if let Some(active_panel) = self.active_panel(cx) {
1170 active_panel.focus_handle(cx)
1171 } else {
1172 self.focus_handle.clone()
1173 }
1174 }
1175}
1176impl EventEmitter<DismissEvent> for TabPanel {}
1177impl EventEmitter<PanelEvent> for TabPanel {}
1178impl Render for TabPanel {
1179 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
1180 let focus_handle = self.focus_handle(cx);
1181 let active_panel = self.active_panel(cx);
1182 let state = TabState {
1183 closable: self.closable(cx),
1184 draggable: self.draggable(cx),
1185 droppable: self.droppable(cx),
1186 zoomable: self.zoomable(cx),
1187 active_panel,
1188 };
1189
1190 self.bind_actions(cx)
1191 .id("tab-panel")
1192 .track_focus(&focus_handle)
1193 .tab_group()
1194 .size_full()
1195 .overflow_hidden()
1196 .bg(cx.theme().background)
1197 .child(self.render_title_bar(&state, window, cx))
1198 .child(self.render_active_panel(&state, window, cx))
1199 }
1200}