ui/components/pane_group/mod.rs
1//! Recursive split-tree layout: [`PaneGroup`] owns a tree of [`Pane`]s
2//! joined by [`PaneAxis`] splits, tracks which pane is active, and exposes
3//! [`SplitDirection`]-driven split/close/focus operations plus the default
4//! [`crate::register_pane_keybindings`] that drive them.
5//!
6//! Deliberately reuses [`gpui::Axis`] (exactly `Horizontal`/`Vertical`)
7//! rather than a redundant parallel enum, and keeps the tree shape
8//! (`Member`/`PaneAxis`, per-axis `flexes`) close to the proven shape used
9//! by editors with recursive pane splitting — reused as an *idea*, not code
10//! (no shared dependency, no copied source).
11
12mod divider;
13mod lifecycle;
14mod preview;
15mod render;
16mod tree;
17
18use std::cell::Cell;
19use std::rc::Rc;
20
21use gpui::{Axis, Bounds, Entity, FocusHandle, Focusable, Subscription};
22
23pub use preview::PaneGroupPreview;
24
25use crate::{Pane, prelude::*};
26
27/// Direction a new pane is inserted relative to the currently active pane.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SplitDirection {
30 Left,
31 Right,
32 Up,
33 Down,
34}
35
36impl SplitDirection {
37 /// The layout axis a split in this direction runs along.
38 pub fn axis(self) -> Axis {
39 match self {
40 SplitDirection::Left | SplitDirection::Right => Axis::Horizontal,
41 SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
42 }
43 }
44
45 /// Whether the new pane is inserted before (`Left`/`Up`) or after
46 /// (`Right`/`Down`) the active pane along [`Self::axis`].
47 fn inserts_before(self) -> bool {
48 matches!(self, SplitDirection::Left | SplitDirection::Up)
49 }
50}
51
52/// A node in [`PaneGroup`]'s recursive split tree: either a leaf pane or a
53/// nested split along some [`Axis`].
54pub enum Member {
55 Leaf(Entity<Pane>),
56 Split(PaneAxis),
57}
58
59/// A row (`Horizontal`) or column (`Vertical`) of `N` [`Member`]s, each
60/// sized by the parallel `flexes` fraction (sums to `1.0`).
61pub struct PaneAxis {
62 pub axis: Axis,
63 pub members: Vec<Member>,
64 pub flexes: Vec<f32>,
65 /// Last-measured container bounds, populated by a `canvas()` during
66 /// render and read back by the divider-drag handler to convert pointer
67 /// deltas into fraction deltas. Persists on the node across re-renders.
68 bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
69}
70
71impl PaneAxis {
72 fn new(axis: Axis, members: Vec<Member>, flexes: Vec<f32>) -> Self {
73 Self {
74 axis,
75 members,
76 flexes,
77 bounds: Rc::new(Cell::new(None)),
78 }
79 }
80}
81
82/// Returned by [`PaneGroup::close_active`] when the active pane is the only
83/// leaf left in the tree — a `PaneGroup` always keeps at least one pane.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CannotRemoveLastPane;
86
87/// Stateful owner of a recursive pane split tree: which pane is active, how
88/// new panes' content is created, and (via its [`Render`] impl) the
89/// split/close/focus keybinding wiring and active-pane highlight.
90///
91/// Create with `cx.new(|cx| PaneGroup::new(cx, pane))`, optionally
92/// `.with_pane_factory(..)` to control what split-created panes contain.
93pub struct PaneGroup {
94 root: Member,
95 active_pane: Entity<Pane>,
96 pane_factory: Box<dyn Fn(&mut Context<Pane>) -> Pane>,
97 focus_handle: FocusHandle,
98 _subscriptions: Vec<Subscription>,
99}
100
101impl PaneGroup {
102 /// Boots a single-leaf tree containing `pane`.
103 ///
104 /// Takes `cx: &mut Context<Self>` (a refinement over phase-01's
105 /// `new(pane: Entity<Pane>)` pseudocode) — needed to allocate this
106 /// group's own [`FocusHandle`] and subscribe to `pane`'s
107 /// [`PaneEvent::Empty`].
108 pub fn new(cx: &mut Context<Self>, pane: Entity<Pane>) -> Self {
109 let subscription = Self::watch_pane(&pane, cx);
110 Self {
111 root: Member::Leaf(pane.clone()),
112 active_pane: pane,
113 pane_factory: Box::new(|_| Pane::new()),
114 focus_handle: cx.focus_handle(),
115 _subscriptions: vec![subscription],
116 }
117 }
118
119 /// Sets the factory used to construct the [`Pane`] for every future
120 /// split. Defaults to an empty [`Pane::new`].
121 pub fn with_pane_factory(
122 mut self,
123 factory: impl Fn(&mut Context<Pane>) -> Pane + 'static,
124 ) -> Self {
125 self.pane_factory = Box::new(factory);
126 self
127 }
128
129 /// The currently active (keyboard/action target) pane.
130 pub fn active_pane(&self) -> &Entity<Pane> {
131 &self.active_pane
132 }
133
134 /// Sets the active pane directly, for callers holding a specific
135 /// `Entity<Pane>` handle (e.g. programmatically laying out an initial
136 /// grid, or a "reveal this file's pane" flow) rather than a keyboard
137 /// direction. No tree-membership validation: setting a pane outside
138 /// this group's tree only makes subsequent `split`/`close_active`
139 /// target a detached pane, which is inert (not a crash risk) but almost
140 /// certainly not what the caller wants.
141 pub fn set_active_pane(&mut self, pane: Entity<Pane>, cx: &mut Context<Self>) {
142 self.active_pane = pane;
143 self.sync_focus(cx);
144 cx.notify();
145 }
146
147 /// Splits the active pane in `dir`: appends a sibling into the active
148 /// pane's parent axis if it already runs along `dir`'s axis (N-way
149 /// split), otherwise wraps the active pane in a new two-child axis. The
150 /// new pane becomes active.
151 pub fn split(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
152 let new_pane = cx.new(|pane_cx| (self.pane_factory)(pane_cx));
153 let subscription = Self::watch_pane(&new_pane, cx);
154 self._subscriptions.push(subscription);
155
156 let active_id = self.active_pane.entity_id();
157 let inserted = self.root.split_active(
158 active_id,
159 dir.axis(),
160 dir.inserts_before(),
161 new_pane.clone(),
162 );
163 debug_assert!(
164 inserted,
165 "PaneGroup's active pane must exist in its own tree"
166 );
167
168 self.active_pane = new_pane;
169 self.sync_focus(cx);
170 cx.notify();
171 }
172
173 /// Marks the active pane focused and every other leaf pane unfocused, so
174 /// only the active pane's active tab renders "selected".
175 pub(super) fn sync_focus(&mut self, cx: &mut Context<Self>) {
176 let active_id = self.active_pane.entity_id();
177 let mut leaves = Vec::new();
178 self.root.collect_leaves(&mut leaves);
179 for pane in leaves {
180 let focused = pane.entity_id() == active_id;
181 pane.update(cx, |pane, cx| pane.set_focused(focused, cx));
182 }
183 }
184
185 /// Removes the active pane from the tree. Errs if it is the last pane.
186 pub fn close_active(&mut self, cx: &mut Context<Self>) -> Result<(), CannotRemoveLastPane> {
187 let target = self.active_pane.clone();
188 self.remove_pane(&target, cx)
189 }
190
191 /// Moves the active pane to its neighbor in `dir`, if one exists in the
192 /// same axis as the active pane's immediate parent split.
193 pub fn focus(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
194 let active_id = self.active_pane.entity_id();
195 let neighbor = match &self.root {
196 Member::Leaf(_) => None,
197 Member::Split(axis) => axis.find_neighbor(active_id, dir),
198 };
199 if let Some(neighbor) = neighbor {
200 self.active_pane = neighbor;
201 self.sync_focus(cx);
202 cx.notify();
203 }
204 }
205}
206
207impl Focusable for PaneGroup {
208 fn focus_handle(&self, _cx: &App) -> FocusHandle {
209 self.focus_handle.clone()
210 }
211}