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