gpui_component/dock/
tab_panel.rs

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