gpui_component/dock/
tab_panel.rs

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    /// The stock_panel can be None, if is None, that means the panels can't be split or move
71    stack_panel: Option<WeakEntity<StackPanel>>,
72    pub(crate) panels: Vec<Arc<dyn PanelView>>,
73    pub(crate) active_ix: usize,
74    /// If this is true, the Panel closable will follow the active panel's closable,
75    /// otherwise this TabPanel will not able to close
76    ///
77    /// This is used for Dock to limit the last TabPanel not able to close, see [`super::Dock::new`].
78    pub(crate) closable: bool,
79
80    tab_bar_scroll_handle: ScrollHandle,
81    zoomed: bool,
82    collapsed: bool,
83    /// When drag move, will get the placement of the panel to be split
84    will_split_placement: Option<Placement>,
85    /// Is TabPanel used in Tiles.
86    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        // 1. When is the final panel in the dock, it will not able to close.
106        // 2. When is in the Tiles, it will always able to close (by active panel state).
107        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    /// Mark the TabPanel as being used in Tiles.
184    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    /// Return current active_panel View
193    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                // Return the first visible panel
201                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        // Sync the active state to all panels
220        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    /// Add a panel to the end of the tabs
239    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        // set the active panel to the new panel
272        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    /// Add panel to try to split
280    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    /// Remove a panel from the tab panel
326    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    /// Check to remove self from the parent StackPanel, if there is no panel left
353    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    /// Return true if self or parent only have last panel.
396    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    /// Return all visible panels
409    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    /// Return true if the tab panel is draggable.
420    ///
421    /// E.g. if the parent and self only have one panel, it is not draggable.
422    fn draggable(&self, cx: &App) -> bool {
423        !self.is_locked(cx) && !self.is_last_panel(cx)
424    }
425
426    /// Return true if the tab panel is droppable.
427    ///
428    /// E.g. if the tab panel is locked, it is not droppable.
429    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        // Check if current TabPanel's entity_id matches the one stored in DockArea for this placement
539        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 -1 for avoid border overlap with the first tab
705                            .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                // Always not show active tab style, if the panel is collapsed
726                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                                // Open dock if clicked on the collapsed bottom dock
747                                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                // empty space to allow move to last tab right
783                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    /// Calculate the split direction based on the current mouse position
897    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        // Check the mouse position to determine the split direction
907        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            // center to merge into the current tab
917            self.will_split_placement = None;
918        }
919        cx.notify()
920    }
921
922    /// Handle the drop event when dragging a panel
923    ///
924    /// - `active` - When true, the panel will be active after the drop
925    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 target is same tab, and it is only one panel, do nothing.
937        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        // Here is looks like remove_panel on a same item, but it difference.
948        //
949        // We must to split it to remove_panel, unless it will be crash by error:
950        // Cannot update ui::dock::tab_panel::TabPanel while it is already being updated
951        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        // Insert into new tabs
961        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    /// Add panel with split placement
976    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        // wrap the panel in a TabPanel
986        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            // 1. Create new StackPanel with new axis
1029            // 2. Move cx.entity() from parent StackPanel to the new StackPanel
1030            // 3. Add the new TabPanel to the new StackPanel at the correct index
1031            // 4. Add new StackPanel to the parent StackPanel at the correct index
1032            let tab_panel = cx.entity().clone();
1033
1034            // Try to use the old stack panel, not just create a new one, to avoid too many nested stack panels
1035            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        // Remove self from the parent DockArea.
1144        // This is ensure to remove from Tiles
1145        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    // Bind actions to the tab panel, only when the tab panel is not collapsed.
1159    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}