twitter_tool/ui_framework/
scroll_buffer.rs

1use crate::ui_framework::bounding_box::BoundingBox;
2use crate::ui_framework::{Input, Render};
3use anyhow::Result;
4use crossterm::cursor;
5use crossterm::event::{KeyCode, KeyEvent};
6use crossterm::queue;
7use crossterm::style::{self, Attributes, Color, Colors};
8use std::cmp::{max, min};
9use std::io::{Stdout, Write};
10
11#[derive(Debug, Clone)]
12pub struct ScrollBuffer {
13    lines: Vec<Vec<TextSegment>>,
14    display_height: usize,
15    display_offset: usize,
16    cursor_position: (usize, usize),
17    should_render: bool,
18    // CR: need to work bounding_box != last_bounding_box => should_render into the framework
19    last_bounding_box: BoundingBox,
20}
21
22impl ScrollBuffer {
23    pub fn new() -> Self {
24        Self {
25            lines: Vec::new(),
26            display_height: 0,
27            display_offset: 0,
28            cursor_position: (0, 0),
29            should_render: true,
30            last_bounding_box: BoundingBox::default(),
31        }
32    }
33
34    pub fn push(&mut self, line: Vec<TextSegment>) {
35        self.lines.push(line);
36        // CR: not optimal
37        self.should_render = true;
38    }
39
40    pub fn push_newline(&mut self) {
41        self.push(vec![]);
42    }
43
44    pub fn append(&mut self, lines: &mut Vec<Vec<TextSegment>>) {
45        self.lines.append(lines);
46        // CR: not optimal
47        self.should_render = true;
48    }
49
50    pub fn clear(&mut self) {
51        self.lines.clear();
52        self.should_render = true;
53    }
54
55    pub fn height(&self) -> usize {
56        self.lines.len()
57    }
58
59    pub fn move_cursor(&mut self, delta: isize) {
60        let line_no = max(0, self.cursor_position.1 as isize + delta) as usize;
61        self.move_cursor_to(self.cursor_position.0, line_no);
62    }
63
64    // CR-soon: this API has turned a bit wonky
65    pub fn move_cursor_to(&mut self, x_offset: usize, line_no: usize) {
66        let new_offset = min(line_no, self.lines.len().saturating_sub(1));
67
68        if new_offset < self.display_offset {
69            self.display_offset = new_offset;
70            self.should_render = true;
71        } else if new_offset >= self.display_offset + self.display_height {
72            self.display_offset = new_offset - self.display_height + 1;
73            self.should_render = true;
74        }
75
76        self.cursor_position = (x_offset, new_offset);
77    }
78
79    pub fn get_cursor_line(&self) -> usize {
80        self.cursor_position.1
81    }
82}
83
84impl Render for ScrollBuffer {
85    fn should_render(&self) -> bool {
86        self.should_render
87    }
88
89    fn invalidate(&mut self) {
90        self.should_render = true;
91    }
92
93    fn render(&mut self, stdout: &mut Stdout, bounding_box: BoundingBox) -> Result<()> {
94        if bounding_box != self.last_bounding_box {
95            self.last_bounding_box = bounding_box;
96            self.should_render = true;
97        }
98
99        if self.should_render {
100            let BoundingBox {
101                left,
102                top,
103                width,
104                height,
105            } = bounding_box;
106
107            if self.display_height != height as usize {
108                self.display_height = height as usize;
109                self.move_cursor(0); // NB: recalculate scroll
110            }
111
112            let str_clear = " ".repeat(width as usize);
113            let from_line = min(self.display_offset, self.lines.len());
114            let to_line = min(self.display_offset + self.display_height, self.lines.len());
115
116            for line_no in from_line..to_line {
117                let delta = (line_no - from_line) as u16;
118
119                queue!(stdout, cursor::MoveTo(left, top + delta))?;
120                queue!(stdout, style::ResetColor)?;
121                queue!(stdout, style::SetAttributes(Attributes::default()))?;
122                queue!(stdout, style::Print(&str_clear))?;
123                queue!(stdout, cursor::MoveTo(left, top + delta))?;
124
125                for TextSegment {
126                    colors,
127                    attributes,
128                    text,
129                } in &self.lines[line_no]
130                {
131                    queue!(stdout, style::SetColors(*colors))?;
132                    queue!(stdout, style::SetAttributes(*attributes))?;
133                    queue!(stdout, style::Print(text))?;
134                }
135            }
136
137            stdout.flush()?;
138            self.should_render = false;
139        }
140
141        Ok(())
142    }
143
144    fn get_cursor(&self) -> (u16, u16) {
145        (
146            self.cursor_position.0 as u16,
147            self.cursor_position.1.saturating_sub(self.display_offset) as u16,
148        )
149    }
150}
151
152impl Input for ScrollBuffer {
153    fn handle_focus(&mut self) {
154        ()
155    }
156
157    fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
158        match event.code {
159            KeyCode::Up => self.move_cursor(-1),
160            KeyCode::Down => self.move_cursor(1),
161            _ => return false,
162        }
163        true
164    }
165}
166
167#[derive(Debug, Clone)]
168pub struct TextSegment {
169    colors: Colors,
170    attributes: Attributes,
171    text: String,
172}
173
174impl TextSegment {
175    pub fn new(text: &str, colors: Colors, attributes: Attributes) -> Self {
176        Self {
177            colors,
178            attributes,
179            text: text.to_string(),
180        }
181    }
182
183    pub fn color(text: &str, colors: Colors) -> Self {
184        Self::new(text, colors, Attributes::default())
185    }
186
187    pub fn plain(text: &str) -> Self {
188        Self::new(
189            text,
190            Colors::new(Color::Reset, Color::Reset),
191            Attributes::default(),
192        )
193    }
194}