Skip to main content

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        cx.notify();
144    }
145
146    /// Splits the active pane in `dir`: appends a sibling into the active
147    /// pane's parent axis if it already runs along `dir`'s axis (N-way
148    /// split), otherwise wraps the active pane in a new two-child axis. The
149    /// new pane becomes active.
150    pub fn split(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
151        let new_pane = cx.new(|pane_cx| (self.pane_factory)(pane_cx));
152        let subscription = Self::watch_pane(&new_pane, cx);
153        self._subscriptions.push(subscription);
154
155        let active_id = self.active_pane.entity_id();
156        let inserted = self.root.split_active(
157            active_id,
158            dir.axis(),
159            dir.inserts_before(),
160            new_pane.clone(),
161        );
162        debug_assert!(
163            inserted,
164            "PaneGroup's active pane must exist in its own tree"
165        );
166
167        self.active_pane = new_pane;
168        cx.notify();
169    }
170
171    /// Removes the active pane from the tree. Errs if it is the last pane.
172    pub fn close_active(&mut self, cx: &mut Context<Self>) -> Result<(), CannotRemoveLastPane> {
173        let target = self.active_pane.clone();
174        self.remove_pane(&target, cx)
175    }
176
177    /// Moves the active pane to its neighbor in `dir`, if one exists in the
178    /// same axis as the active pane's immediate parent split.
179    pub fn focus(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
180        let active_id = self.active_pane.entity_id();
181        let neighbor = match &self.root {
182            Member::Leaf(_) => None,
183            Member::Split(axis) => axis.find_neighbor(active_id, dir),
184        };
185        if let Some(neighbor) = neighbor {
186            self.active_pane = neighbor;
187            cx.notify();
188        }
189    }
190}
191
192impl Focusable for PaneGroup {
193    fn focus_handle(&self, _cx: &App) -> FocusHandle {
194        self.focus_handle.clone()
195    }
196}