Skip to main content

tui/rendering/
frame.rs

1use super::line::Line;
2
3/// Logical cursor position within a component's rendered output.
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub struct Cursor {
6    pub row: usize,
7    pub col: usize,
8    pub is_visible: bool,
9}
10
11impl Cursor {
12    /// Create a hidden cursor at position (0, 0).
13    pub fn hidden() -> Self {
14        Self::default()
15    }
16
17    /// Create a visible cursor at the given position.
18    pub fn visible(row: usize, col: usize) -> Self {
19        Self {
20            row,
21            col,
22            is_visible: true,
23        }
24    }
25}
26
27/// Logical component output: lines plus cursor state.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Frame {
30    lines: Vec<Line>,
31    cursor: Cursor,
32}
33
34impl Frame {
35    pub fn new(lines: Vec<Line>) -> Self {
36        Self {
37            lines,
38            cursor: Cursor::hidden(),
39        }
40    }
41
42    pub fn lines(&self) -> &[Line] {
43        &self.lines
44    }
45
46    pub fn cursor(&self) -> Cursor {
47        self.cursor
48    }
49
50    /// Replace the cursor without cloning lines.
51    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
52        self.cursor = cursor;
53        self
54    }
55
56    pub fn into_lines(self) -> Vec<Line> {
57        self.lines
58    }
59
60    pub fn into_parts(self) -> (Vec<Line>, Cursor) {
61        (self.lines, self.cursor)
62    }
63
64    pub fn clamp_cursor(mut self) -> Self {
65        if self.cursor.row >= self.lines.len() {
66            self.cursor.row = self.lines.len().saturating_sub(1);
67        }
68        self
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn cursor_hidden_returns_invisible_cursor_at_origin() {
78        let cursor = Cursor::hidden();
79        assert_eq!(cursor.row, 0);
80        assert_eq!(cursor.col, 0);
81        assert!(!cursor.is_visible);
82    }
83
84    #[test]
85    fn cursor_visible_returns_visible_cursor_at_position() {
86        let cursor = Cursor::visible(5, 10);
87        assert_eq!(cursor.row, 5);
88        assert_eq!(cursor.col, 10);
89        assert!(cursor.is_visible);
90    }
91
92    #[test]
93    fn clamp_cursor_clamps_out_of_bounds_row() {
94        let frame = Frame::new(vec![Line::new("a")]).with_cursor(Cursor::visible(10, 100));
95        let frame = frame.clamp_cursor();
96        assert_eq!(frame.cursor().row, 0);
97        assert_eq!(frame.cursor().col, 100);
98    }
99
100    #[test]
101    fn with_cursor_replaces_cursor_without_cloning_lines() {
102        let frame = Frame::new(vec![Line::new("hello")]);
103        let new_cursor = Cursor::visible(0, 3);
104        let frame = frame.with_cursor(new_cursor);
105        assert_eq!(frame.cursor(), new_cursor);
106        assert_eq!(frame.lines()[0].plain_text(), "hello");
107    }
108}