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 { row, col, is_visible: true }
20    }
21}
22
23#[doc = include_str!("../docs/frame.md")]
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Frame {
26    lines: Vec<Line>,
27    cursor: Cursor,
28}
29
30impl Frame {
31    pub fn new(lines: Vec<Line>) -> Self {
32        Self { lines, cursor: Cursor::hidden() }
33    }
34
35    pub fn lines(&self) -> &[Line] {
36        &self.lines
37    }
38
39    pub fn cursor(&self) -> Cursor {
40        self.cursor
41    }
42
43    /// Replace the cursor without cloning lines.
44    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
45        self.cursor = cursor;
46        self
47    }
48
49    pub fn into_lines(self) -> Vec<Line> {
50        self.lines
51    }
52
53    pub fn into_parts(self) -> (Vec<Line>, Cursor) {
54        (self.lines, self.cursor)
55    }
56
57    pub fn clamp_cursor(mut self) -> Self {
58        if self.cursor.row >= self.lines.len() {
59            self.cursor.row = self.lines.len().saturating_sub(1);
60        }
61        self
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn cursor_hidden_returns_invisible_cursor_at_origin() {
71        let cursor = Cursor::hidden();
72        assert_eq!(cursor.row, 0);
73        assert_eq!(cursor.col, 0);
74        assert!(!cursor.is_visible);
75    }
76
77    #[test]
78    fn cursor_visible_returns_visible_cursor_at_position() {
79        let cursor = Cursor::visible(5, 10);
80        assert_eq!(cursor.row, 5);
81        assert_eq!(cursor.col, 10);
82        assert!(cursor.is_visible);
83    }
84
85    #[test]
86    fn clamp_cursor_clamps_out_of_bounds_row() {
87        let frame = Frame::new(vec![Line::new("a")]).with_cursor(Cursor::visible(10, 100));
88        let frame = frame.clamp_cursor();
89        assert_eq!(frame.cursor().row, 0);
90        assert_eq!(frame.cursor().col, 100);
91    }
92
93    #[test]
94    fn with_cursor_replaces_cursor_without_cloning_lines() {
95        let frame = Frame::new(vec![Line::new("hello")]);
96        let new_cursor = Cursor::visible(0, 3);
97        let frame = frame.with_cursor(new_cursor);
98        assert_eq!(frame.cursor(), new_cursor);
99        assert_eq!(frame.lines()[0].plain_text(), "hello");
100    }
101}