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