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, canvas};
5
6use super::{Pane, PaneEvent};
7use crate::{
8    IconButton, IconButtonShape, IconName, IconSize, Tab, TabBar, TabCloseSide, TabPosition,
9    prelude::*,
10};
11
12/// Bare `IconButton::new`'s own default `debug_selector` (`"ICON-{icon:?}"`)
13/// collides across every tab sharing the same icon within a `Pane` — this
14/// wraps it in a `div` carrying a per-instance selector instead. The wrapper
15/// does not intercept clicks; the inner `IconButton` still owns the only
16/// click handler (mirrors `ActionPanel`'s Save/Cancel wrapper precedent).
17fn debug_wrap(id: impl Into<ElementId>, selector: String, child: IconButton) -> impl IntoElement {
18    div().id(id).debug_selector(move || selector).child(child)
19}
20
21/// Drag payload used for reordering tabs within a [`Pane`]'s tab strip.
22#[derive(Clone, Copy)]
23struct TabDragPayload {
24    from_idx: usize,
25}
26
27impl Render for Pane {
28    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
29        let active_idx = self.active_idx;
30
31        // One-frame-lagged resize hook: the `canvas()` below wrote the
32        // content-area bounds during the previous paint. Deliver `on_resize`
33        // to the active tab when EITHER the bounds changed OR the active tab
34        // changed (keyed by `TabId`) — so a freshly activated/added tab gets an
35        // initial size even if the pane itself never physically resized.
36        let active_id = self.tabs.get(active_idx).map(|(id, _)| *id);
37        if let (Some(bounds), Some(id)) = (self.content_bounds.get(), active_id) {
38            if self.notified_resize.get() != Some((id, bounds)) {
39                self.notified_resize.set(Some((id, bounds)));
40                self.tabs[active_idx].1.on_resize(bounds, cx);
41            }
42        }
43
44        let mut tab_bar = TabBar::new("pane-tab-bar");
45
46        let tab_count = self.tabs.len();
47        for ix in 0..tab_count {
48            let tab_id = self.tabs[ix].0;
49            let title = self.tabs[ix].1.title();
50            // Only the focused pane's active tab is drawn selected, so the
51            // whole window shows a single active tab.
52            let selected = ix == active_idx && self.focused;
53            let position = if ix == 0 {
54                TabPosition::First
55            } else if ix == tab_count - 1 {
56                TabPosition::Last
57            } else {
58                TabPosition::Middle(ix.cmp(&active_idx))
59            };
60
61            // Per-tab hover group: the close button is hidden until the mouse
62            // hovers this specific tab (each tab is its own `group` scope, so
63            // hovering one tab reveals only its own close button).
64            let hover_group = SharedString::from(format!("pane-tab-{}", tab_id.0));
65            // Close button is hover-only for every tab (active and inactive
66            // alike), per Zed's `pane.rs` tab close-button spec.
67            let close_ib = IconButton::new(("pane-tab-close", tab_id.0), IconName::Close)
68                .icon_size(IconSize::Small)
69                .icon_color(Color::Muted)
70                .shape(IconButtonShape::Square)
71                .size(ButtonSize::None)
72                .on_click(cx.listener(move |this, _, _, cx| {
73                    this.close_tab(ix, cx);
74                }))
75                .visible_on_hover(hover_group.clone());
76            let close_button = debug_wrap(
77                ("pane-tab-close-wrap", tab_id.0),
78                format!("PANE-TAB-CLOSE-{}", tab_id.0),
79                close_ib,
80            );
81
82            let tab = Tab::new(("pane-tab", tab_id.0))
83                .group(hover_group)
84                .toggle_state(selected)
85                .position(position)
86                .close_side(TabCloseSide::End)
87                .end_slot(close_button)
88                // Wrap in `Label` so the tab title uses the UI font family/size
89                // consistently (a bare string child inherits the window's
90                // default text size, which mismatches other UI text and can
91                // overflow the tab strip's height).
92                .child(Label::new(title).size(LabelSize::Small))
93                .on_click(cx.listener(move |this, _, _, cx| {
94                    this.activate(ix, cx);
95                }))
96                .on_drag(TabDragPayload { from_idx: ix }, |_, _, _, cx| {
97                    cx.new(|_| Empty)
98                })
99                .drag_over::<TabDragPayload>(|style, _, _, _| style.opacity(0.5))
100                .on_drop(cx.listener(move |this, payload: &TabDragPayload, _, cx| {
101                    this.reorder(payload.from_idx, ix, cx);
102                }));
103
104            tab_bar = tab_bar.child(tab);
105        }
106
107        tab_bar = tab_bar.end_child(debug_wrap(
108            "pane-add-tab-wrap",
109            "PANE-ADD-TAB".to_string(),
110            IconButton::new("pane-add-tab", IconName::Plus)
111                .icon_size(IconSize::XSmall)
112                .on_click(cx.listener(|this, _, _, cx| {
113                    let content = (this.new_tab_factory)();
114                    this.add_tab(content, cx);
115                })),
116        ));
117
118        // Close-pane "x" at the far right of the header — asks the parent
119        // PaneGroup to remove this whole pane (ignored if it is the last one).
120        tab_bar = tab_bar.end_child(debug_wrap(
121            "pane-close-wrap",
122            "PANE-CLOSE".to_string(),
123            IconButton::new("pane-close", IconName::Close)
124                .icon_size(IconSize::XSmall)
125                .on_click(cx.listener(|_, _, _, cx| {
126                    cx.emit(PaneEvent::CloseRequested);
127                })),
128        ));
129
130        let content = self
131            .tabs
132            .get(active_idx)
133            .map(|(_, content)| content.render(true, window, cx));
134
135        // Measure the tab-content area so `on_resize` can fire next frame. The
136        // canvas paints nothing; it only records its own bounds (mirrors
137        // `TerminalView`'s container measurement).
138        let measure = self.content_bounds.clone();
139
140        v_flex().id("pane").size_full().child(tab_bar).child(
141            div()
142                .relative()
143                .flex_1()
144                .min_h_0()
145                .overflow_hidden()
146                .child(
147                    canvas(
148                        move |bounds, _, _| measure.set(Some(bounds)),
149                        |_, _, _, _| {},
150                    )
151                    .absolute()
152                    .size_full(),
153                )
154                .children(content),
155        )
156    }
157}