Skip to main content

tui/rendering/
render_context.rs

1use std::sync::Arc;
2
3use crate::theme::Theme;
4
5#[cfg(feature = "syntax")]
6use crate::syntax_highlighting::SyntaxHighlighter;
7
8#[doc = include_str!("../docs/view_context.md")]
9#[derive(Clone)]
10pub struct ViewContext {
11    /// The size this context allows the component to draw into. This is *not*
12    /// the terminal size — parents pass child contexts whose size is the slice
13    /// of the terminal allocated to the child.
14    pub size: Size,
15    pub theme: Arc<Theme>,
16    #[cfg(feature = "syntax")]
17    pub(crate) highlighter: Arc<SyntaxHighlighter>,
18}
19
20/// The size, in columns and rows, a component is permitted to draw into.
21///
22/// Parents produce child sizes by slicing their own (via
23/// [`ViewContext::with_width`], [`ViewContext::with_height`], or
24/// [`ViewContext::inset`]). Children should treat the size as authoritative
25/// and never assume the full terminal width.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Size {
28    pub width: u16,
29    pub height: u16,
30}
31
32/// Edge insets used to shrink a [`ViewContext`] to a smaller size.
33///
34/// Used by parents that want to render a child inside a padded box: subtract
35/// the insets from the parent size to get the child's allocated size.
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37pub struct Insets {
38    pub left: u16,
39    pub right: u16,
40    pub top: u16,
41    pub bottom: u16,
42}
43
44impl Insets {
45    pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
46        Self { left, right, top, bottom }
47    }
48
49    /// Insets that are equal on all four sides.
50    pub fn all(amount: u16) -> Self {
51        Self { left: amount, right: amount, top: amount, bottom: amount }
52    }
53
54    /// Insets that are equal on opposing sides.
55    pub fn symmetric(horizontal: u16, vertical: u16) -> Self {
56        Self { left: horizontal, right: horizontal, top: vertical, bottom: vertical }
57    }
58
59    /// Insets that only affect the horizontal axis.
60    pub fn horizontal(amount: u16) -> Self {
61        Self::symmetric(amount, 0)
62    }
63
64    /// Insets that only affect the vertical axis.
65    pub fn vertical(amount: u16) -> Self {
66        Self::symmetric(0, amount)
67    }
68}
69
70impl ViewContext {
71    pub fn new(size: impl Into<Size>) -> Self {
72        Self::new_with_theme(size, Theme::default())
73    }
74
75    pub fn new_with_theme(size: impl Into<Size>, theme: Theme) -> Self {
76        Self {
77            size: size.into(),
78            theme: Arc::new(theme),
79            #[cfg(feature = "syntax")]
80            highlighter: Arc::new(SyntaxHighlighter::new()),
81        }
82    }
83
84    #[cfg(feature = "syntax")]
85    pub fn highlighter(&self) -> &SyntaxHighlighter {
86        &self.highlighter
87    }
88
89    /// Clone this context with a new size, preserving theme state.
90    pub fn with_size(&self, size: impl Into<Size>) -> Self {
91        Self {
92            size: size.into(),
93            theme: self.theme.clone(),
94            #[cfg(feature = "syntax")]
95            highlighter: self.highlighter.clone(),
96        }
97    }
98
99    /// Clone this context with the width replaced, preserving height and theme.
100    pub fn with_width(&self, width: u16) -> Self {
101        self.with_size((width, self.size.height))
102    }
103
104    /// Clone this context with the height replaced, preserving width and theme.
105    pub fn with_height(&self, height: u16) -> Self {
106        self.with_size((self.size.width, height))
107    }
108
109    /// Clone this context with the size shrunk by `insets` on each side.
110    /// Saturates at zero on each axis.
111    pub fn inset(&self, insets: Insets) -> Self {
112        let width = self.size.width.saturating_sub(insets.left).saturating_sub(insets.right);
113        let height = self.size.height.saturating_sub(insets.top).saturating_sub(insets.bottom);
114        self.with_size((width, height))
115    }
116}
117
118impl From<(u16, u16)> for Size {
119    fn from((width, height): (u16, u16)) -> Self {
120        Self { width, height }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::theme::Theme;
128
129    fn ctx(width: u16, height: u16) -> ViewContext {
130        ViewContext::new_with_theme((width, height), Theme::default())
131    }
132
133    #[test]
134    fn with_width_replaces_width_and_keeps_height() {
135        let parent = ctx(80, 24);
136        let child = parent.with_width(40);
137        assert_eq!(child.size.width, 40);
138        assert_eq!(child.size.height, 24);
139    }
140
141    #[test]
142    fn with_width_preserves_theme_arc() {
143        let parent = ctx(80, 24);
144        let child = parent.with_width(40);
145        assert!(Arc::ptr_eq(&parent.theme, &child.theme));
146    }
147
148    #[test]
149    fn with_height_replaces_height_and_keeps_width() {
150        let parent = ctx(80, 24);
151        let child = parent.with_height(10);
152        assert_eq!(child.size.width, 80);
153        assert_eq!(child.size.height, 10);
154    }
155
156    #[test]
157    fn inset_subtracts_on_all_sides() {
158        let parent = ctx(80, 24);
159        let child = parent.inset(Insets::new(2, 3, 1, 4));
160        assert_eq!(child.size.width, 80 - 2 - 3);
161        assert_eq!(child.size.height, 24 - 1 - 4);
162    }
163
164    #[test]
165    fn inset_saturates_at_zero() {
166        let parent = ctx(4, 4);
167        let child = parent.inset(Insets::all(10));
168        assert_eq!(child.size.width, 0);
169        assert_eq!(child.size.height, 0);
170    }
171
172    #[test]
173    fn insets_symmetric_sets_opposite_sides_equal() {
174        let insets = Insets::symmetric(3, 5);
175        assert_eq!(insets.left, 3);
176        assert_eq!(insets.right, 3);
177        assert_eq!(insets.top, 5);
178        assert_eq!(insets.bottom, 5);
179    }
180
181    #[test]
182    fn insets_horizontal_only_affects_left_and_right() {
183        let insets = Insets::horizontal(4);
184        assert_eq!(insets.left, 4);
185        assert_eq!(insets.right, 4);
186        assert_eq!(insets.top, 0);
187        assert_eq!(insets.bottom, 0);
188    }
189
190    #[test]
191    fn insets_vertical_only_affects_top_and_bottom() {
192        let insets = Insets::vertical(2);
193        assert_eq!(insets.left, 0);
194        assert_eq!(insets.right, 0);
195        assert_eq!(insets.top, 2);
196        assert_eq!(insets.bottom, 2);
197    }
198
199    #[test]
200    fn inset_horizontal_only_shrinks_width() {
201        let parent = ctx(80, 24);
202        let child = parent.inset(Insets::horizontal(2));
203        assert_eq!(child.size.width, 76);
204        assert_eq!(child.size.height, 24);
205    }
206}