twitter_tool/ui_framework/
scroll_buffer.rs1use 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 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 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 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 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); }
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}