mudrs_milk/widget/
scrollback.rs

1use 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        // Start the scrolling at lines.len()+1 to account for an incomplete line
97        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            // TODO: Cache this and only recalculate when the screen width changes.
164            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}