Skip to main content

panes_ratatui/
lib.rs

1//! Convert panes layouts into `ratatui::layout::Rect` with pixel-perfect edge rounding.
2//!
3//! Primary API: [`resolve`] wraps a runtime resolve into a [`TerminalFrame`]
4//! with quantized u16 rects. Existing free functions remain for manual use.
5
6use panes::diff::LayoutDiff;
7use panes::runtime::{Frame as PanesFrame, LayoutRuntime};
8use panes::{AdapterFrame, Layout, OverlayEntry, PaneError, PanelEntry, PanelId, ResolvedLayout};
9use ratatui::Frame;
10use ratatui::layout::Rect;
11use ratatui::widgets::Clear;
12
13// ---------------------------------------------------------------------------
14// TerminalFrame
15// ---------------------------------------------------------------------------
16
17/// Resolved layout with rects quantized to terminal cells (u16).
18///
19/// Created via [`resolve`]. Provides [`diff`](Self::diff) and [`inner`](Self::inner)
20/// for access to the underlying runtime state.
21pub struct TerminalFrame<'a> {
22    shell: AdapterFrame<'a>,
23}
24
25impl<'a> TerminalFrame<'a> {
26    /// Quantized rect for a panel.
27    pub fn get(&self, id: PanelId) -> Option<Rect> {
28        self.shell.resolved().get(id).map(quantize)
29    }
30
31    /// All panels with quantized rects, in kind-grouped order.
32    pub fn panels(&self) -> impl Iterator<Item = PanelEntry<'_, Rect>> {
33        self.shell.resolved().panels().map(|e| e.map_rect(quantize))
34    }
35
36    /// Overlays with quantized rects.
37    pub fn overlays(&self) -> impl Iterator<Item = OverlayEntry<'_, Rect>> {
38        self.shell
39            .resolved()
40            .overlays()
41            .map(|e| e.map_rect(quantize))
42    }
43
44    /// Panels with focus flag and quantized rects.
45    ///
46    /// A panel is focused when its id matches `focused`, or when it is a
47    /// decoration panel whose content kind matches the focused panel's kind.
48    pub fn focused_panels(
49        &self,
50        focused: Option<PanelId>,
51    ) -> impl Iterator<Item = (PanelEntry<'_, Rect>, bool)> {
52        focused_panels_impl(self.shell.resolved(), focused)
53    }
54
55    /// Clear underlying cells then render each overlay. Call after panels.
56    pub fn render_overlays(
57        &self,
58        frame: &mut Frame,
59        render: impl FnMut(&mut Frame, OverlayEntry<'_, Rect>),
60    ) {
61        render_overlays_impl(frame, self.overlays(), render);
62    }
63
64    /// Layout diff (panel IDs, not rects). `None` for stateless resolves.
65    pub fn diff(&self) -> Option<LayoutDiff<'_>> {
66        self.shell.diff()
67    }
68
69    /// Raw panes `Frame`. `None` for stateless resolves.
70    pub fn inner(&self) -> Option<&PanesFrame> {
71        self.shell.inner()
72    }
73
74    /// Overlays that failed to anchor during the most recent resolve.
75    pub fn overlay_failures(
76        &self,
77    ) -> &[(panes::OverlayId, std::sync::Arc<str>, panes::AnchorFailure)] {
78        self.shell.overlay_failures()
79    }
80}
81
82/// Resolve a runtime and quantize in one step.
83///
84/// Calls [`LayoutRuntime::resolve`] then wraps the result in a [`TerminalFrame`]
85/// with quantized u16 rects. The returned frame borrows the runtime for
86/// [`diff`](TerminalFrame::diff) access.
87pub fn resolve<'a>(rt: &'a mut LayoutRuntime, area: Rect) -> Result<TerminalFrame<'a>, PaneError> {
88    let frame = rt.resolve(f32::from(area.width), f32::from(area.height))?;
89    Ok(TerminalFrame {
90        shell: AdapterFrame::from_runtime(frame, rt),
91    })
92}
93
94/// Resolve a layout without runtime state.
95///
96/// Returns a [`TerminalFrame`] with quantized rects. [`diff`](TerminalFrame::diff)
97/// and [`inner`](TerminalFrame::inner) return `None`. The `'static` lifetime
98/// reflects that the `Stateless` variant owns all its data.
99pub fn resolve_layout(layout: &Layout, area: Rect) -> Result<TerminalFrame<'static>, PaneError> {
100    let resolved = layout.resolve(f32::from(area.width), f32::from(area.height))?;
101    Ok(TerminalFrame {
102        shell: AdapterFrame::from_stateless(resolved),
103    })
104}
105
106// ---------------------------------------------------------------------------
107// Existing free functions (backward compatible)
108// ---------------------------------------------------------------------------
109
110panes::impl_adapter! {
111    rect: Rect,
112    origin: Rect,
113    convert_fn: quantize,
114    convert_at_fn: |r, origin: Rect| offset_rect(quantize(r), origin),
115}
116
117pub fn convert_at(resolved: &ResolvedLayout, origin: Rect) -> panes::__FxHashMap<PanelId, Rect> {
118    resolved
119        .iter()
120        .map(|(pid, r)| (pid, offset_rect(quantize(r), origin)))
121        .collect()
122}
123
124/// A panel is focused when its id matches `focused`, or when it is a
125/// decoration panel whose content kind matches the focused panel's kind.
126pub fn focused_panels<'a>(
127    resolved: &'a ResolvedLayout,
128    focused: Option<PanelId>,
129) -> impl Iterator<Item = (PanelEntry<'a, Rect>, bool)> {
130    focused_panels_impl(resolved, focused)
131}
132
133pub fn focused_panels_at<'a>(
134    resolved: &'a ResolvedLayout,
135    focused: Option<PanelId>,
136    origin: Rect,
137) -> impl Iterator<Item = (PanelEntry<'a, Rect>, bool)> {
138    focused_panels_impl(resolved, focused)
139        .map(move |(e, is_focused)| (e.map_rect(|r| offset_rect(r, origin)), is_focused))
140}
141
142fn focused_panels_impl<'a>(
143    resolved: &'a ResolvedLayout,
144    focused: Option<PanelId>,
145) -> impl Iterator<Item = (PanelEntry<'a, Rect>, bool)> {
146    let focused_kind = focused.and_then(|pid| resolved.kind_of(pid));
147
148    let content = resolved.panels().map(move |entry| {
149        let is_focused = matches!(focused, Some(fid) if entry.id == fid);
150        (entry.map_rect(quantize), is_focused)
151    });
152
153    let decorations = resolved.decoration_panels().iter().filter_map(move |d| {
154        let rect = resolved.get(d.id)?;
155        let is_focused = focused_kind.is_some_and(|fk| fk == d.content_kind.as_ref());
156        let kind_index = resolved.kind_index_of(d.content_kind.as_ref())?;
157        Some((
158            PanelEntry {
159                id: d.id,
160                kind: &d.content_kind,
161                rect: quantize(rect),
162                kind_index,
163            },
164            is_focused,
165        ))
166    });
167
168    content.chain(decorations)
169}
170
171/// Clear underlying cells before rendering each overlay. Call after panels.
172pub fn render_overlays(
173    frame: &mut Frame,
174    resolved: &ResolvedLayout,
175    render: impl FnMut(&mut Frame, OverlayEntry<'_, Rect>),
176) {
177    render_overlays_impl(frame, overlays(resolved), render);
178}
179
180pub fn render_overlays_at(
181    frame: &mut Frame,
182    resolved: &ResolvedLayout,
183    origin: Rect,
184    render: impl FnMut(&mut Frame, OverlayEntry<'_, Rect>),
185) {
186    render_overlays_impl(frame, overlays_at(resolved, origin), render);
187}
188
189fn render_overlays_impl<'a>(
190    frame: &mut Frame,
191    overlays: impl Iterator<Item = OverlayEntry<'a, Rect>>,
192    mut render: impl FnMut(&mut Frame, OverlayEntry<'a, Rect>),
193) {
194    for entry in overlays {
195        frame.render_widget(Clear, entry.rect);
196        render(frame, entry);
197    }
198}
199
200fn offset_rect(r: Rect, origin: Rect) -> Rect {
201    Rect {
202        x: r.x + origin.x,
203        y: r.y + origin.y,
204        ..r
205    }
206}
207
208/// Edge-rounding quantization: each edge is rounded independently,
209/// so adjacent panels sharing a float edge produce the same integer.
210fn quantize(r: &panes::Rect) -> Rect {
211    let left = clamp_edge(r.x.round());
212    let top = clamp_edge(r.y.round());
213    let right = clamp_edge((r.x + r.w).round());
214    let bottom = clamp_edge((r.y + r.h).round());
215
216    Rect {
217        x: left,
218        y: top,
219        width: right.saturating_sub(left),
220        height: bottom.saturating_sub(top),
221    }
222}
223
224fn clamp_edge(v: f32) -> u16 {
225    match v {
226        v if v <= 0.0 => 0,
227        v if v >= f32::from(u16::MAX) => u16::MAX,
228        _ => v as u16,
229    }
230}