Skip to main content

ui/components/pane_group/
render.rs

1//! [`Render`] impl for [`PaneGroup`]: recursive `h_flex`/`v_flex` +
2//! [`ResizablePanel`] layout, click-to-focus, the active-pane highlight
3//! border, and the `key_context`/`on_action` wiring that makes
4//! `pane_actions`'s actions drive this group for free once mounted.
5//! Divider drag math lives in [`super::divider`].
6
7use gpui::{AnyElement, Axis, Entity, MouseButton, canvas};
8
9use super::{Member, PaneAxis, PaneGroup, SplitDirection};
10use crate::{
11    ClosePane, FocusDown, FocusLeft, FocusRight, FocusUp, Pane, ResizablePanel, SplitDown,
12    SplitLeft, SplitRight, SplitUp, prelude::*,
13};
14
15impl PaneGroup {
16    fn render_member(
17        &self,
18        member: &Member,
19        path: Vec<usize>,
20        window: &mut Window,
21        cx: &mut Context<Self>,
22    ) -> AnyElement {
23        match member {
24            Member::Leaf(pane) => self.render_leaf(pane, cx),
25            Member::Split(axis) => self.render_axis(axis, path, window, cx),
26        }
27    }
28
29    fn render_leaf(&self, pane: &Entity<Pane>, cx: &mut Context<Self>) -> AnyElement {
30        let is_active = pane.entity_id() == self.active_pane.entity_id();
31        let focus_target = pane.clone();
32        let leaf_id = pane.entity_id().as_u64();
33
34        let _ = is_active;
35        div()
36            .id(("pane-leaf", leaf_id))
37            // Test-only (no-op in release builds, per `debug_selector`'s own
38            // cfg gate): lets tests locate a specific pane leaf's real
39            // rendered bounds by its stable entity id, e.g. to assert split
40            // quadrants or resize deltas.
41            .debug_selector(move || format!("PANE-LEAF-{leaf_id}"))
42            .size_full()
43            .overflow_hidden()
44            // No active-pane border: the active tab's accent already indicates
45            // focus, and a border would inset the (otherwise flush) panes.
46            .on_mouse_down(
47                MouseButton::Left,
48                cx.listener(move |this, _, window, cx| {
49                    this.active_pane = focus_target.clone();
50                    this.sync_focus(cx);
51                    window.focus(&this.focus_handle, cx);
52                    cx.notify();
53                }),
54            )
55            .child(pane.clone())
56            .into_any_element()
57    }
58
59    fn render_axis(
60        &self,
61        pane_axis: &PaneAxis,
62        path: Vec<usize>,
63        window: &mut Window,
64        cx: &mut Context<Self>,
65    ) -> AnyElement {
66        let axis = pane_axis.axis;
67        let mut container = match axis {
68            Axis::Horizontal => h_flex(),
69            Axis::Vertical => v_flex(),
70        }
71        .id(format!("pane-axis-{path:?}"))
72        .size_full()
73        .overflow_hidden();
74
75        let measure = pane_axis.bounds.clone();
76        container = container.child(
77            canvas(
78                move |bounds, _, _| measure.set(Some(bounds)),
79                |_, _, _, _| {},
80            )
81            .absolute()
82            .size_full(),
83        );
84
85        let n = pane_axis.members.len();
86        for (ix, child) in pane_axis.members.iter().enumerate() {
87            let mut child_path = path.clone();
88            child_path.push(ix);
89            let child_el = self.render_member(child, child_path, window, cx);
90
91            container = container.child(
92                ResizablePanel::new()
93                    .axis(axis)
94                    .fraction(pane_axis.flexes[ix])
95                    .child(child_el),
96            );
97
98            if ix + 1 < n {
99                container = container.child(self.render_divider(
100                    path.clone(),
101                    ix,
102                    axis,
103                    pane_axis.flexes[ix],
104                    pane_axis.flexes[ix + 1],
105                    window,
106                    cx,
107                ));
108            }
109        }
110
111        container.into_any_element()
112    }
113}
114
115impl Render for PaneGroup {
116    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
117        let content = self.render_member(&self.root, Vec::new(), window, cx);
118
119        div()
120            .id("pane-group")
121            .key_context("PaneGroup")
122            .track_focus(&self.focus_handle)
123            .size_full()
124            .on_action(cx.listener(|this, _: &SplitRight, _, cx| {
125                this.split(SplitDirection::Right, cx);
126            }))
127            .on_action(cx.listener(|this, _: &SplitDown, _, cx| {
128                this.split(SplitDirection::Down, cx);
129            }))
130            .on_action(cx.listener(|this, _: &SplitLeft, _, cx| {
131                this.split(SplitDirection::Left, cx);
132            }))
133            .on_action(cx.listener(|this, _: &SplitUp, _, cx| {
134                this.split(SplitDirection::Up, cx);
135            }))
136            .on_action(cx.listener(|this, _: &ClosePane, _, cx| {
137                let _ = this.close_active(cx);
138            }))
139            .on_action(cx.listener(|this, _: &FocusLeft, _, cx| {
140                this.focus(SplitDirection::Left, cx);
141            }))
142            .on_action(cx.listener(|this, _: &FocusRight, _, cx| {
143                this.focus(SplitDirection::Right, cx);
144            }))
145            .on_action(cx.listener(|this, _: &FocusUp, _, cx| {
146                this.focus(SplitDirection::Up, cx);
147            }))
148            .on_action(cx.listener(|this, _: &FocusDown, _, cx| {
149                this.focus(SplitDirection::Down, cx);
150            }))
151            .child(content)
152    }
153}