mudrs_milk/widget/
scrollback.rs1use std::{
2 sync::Arc,
3 time::Instant,
4};
5
6use crossterm::event::{
7 Event,
8 KeyCode,
9 KeyEvent,
10 KeyModifiers,
11 MouseEvent,
12 MouseEventKind,
13};
14use futures::{
15 channel::mpsc,
16 lock::Mutex,
17 prelude::*,
18};
19use textwrap::core::Fragment;
20use tracing::{
21 debug,
22 instrument,
23};
24use tui::{
25 buffer::Buffer,
26 layout::Rect,
27 text::StyledGrapheme,
28 widgets::Widget,
29};
30
31use crate::text::{
32 Line,
33 Text,
34};
35
36#[derive(Debug, Default)]
37pub struct Scroll {
38 pub y: Option<usize>,
39 pub x: Option<usize>,
40}
41
42pub struct ScrollbackBuffer {
43 pub lines: Vec<Line>,
44 pub current: Text,
45 pub scroll: Scroll,
46 pub wrap: bool,
47 pub last_height: usize,
48 pub render_tx: mpsc::Sender<Instant>,
49}
50
51impl ScrollbackBuffer {
52 pub fn new(render_tx: mpsc::Sender<Instant>) -> Self {
53 ScrollbackBuffer {
54 lines: vec![],
55 current: Text::new(),
56 scroll: Default::default(),
57 last_height: 20,
58 wrap: true,
59 render_tx,
60 }
61 }
62
63 pub fn append_lines(this: Arc<Mutex<Self>>, mut lines: mpsc::Receiver<(Vec<Line>, Text)>) {
64 tokio::spawn(async move {
65 while let Some(line) = lines.next().await {
66 let mut this = this.lock().await;
67 this.extend(line);
68 let _ = this.render_tx.send(Instant::now()).await;
69 }
70 });
71 }
72
73 #[instrument(level = "trace", skip(self))]
74 pub async fn handle_user_event(&mut self, event: &Event) -> bool {
75 match event {
76 Event::Key(KeyEvent {
77 code,
78 modifiers: KeyModifiers::NONE,
79 }) => match code {
80 KeyCode::PageDown => self.page_down(),
81 KeyCode::PageUp => self.page_up(),
82 _ => return false,
83 },
84 Event::Mouse(MouseEvent { kind, .. }) => match kind {
85 MouseEventKind::ScrollDown => self.scroll_down(1),
86 MouseEventKind::ScrollUp => self.scroll_up(1),
87 _ => return false,
88 },
89 _ => return false,
90 }
91 let _ = self.render_tx.send(Instant::now()).await;
92 true
93 }
94
95 pub fn scroll_up(&mut self, by: usize) {
96 let scroll_y = self.scroll.y.get_or_insert(self.lines.len() + 1);
98 *scroll_y = scroll_y.saturating_sub(by);
99 }
100
101 pub fn scroll_down(&mut self, by: usize) {
102 if let Some(mut y) = self.scroll.y.take() {
103 y += by;
104 if y <= self.lines.len() {
105 self.scroll.y = Some(y);
106 }
107 }
108 }
109
110 pub fn page_up(&mut self) {
111 self.scroll_up(self.last_height);
112 }
113
114 pub fn page_down(&mut self) {
115 self.scroll_down(self.last_height);
116 }
117
118 pub fn extend(&mut self, (lines, mut incomplete): (impl IntoIterator<Item = Line>, Text)) {
119 for line in lines {
120 let full_line = self.current.swap_complete(line);
121 self.lines.push(full_line);
122 }
123
124 self.current.extend(&mut incomplete)
125 }
126
127 pub fn widget(&self) -> ScrollbackBufferWidget {
128 let scroll = self.scroll.y;
129 ScrollbackBufferWidget {
130 lines: &self.lines[..scroll.unwrap_or(self.lines.len())],
131 last: Some(&self.current).filter(|_| scroll.is_none()),
132 x: if !self.wrap {
133 self.scroll.x.unwrap_or_default()
134 } else {
135 0
136 },
137 wrap: self.wrap,
138 }
139 }
140}
141
142pub struct ScrollbackBufferWidget<'a> {
143 pub lines: &'a [Line],
144 pub last: Option<&'a Text>,
145 pub x: usize,
146 pub wrap: bool,
147}
148
149impl<'a> Widget for ScrollbackBufferWidget<'a> {
150 fn render(self, area: Rect, buf: &mut Buffer) {
151 let max_buffer = &self.lines[self.lines.len().saturating_sub(area.height as usize)..];
152 let mut out_lines = vec![];
153 let lines = max_buffer
154 .iter()
155 .map(|l| l.spans())
156 .chain(self.last.iter().map(|t| t.spans()))
157 .map(|l| {
158 l.iter()
159 .flat_map(|s| s.styled_graphemes(Default::default()))
160 });
161
162 if self.wrap {
163 for line in lines {
165 let words = WordsIter::new(line).collect::<Vec<_>>();
166 let lines =
167 textwrap::wrap_algorithms::wrap_first_fit(&words, &[area.width as usize; 512]);
168 out_lines.extend(lines.into_iter().map(|words| {
169 words
170 .iter()
171 .flat_map(|word| word.graphemes.iter().cloned())
172 .collect::<Vec<StyledGrapheme>>()
173 }))
174 }
175 } else {
176 out_lines.extend(lines.map(Vec::from_iter));
177 }
178 let out_lines = &out_lines[out_lines.len().saturating_sub(area.height as usize)..];
179 let y_offset = area.height.saturating_sub(out_lines.len() as _);
180 for (mut y, line) in out_lines.iter().enumerate() {
181 y += y_offset as usize;
182 if y > area.height as usize {
183 break;
184 }
185 for (x, g) in line.iter().enumerate() {
186 if x >= area.width as usize {
187 break;
188 }
189 buf.get_mut(area.x + x as u16, area.y + y as u16)
190 .set_style(g.style.clone())
191 .set_symbol(g.symbol);
192 }
193 }
194 }
195}
196
197#[derive(Default, Debug, Clone)]
198struct StyledWord<'a> {
199 graphemes: Vec<StyledGrapheme<'a>>,
200 word_len: usize,
201 trailing_ws_len: usize,
202}
203
204impl<'a> StyledWord<'a> {
205 fn is_complete_word(&self) -> bool {
206 self.graphemes.iter().any(|g| !is_whitespace(g))
207 && self.graphemes.last().map(is_whitespace).unwrap_or(false)
208 }
209
210 fn push(&mut self, g: StyledGrapheme<'a>) {
211 let ws = is_whitespace(&g);
212 let existing_word = self.word_len > 0;
213 if !ws {
214 self.word_len += 1;
215 } else if existing_word {
216 self.trailing_ws_len += 1;
217 }
218 self.graphemes.push(g);
219 }
220}
221
222struct WordsIter<'a, I> {
223 graphemes: I,
224 current: Option<StyledWord<'a>>,
225}
226
227impl<'a, I> WordsIter<'a, I> {
228 fn new(graphemes: I) -> Self {
229 Self {
230 graphemes,
231 current: None,
232 }
233 }
234}
235
236impl<'a, I> Iterator for WordsIter<'a, I>
237where
238 I: Iterator<Item = StyledGrapheme<'a>>,
239{
240 type Item = StyledWord<'a>;
241
242 fn next(&mut self) -> Option<Self::Item> {
243 loop {
244 let g = if let Some(g) = self.graphemes.next() {
245 g
246 } else {
247 return self.current.take();
248 };
249
250 let whitespace = is_whitespace(&g);
251
252 let current = self.current.get_or_insert_with(Default::default);
253
254 if current.is_complete_word() {
255 if !whitespace {
256 let word = self.current.take();
257 let mut new = StyledWord::default();
258 new.push(g);
259 self.current = Some(new);
260 return word;
261 }
262 }
263
264 current.push(g);
265 }
266 }
267}
268
269fn is_whitespace(g: &StyledGrapheme) -> bool {
270 g.symbol
271 .chars()
272 .nth(0)
273 .map(|c| c.is_whitespace())
274 .unwrap_or(true)
275}
276
277impl<'a> Fragment for StyledWord<'a> {
278 fn whitespace_width(&self) -> usize {
279 self.trailing_ws_len
280 }
281 fn penalty_width(&self) -> usize {
282 0
283 }
284 fn width(&self) -> usize {
285 self.graphemes.len() - self.trailing_ws_len
286 }
287}