Skip to main content

gm_lib/tui/app/widgets/
text_scroll.rs

1use crossterm::event::KeyCode;
2use ratatui::{
3    layout::{Constraint, Layout, Rect},
4    widgets::Widget,
5};
6
7use crate::tui::{
8    traits::{HandleResult, RectUtil},
9    Event,
10};
11
12use super::scroll_bar::CustomScrollBar;
13
14#[derive(Default)]
15pub struct TextScroll {
16    pub text: String,
17    pub scroll_offset: usize,
18}
19
20impl TextScroll {
21    pub fn new(text: String) -> Self {
22        Self {
23            text,
24            scroll_offset: 0,
25        }
26    }
27
28    fn lines(&self, width: usize) -> Vec<&str> {
29        self.text
30            .lines()
31            .flat_map(|line| split_str_by_width(line, width))
32            .collect()
33    }
34
35    pub fn scroll_up(&mut self) {
36        if self.scroll_offset > 0 {
37            self.scroll_offset -= 1;
38        }
39    }
40
41    pub fn scroll_down(&mut self, width: usize, height: usize) {
42        let lines = self.lines(width).len();
43        if self.scroll_offset + height < lines {
44            self.scroll_offset += 1;
45        }
46    }
47
48    pub fn get_visible_text(&self, area: Rect) -> (Vec<&str>, usize) {
49        let lines: Vec<&str> = self.lines(area.width as usize);
50        (
51            lines
52                .iter()
53                .skip(self.scroll_offset)
54                .take(area.height as usize)
55                .map(|line| line.trim_end())
56                .collect(),
57            lines.len(),
58        )
59    }
60
61    pub fn handle_event(
62        &mut self,
63        event: &Event,
64        area: ratatui::prelude::Rect,
65    ) -> crate::Result<HandleResult> {
66        #[allow(clippy::single_match)]
67        match event {
68            Event::Input(key) => match key.code {
69                KeyCode::Up => {
70                    self.scroll_up();
71                }
72                KeyCode::Down => {
73                    self.scroll_down(area.width as usize, area.height as usize);
74                }
75                _ => {}
76            },
77            _ => {}
78        }
79        Ok(HandleResult::default())
80    }
81}
82
83fn split_str_by_width(s: &str, width: usize) -> Vec<&str> {
84    let mut result = Vec::new();
85    let mut start = 0;
86    let mut count = 0;
87
88    for (i, _) in s.char_indices() {
89        if count == width {
90            result.push(&s[start..i]);
91            start = i;
92            count = 0;
93        }
94        count += 1;
95    }
96
97    if start < s.len() {
98        result.push(&s[start..]);
99    }
100
101    result
102}
103
104impl Widget for &TextScroll {
105    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
106    where
107        Self: Sized,
108    {
109        let [mut text_area, scroll_area] =
110            Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).areas(area);
111
112        let (lines, total) = self.get_visible_text(text_area);
113        if total > area.height as usize {
114            for line in &lines {
115                line.render(text_area, buf);
116                let Ok(text_area_new) = text_area.consume_height(1) else {
117                    return;
118                };
119                text_area = text_area_new;
120            }
121
122            CustomScrollBar {
123                cursor: self.scroll_offset,
124                total: total - area.height as usize,
125            }
126            .render(scroll_area, buf);
127        } else {
128            for line in &lines {
129                line.render(text_area, buf);
130                let Ok(text_area_new) = text_area.consume_height(1) else {
131                    return;
132                };
133                text_area = text_area_new;
134            }
135        }
136    }
137}