Skip to main content

ui/components/pane/
render.rs

1//! [`Render`] impl for [`super::Pane`] — the tab strip (with close/add
2//! affordances and drag-to-reorder) plus the active tab's content.
3
4use gpui::Empty;
5
6use super::{Pane, PaneEvent};
7use crate::{IconButton, IconName, IconSize, Tab, TabBar, prelude::*};
8
9/// Bare `IconButton::new`'s own default `debug_selector` (`"ICON-{icon:?}"`)
10/// collides across every tab sharing the same icon within a `Pane` — this
11/// wraps it in a `div` carrying a per-instance selector instead. The wrapper
12/// does not intercept clicks; the inner `IconButton` still owns the only
13/// click handler (mirrors `ActionPanel`'s Save/Cancel wrapper precedent).
14fn debug_wrap(id: impl Into<ElementId>, selector: String, child: IconButton) -> impl IntoElement {
15    div().id(id).debug_selector(move || selector).child(child)
16}
17
18/// Drag payload used for reordering tabs within a [`Pane`]'s tab strip.
19#[derive(Clone, Copy)]
20struct TabDragPayload {
21    from_idx: usize,
22}
23
24impl Render for Pane {
25    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
26        let active_idx = self.active_idx;
27        let mut tab_bar = TabBar::new("pane-tab-bar");
28
29        for ix in 0..self.tabs.len() {
30            let tab_id = self.tabs[ix].0;
31            let title = self.tabs[ix].1.title();
32            // Only the focused pane's active tab is drawn selected, so the
33            // whole window shows a single active tab.
34            let selected = ix == active_idx && self.focused;
35
36            // Per-tab hover group: the close button is hidden until the mouse
37            // hovers this specific tab (each tab is its own `group` scope, so
38            // hovering one tab reveals only its own close button).
39            let hover_group = SharedString::from(format!("pane-tab-{}", tab_id.0));
40            // VSCode behavior: the active tab always shows its close button;
41            // inactive tabs reveal it only on hover.
42            let mut close_ib = IconButton::new(("pane-tab-close", tab_id.0), IconName::Close)
43                .icon_size(IconSize::XSmall)
44                .on_click(cx.listener(move |this, _, _, cx| {
45                    this.close_tab(ix, cx);
46                }));
47            if !selected {
48                close_ib = close_ib.visible_on_hover(hover_group.clone());
49            }
50            let close_button = debug_wrap(
51                ("pane-tab-close-wrap", tab_id.0),
52                format!("PANE-TAB-CLOSE-{}", tab_id.0),
53                close_ib,
54            );
55
56            let tab = Tab::new(("pane-tab", tab_id.0))
57                .group(hover_group)
58                .toggle_state(selected)
59                .end_slot(close_button)
60                // Wrap in `Label` so the tab title uses the UI font family/size
61                // consistently (a bare string child inherits the window's
62                // default text size, which mismatches other UI text and can
63                // overflow the tab strip's height).
64                .child(Label::new(title).size(LabelSize::Small))
65                .on_click(cx.listener(move |this, _, _, cx| {
66                    this.activate(ix, cx);
67                }))
68                .on_drag(TabDragPayload { from_idx: ix }, |_, _, _, cx| {
69                    cx.new(|_| Empty)
70                })
71                .drag_over::<TabDragPayload>(|style, _, _, _| style.opacity(0.5))
72                .on_drop(cx.listener(move |this, payload: &TabDragPayload, _, cx| {
73                    this.reorder(payload.from_idx, ix, cx);
74                }));
75
76            tab_bar = tab_bar.child(tab);
77        }
78
79        tab_bar = tab_bar.end_child(debug_wrap(
80            "pane-add-tab-wrap",
81            "PANE-ADD-TAB".to_string(),
82            IconButton::new("pane-add-tab", IconName::Plus)
83                .icon_size(IconSize::XSmall)
84                .on_click(cx.listener(|this, _, _, cx| {
85                    let content = (this.new_tab_factory)();
86                    this.add_tab(content, cx);
87                })),
88        ));
89
90        // Close-pane "x" at the far right of the header — asks the parent
91        // PaneGroup to remove this whole pane (ignored if it is the last one).
92        tab_bar = tab_bar.end_child(debug_wrap(
93            "pane-close-wrap",
94            "PANE-CLOSE".to_string(),
95            IconButton::new("pane-close", IconName::Close)
96                .icon_size(IconSize::XSmall)
97                .on_click(cx.listener(|_, _, _, cx| {
98                    cx.emit(PaneEvent::CloseRequested);
99                })),
100        ));
101
102        let content = self
103            .tabs
104            .get(active_idx)
105            .map(|(_, content)| content.render(true, window, cx));
106
107        v_flex()
108            .id("pane")
109            .size_full()
110            .child(tab_bar)
111            .child(div().flex_1().min_h_0().overflow_hidden().children(content))
112    }
113}