Skip to main content

smelt_term/
surface.rs

1//! Renderer facade: bundles a [`Compositor`], [`Arc<Theme>`], [`LayoutTree`],
2//! and terminal size. Use directly for standalone renderers; `smelt-edit`'s
3//! `Ui` wraps it and adds editor-specific state.
4
5use std::io::Write;
6use std::sync::Arc;
7
8use smelt_style::theme::Theme;
9
10use crate::compositor::Compositor;
11use crate::grid::{Grid, GridSlice};
12use crate::layout::{resolve_layout, LayoutTree, PaintId, Rect};
13use crate::paint_layout_tree;
14
15pub struct Surface {
16    compositor: Compositor,
17    layout: LayoutTree,
18    theme: Arc<Theme>,
19    size: (u16, u16),
20}
21
22impl Surface {
23    pub fn new(width: u16, height: u16) -> Self {
24        Self::with_theme(width, height, Theme::new())
25    }
26
27    pub fn with_theme(width: u16, height: u16, theme: Theme) -> Self {
28        Self {
29            compositor: Compositor::new(width, height),
30            layout: LayoutTree::vbox(Vec::new()),
31            theme: Arc::new(theme),
32            size: (width, height),
33        }
34    }
35
36    pub fn terminal_size(&self) -> (u16, u16) {
37        self.size
38    }
39
40    pub fn set_terminal_size(&mut self, w: u16, h: u16) {
41        self.size = (w, h);
42        self.compositor.resize(w, h);
43    }
44
45    /// Full-screen rect at the current terminal size.
46    pub fn area(&self) -> Rect {
47        Rect::new(0, 0, self.size.0, self.size.1)
48    }
49
50    pub fn layout(&self) -> &LayoutTree {
51        &self.layout
52    }
53
54    pub fn set_layout(&mut self, layout: LayoutTree) {
55        self.layout = layout;
56    }
57
58    pub fn theme(&self) -> &Arc<Theme> {
59        &self.theme
60    }
61
62    /// Mutable theme handle; copy-on-write via `Arc::make_mut`.
63    pub fn theme_mut(&mut self) -> &mut Theme {
64        Arc::make_mut(&mut self.theme)
65    }
66
67    pub fn force_redraw(&mut self) {
68        self.compositor.force_redraw();
69    }
70
71    /// Resolved screen rect for a `PaintId` leaf, or `None` if not present.
72    pub fn paint_rect(&self, id: PaintId) -> Option<Rect> {
73        resolve_layout(&self.layout, self.area()).get(&id).copied()
74    }
75
76    pub fn compositor(&self) -> &Compositor {
77        &self.compositor
78    }
79
80    pub fn compositor_mut(&mut self) -> &mut Compositor {
81        &mut self.compositor
82    }
83
84    /// Walk the layout tree, paint chrome, and hand each leaf's `GridSlice` to `paint`.
85    pub fn render<W, F>(&mut self, w: &mut W, mut paint: F) -> std::io::Result<()>
86    where
87        W: Write,
88        F: FnMut(PaintId, &mut GridSlice<'_>, &Arc<Theme>),
89    {
90        let layout = self.layout.clone();
91        let area = self.area();
92        let size = self.size;
93        let theme_arc = Arc::clone(&self.theme);
94        self.compositor
95            .render_with(&self.theme, w, move |grid, _theme| {
96                let theme = &theme_arc;
97                let mut dispatch = |id: PaintId,
98                                    leaf: Rect,
99                                    grid: &mut Grid,
100                                    theme: &Arc<Theme>,
101                                    _ts: (u16, u16)| {
102                    let mut slice = grid.slice_mut(leaf);
103                    paint(id, &mut slice, theme);
104                };
105                paint_layout_tree(grid, theme, &layout, area, size, &mut dispatch);
106            })
107    }
108
109    /// Paint directly into the compositor's grid, bypassing the layout tree.
110    pub fn render_raw<W, F>(&mut self, w: &mut W, paint: F) -> std::io::Result<()>
111    where
112        W: Write,
113        F: FnOnce(&mut Grid, &Theme),
114    {
115        self.compositor.render_with(&self.theme, w, paint)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::layout::{Constraint, PaintId};
123
124    #[test]
125    fn area_reports_full_terminal_rect() {
126        let s = Surface::new(80, 24);
127        assert_eq!(s.area(), Rect::new(0, 0, 80, 24));
128    }
129
130    #[test]
131    fn set_terminal_size_updates_reported_size_and_area() {
132        let mut s = Surface::new(10, 5);
133        s.set_terminal_size(40, 20);
134        assert_eq!(s.terminal_size(), (40, 20));
135        assert_eq!(s.area(), Rect::new(0, 0, 40, 20));
136    }
137
138    #[test]
139    fn paint_rect_returns_leaf_rect_resolved_against_current_size() {
140        // A layout with two equal-height panes split vertically: each
141        // leaf should resolve to half the surface area.
142        let mut s = Surface::new(80, 24);
143        let top = PaintId(1);
144        let bottom = PaintId(2);
145        s.set_layout(LayoutTree::vbox(vec![
146            (Constraint::Fill, LayoutTree::leaf(top)),
147            (Constraint::Fill, LayoutTree::leaf(bottom)),
148        ]));
149        let top_rect = s.paint_rect(top).expect("top leaf resolved");
150        let bot_rect = s.paint_rect(bottom).expect("bottom leaf resolved");
151        assert_eq!(top_rect.height + bot_rect.height, 24);
152        assert_eq!(top_rect.width, 80);
153        assert_eq!(bot_rect.width, 80);
154    }
155
156    #[test]
157    fn paint_rect_returns_none_for_unknown_leaf() {
158        let s = Surface::new(80, 24);
159        assert_eq!(s.paint_rect(PaintId(999)), None);
160    }
161
162    #[test]
163    fn theme_mut_allows_in_place_mutation() {
164        use smelt_style::style::{Color, Style};
165        let mut s = Surface::new(10, 5);
166        s.theme_mut().set("Error", Style::new().fg(Color::Red));
167        // The same theme handle now resolves "Error".
168        assert_eq!(s.theme().get("Error").fg, Some(Color::Red));
169    }
170}