gm_lib/tui/app/widgets/
text_scroll.rs1use 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}