Skip to main content

panes/
runtime.rs

1use std::sync::Arc;
2
3use crate::compiler::{CompileResult, compile, compute_layout};
4use crate::diff::{self, LayoutDiff};
5use crate::error::PaneError;
6use crate::focus::{self, FocusDirection};
7use crate::layout::Layout;
8use crate::node::PanelId;
9use crate::panel::fixed;
10use crate::rect::Rect;
11use crate::resolver::{self, ResolveScratch, ResolvedLayout};
12use crate::sequence::PanelSequence;
13use crate::strategy::StrategyKind;
14use crate::tree::LayoutTree;
15use crate::viewport::ViewportState;
16
17/// Result of a single resolve call: the resolved layout and its diff against the previous frame.
18pub struct Frame {
19    layout: Arc<ResolvedLayout>,
20    diff: LayoutDiff,
21}
22
23impl Frame {
24    /// The resolved layout for this frame.
25    pub fn layout(&self) -> &ResolvedLayout {
26        &self.layout
27    }
28
29    /// The diff between this frame and the previous one.
30    pub fn diff(&self) -> &LayoutDiff {
31        &self.diff
32    }
33}
34
35/// Stateful layout wrapper that tracks tree, viewport, and frame history.
36pub struct LayoutRuntime {
37    tree: LayoutTree,
38    viewport: ViewportState,
39    previous: Option<Arc<ResolvedLayout>>,
40    cached_compile: Option<CompileResult>,
41    cached_kinds: Option<resolver::KindIndex>,
42    rects_buf: Option<Vec<Option<Rect>>>,
43    diff_scratch: diff::DiffScratch,
44    resolve_scratch: ResolveScratch,
45    strategy: Option<StrategyKind>,
46    sequence: PanelSequence,
47}
48
49impl LayoutRuntime {
50    /// Create a runtime from an existing tree (legacy path, no strategy).
51    pub fn new(tree: LayoutTree) -> Self {
52        Self {
53            tree,
54            viewport: ViewportState::default(),
55            previous: None,
56            cached_compile: None,
57            cached_kinds: None,
58            rects_buf: None,
59            diff_scratch: diff::DiffScratch::default(),
60            resolve_scratch: ResolveScratch::default(),
61            strategy: None,
62            sequence: PanelSequence::default(),
63        }
64    }
65
66    /// Create a runtime from a strategy and initial panel kinds.
67    pub fn from_strategy(strategy: StrategyKind, kinds: &[Arc<str>]) -> Result<Self, PaneError> {
68        let mut sequence = PanelSequence::default();
69        let mut viewport = ViewportState::default();
70        let tree = crate::strategy::build_initial(&strategy, kinds, &mut sequence, &mut viewport)?;
71        Ok(Self {
72            tree,
73            viewport,
74            previous: None,
75            cached_compile: None,
76            cached_kinds: None,
77            rects_buf: None,
78            diff_scratch: diff::DiffScratch::default(),
79            resolve_scratch: ResolveScratch::default(),
80            strategy: Some(strategy),
81            sequence,
82        })
83    }
84
85    /// Create a runtime from a pre-built tree and a strategy.
86    /// Populates the sequence by looking up each kind in the tree.
87    pub fn from_tree_and_strategy(
88        tree: LayoutTree,
89        strategy: StrategyKind,
90        kinds: &[Arc<str>],
91    ) -> Result<Self, PaneError> {
92        let mut sequence = PanelSequence::default();
93        for kind in kinds {
94            for &pid in tree.panels_by_kind(kind) {
95                sequence.push(pid);
96            }
97        }
98        let focus = sequence.get(0);
99        Ok(Self {
100            tree,
101            viewport: ViewportState {
102                focus,
103                ..ViewportState::default()
104            },
105            previous: None,
106            cached_compile: None,
107            cached_kinds: None,
108            rects_buf: None,
109            diff_scratch: diff::DiffScratch::default(),
110            resolve_scratch: ResolveScratch::default(),
111            strategy: Some(strategy),
112            sequence,
113        })
114    }
115
116    /// Immutable access to the underlying tree.
117    pub fn tree(&self) -> &LayoutTree {
118        &self.tree
119    }
120
121    /// Mutable access to the underlying tree for structural mutations.
122    pub fn tree_mut(&mut self) -> &mut LayoutTree {
123        &mut self.tree
124    }
125
126    /// Immutable access to the viewport state.
127    pub fn viewport(&self) -> &ViewportState {
128        &self.viewport
129    }
130
131    /// Toggle a panel's collapsed state.
132    ///
133    /// Collapsing saves the current constraints and sets the panel to fixed(0.0).
134    /// Uncollapsing restores the saved constraints.
135    pub fn toggle_collapsed(&mut self, pid: PanelId) -> Result<(), PaneError> {
136        match self.viewport.collapsed.contains(&pid) {
137            true => {
138                let saved = self
139                    .viewport
140                    .saved_constraints
141                    .remove(&pid)
142                    .ok_or_else(|| {
143                        PaneError::InvalidViewport(
144                            format!("no saved constraints for panel {pid}").into(),
145                        )
146                    })?;
147                self.tree.set_constraints(pid, saved)?;
148                self.viewport.collapsed.remove(&pid);
149                Ok(())
150            }
151            false => {
152                let current = self.tree.panel_constraints(pid)?;
153                self.viewport.saved_constraints.insert(pid, current);
154                self.tree.set_constraints(pid, fixed(0.0))?;
155                self.viewport.collapsed.insert(pid);
156                Ok(())
157            }
158        }
159    }
160
161    /// Shift the scroll offset by a delta.
162    pub fn scroll_by(&mut self, delta: f32) {
163        self.viewport.scroll_offset += delta;
164    }
165
166    /// Set the scroll offset to an absolute value.
167    pub fn scroll_to(&mut self, offset: f32) {
168        self.viewport.scroll_offset = offset;
169    }
170
171    /// Set the active panel.
172    pub fn set_active(&mut self, pid: PanelId) {
173        self.viewport.focus = Some(pid);
174    }
175
176    /// Get the currently active panel, if any.
177    pub fn active_panel(&self) -> Option<PanelId> {
178        self.viewport.focus
179    }
180
181    /// The layout strategy, if this runtime was created via `from_strategy`.
182    pub fn strategy(&self) -> Option<&StrategyKind> {
183        self.strategy.as_ref()
184    }
185
186    /// The panel sequence (logical order).
187    pub fn sequence(&self) -> &PanelSequence {
188        &self.sequence
189    }
190
191    /// The currently focused panel.
192    pub fn focused(&self) -> Option<PanelId> {
193        self.viewport.focus
194    }
195
196    /// The kind of the currently focused panel.
197    pub fn focused_kind(&self) -> Option<&str> {
198        let pid = self.viewport.focus?;
199        self.tree.panel_kind(pid).ok()
200    }
201
202    /// Whether `pid` is a decorative panel (tab bar, title bar) for `content_pid`.
203    pub fn is_decoration_for(&self, pid: PanelId, content_pid: PanelId) -> bool {
204        let (Ok(dec_kind), Ok(content_kind)) =
205            (self.tree.panel_kind(pid), self.tree.panel_kind(content_pid))
206        else {
207            return false;
208        };
209        let base = dec_kind
210            .strip_suffix("_tab")
211            .or_else(|| dec_kind.strip_suffix("_title"));
212        matches!(base, Some(b) if b == content_kind)
213    }
214
215    /// Add a panel using the active strategy.
216    pub fn add_panel(&mut self, kind: Arc<str>) -> Result<PanelId, PaneError> {
217        let strategy = self
218            .strategy
219            .as_ref()
220            .ok_or_else(|| PaneError::InvalidMutation("no strategy set".into()))?
221            .clone();
222        crate::strategy::apply_add(
223            &strategy,
224            &mut self.tree,
225            &mut self.sequence,
226            &mut self.viewport,
227            kind,
228        )
229    }
230
231    /// Remove a panel using the active strategy. Returns the new focus panel.
232    pub fn remove_panel(&mut self, pid: PanelId) -> Result<Option<PanelId>, PaneError> {
233        let strategy = self
234            .strategy
235            .as_ref()
236            .ok_or_else(|| PaneError::InvalidMutation("no strategy set".into()))?
237            .clone();
238        crate::strategy::apply_remove(
239            &strategy,
240            &mut self.tree,
241            &mut self.sequence,
242            &mut self.viewport,
243            pid,
244        )
245    }
246
247    /// Move a panel to a new sequence index using the active strategy.
248    pub fn move_panel(&mut self, pid: PanelId, new_index: usize) -> Result<PanelId, PaneError> {
249        let strategy = self
250            .strategy
251            .as_ref()
252            .ok_or_else(|| PaneError::InvalidMutation("no strategy set".into()))?
253            .clone();
254        crate::strategy::apply_move(
255            &strategy,
256            &mut self.tree,
257            &mut self.sequence,
258            &mut self.viewport,
259            pid,
260            new_index,
261        )
262    }
263
264    /// Set focus to a specific panel using the active strategy.
265    pub fn focus(&mut self, pid: PanelId) -> Result<(), PaneError> {
266        match &self.strategy {
267            Some(strategy) => {
268                let strategy = strategy.clone();
269                crate::strategy::apply_focus(
270                    &strategy,
271                    &mut self.tree,
272                    &mut self.sequence,
273                    &mut self.viewport,
274                    pid,
275                )
276            }
277            None => {
278                self.viewport.focus = Some(pid);
279                Ok(())
280            }
281        }
282    }
283
284    /// Move focus to the next panel in the sequence.
285    pub fn focus_next(&mut self) -> Result<(), PaneError> {
286        let next = match self.viewport.focus {
287            Some(current) => {
288                let idx = self.sequence.index_of(current).unwrap_or(0);
289                let next_idx = (idx + 1) % self.sequence.len().max(1);
290                self.sequence.get(next_idx)
291            }
292            None => self.sequence.get(0),
293        };
294        match next {
295            Some(pid) => self.focus(pid),
296            None => Ok(()),
297        }
298    }
299
300    /// Move focus to the previous panel in the sequence.
301    pub fn focus_prev(&mut self) -> Result<(), PaneError> {
302        let prev = match self.viewport.focus {
303            Some(current) => {
304                let len = self.sequence.len().max(1);
305                let idx = self.sequence.index_of(current).unwrap_or(0);
306                let prev_idx = (idx + len - 1) % len;
307                self.sequence.get(prev_idx)
308            }
309            None => self.sequence.get(0),
310        };
311        match prev {
312            Some(pid) => self.focus(pid),
313            None => Ok(()),
314        }
315    }
316
317    /// Move focus to the nearest panel in a spatial direction.
318    ///
319    /// Returns `Ok(Some(target))` when focus moved, `Ok(None)` when no
320    /// candidate exists in that direction or no panel is focused.
321    pub fn focus_direction(
322        &mut self,
323        layout: &ResolvedLayout,
324        direction: FocusDirection,
325    ) -> Result<Option<PanelId>, PaneError> {
326        let focused = match self.focused() {
327            Some(pid) => pid,
328            None => return Ok(None),
329        };
330        match focus::find_nearest(layout, focused, &self.sequence, direction) {
331            Some(target) => {
332                self.focus(target)?;
333                Ok(Some(target))
334            }
335            None => Ok(None),
336        }
337    }
338
339    /// Move focus to the nearest panel in a spatial direction, using the
340    /// most recently resolved layout.
341    ///
342    /// Equivalent to [`focus_direction`](Self::focus_direction) but reads
343    /// geometry from the cached layout so the caller doesn't need to pass it.
344    /// Requires at least one prior [`resolve`](Self::resolve) call.
345    pub fn focus_direction_current(
346        &mut self,
347        direction: FocusDirection,
348    ) -> Result<Option<PanelId>, PaneError> {
349        let layout = Arc::clone(self.previous.as_ref().ok_or_else(|| {
350            PaneError::InvalidViewport("no resolved layout; call resolve() first".into())
351        })?);
352        self.focus_direction(&layout, direction)
353    }
354
355    /// Resolve the layout at the given dimensions, producing a Frame with layout and diff.
356    pub fn resolve(&mut self, width: f32, height: f32) -> Result<Frame, PaneError> {
357        let tree_dirty = self.tree.is_dirty();
358
359        let mut result = match (tree_dirty, self.cached_compile.take()) {
360            (false, Some(cached)) => cached,
361            _ => {
362                self.tree.clear_dirty();
363                compile(&self.tree)?
364            }
365        };
366
367        compute_layout(&mut result, width, height)?;
368
369        let mut layout = match (tree_dirty, self.cached_kinds.take()) {
370            (false, Some(kinds)) => resolver::resolve_with_cached_kinds(
371                &result,
372                &self.tree,
373                kinds,
374                &mut self.resolve_scratch,
375                self.rects_buf.take(),
376            )?,
377            _ => resolver::resolve(&result, &self.tree)?,
378        };
379
380        self.cached_kinds = Some(Arc::clone(layout.kinds_arc()));
381        self.cached_compile = Some(result);
382
383        apply_scroll_offset(&mut layout, self.viewport.scroll_offset);
384
385        let prev_arc = self.previous.take();
386        let diff = match (tree_dirty, prev_arc.as_deref()) {
387            (_, None) => diff::first_frame(&layout),
388            (false, Some(prev)) => diff::diff_same_panels(prev, &layout),
389            (true, Some(prev)) => diff::diff_reuse(prev, &layout, &mut self.diff_scratch),
390        };
391
392        let layout = Arc::new(layout);
393        self.previous = Some(Arc::clone(&layout));
394
395        // Reclaim the previous frame's rects buffer if no other consumers hold a reference.
396        if let Some(Ok(mut prev_layout)) = prev_arc.map(Arc::try_unwrap) {
397            self.rects_buf = Some(prev_layout.take_rects());
398        }
399
400        Ok(Frame { layout, diff })
401    }
402}
403
404impl From<LayoutTree> for LayoutRuntime {
405    fn from(tree: LayoutTree) -> Self {
406        Self::new(tree)
407    }
408}
409
410/// Shift all resolved rect x-positions by the negative scroll offset.
411fn apply_scroll_offset(layout: &mut ResolvedLayout, offset: f32) {
412    match offset.abs() < f32::EPSILON {
413        true => {}
414        false => layout.shift_x(-offset),
415    }
416}
417
418impl From<Layout> for LayoutRuntime {
419    fn from(layout: Layout) -> Self {
420        Self::new(LayoutTree::from(layout))
421    }
422}