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        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 -1 for avoid border overlap with the first tab
701                        .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                // Always not show active tab style, if the panel is collapsed
721                if self.collapsed {
722                    active = false;
723                }
724
725                Some(
726                    Tab::default()
727                        .when(!has_extend_dock_button && ix == 0, |this| {
728                            // Right 1px for avoid border overlap with the first tab
729                            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                                // Open dock if clicked on the collapsed bottom dock
746                                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                // empty space to allow move to last tab right
782                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    /// Calculate the split direction based on the current mouse position
896    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        // Check the mouse position to determine the split direction
906        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            // center to merge into the current tab
916            self.will_split_placement = None;
917        }
918        cx.notify()
919    }
920
921    /// Handle the drop event when dragging a panel
922    ///
923    /// - `active` - When true, the panel will be active after the drop
924    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 target is same tab, and it is only one panel, do nothing.
936        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        // Here is looks like remove_panel on a same item, but it difference.
947        //
948        // We must to split it to remove_panel, unless it will be crash by error:
949        // Cannot update ui::dock::tab_panel::TabPanel while it is already being updated
950        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        // Insert into new tabs
960        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    /// Add panel with split placement
975    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        // wrap the panel in a TabPanel
985        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            // 1. Create new StackPanel with new axis
1028            // 2. Move cx.entity() from parent StackPanel to the new StackPanel
1029            // 3. Add the new TabPanel to the new StackPanel at the correct index
1030            // 4. Add new StackPanel to the parent StackPanel at the correct index
1031            let tab_panel = cx.entity().clone();
1032
1033            // Try to use the old stack panel, not just create a new one, to avoid too many nested stack panels
1034            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        // Remove self from the parent DockArea.
1143        // This is ensure to remove from Tiles
1144        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    // Bind actions to the tab panel, only when the tab panel is not collapsed.
1158    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}