hefesto_widgets/scroll_list/
mod.rs1use ratatui::{
2 buffer::Buffer,
3 layout::{Constraint, Layout, Rect},
4 style::Style,
5 widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget, Widget},
6};
7
8use crate::{default_scrollbar, default_scrollbar_state, BORDER_GRAY, DEFAULT_HIGHLIGHT, HIGHLIGHT_SYMBOL};
9
10#[derive(Clone)]
11pub struct ScrollListState {
12 pub list_state: ListState,
13 pub follow: bool,
14}
15
16impl Default for ScrollListState {
17 fn default() -> Self {
18 Self {
19 list_state: ListState::default(),
20 follow: true,
21 }
22 }
23}
24
25impl ScrollListState {
26 pub fn select(&mut self, index: Option<usize>) {
27 self.list_state.select(index);
28 }
29
30 pub fn next(&mut self, item_count: usize) {
31 let i = self.list_state.selected().map_or(0, |s| s + 1);
32 let max = item_count.saturating_sub(1);
33 self.list_state.select(Some(i.min(max)));
34 }
35
36 pub fn previous(&mut self) {
37 let i = self.list_state.selected().map_or(0, |s| s.saturating_sub(1));
38 self.list_state.select(Some(i));
39 }
40
41 pub fn last(&mut self, item_count: usize) {
42 self.list_state.select(Some(item_count.saturating_sub(1)));
43 }
44}
45
46pub struct ScrollList<'a> {
52 items: Vec<ListItem<'a>>,
53 highlight_style: Style,
54 highlight_symbol: String,
55 borders: Borders,
56 border_style: Style,
57}
58
59impl<'a> ScrollList<'a> {
60 pub fn new(items: Vec<String>) -> Self {
61 Self {
62 items: items
63 .into_iter()
64 .map(|s| ListItem::new(s))
65 .collect(),
66 highlight_style: DEFAULT_HIGHLIGHT,
67 highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
68 borders: Borders::NONE,
69 border_style: Style::new().fg(BORDER_GRAY),
70 }
71 }
72
73 pub fn new_styled(items: Vec<(String, Style)>) -> Self {
74 Self {
75 items: items
76 .into_iter()
77 .map(|(s, style)| ListItem::new(s).style(style))
78 .collect(),
79 highlight_style: DEFAULT_HIGHLIGHT,
80 highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
81 borders: Borders::NONE,
82 border_style: Style::new().fg(BORDER_GRAY),
83 }
84 }
85
86 pub fn new_list(items: Vec<ListItem<'a>>) -> Self {
89 Self {
90 items,
91 highlight_style: DEFAULT_HIGHLIGHT,
92 highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
93 borders: Borders::NONE,
94 border_style: Style::new().fg(BORDER_GRAY),
95 }
96 }
97
98 pub fn style(mut self, style: Style) -> Self {
99 for item in &mut self.items {
100 *item = item.clone().style(style);
101 }
102 self
103 }
104
105 pub fn highlight_style(mut self, style: Style) -> Self {
106 self.highlight_style = style;
107 self
108 }
109
110 pub fn highlight_symbol(mut self, symbol: &str) -> Self {
111 self.highlight_symbol = symbol.to_string();
112 self
113 }
114
115 pub fn borders(mut self, borders: Borders) -> Self {
116 self.borders = borders;
117 self
118 }
119
120 pub fn border_style(mut self, style: Style) -> Self {
121 self.border_style = style;
122 self
123 }
124}
125
126impl StatefulWidget for ScrollList<'_> {
127 type State = ScrollListState;
128
129 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
130 if state.follow && !self.items.is_empty() {
131 state
132 .list_state
133 .select(Some(self.items.len().saturating_sub(1)));
134 }
135
136 let inner = if self.borders != Borders::NONE {
137 let block = Block::bordered()
138 .borders(self.borders)
139 .border_style(self.border_style);
140 let inner = block.inner(area);
141 block.render(area, buf);
142 inner
143 } else {
144 area
145 };
146
147 let hchunks = Layout::horizontal([
148 Constraint::Min(1),
149 Constraint::Length(1),
150 ])
151 .split(inner);
152
153 let list_area = hchunks[0];
154 let scrollbar_area = hchunks[1];
155 let total = self.items.len();
156 let mut list = List::new(self.items)
157 .highlight_style(self.highlight_style)
158 .scroll_padding(1);
159 if !self.highlight_symbol.is_empty() {
160 list = list.highlight_symbol(self.highlight_symbol);
161 }
162
163 StatefulWidget::render(list, list_area, buf, &mut state.list_state);
164
165 if state.list_state.selected().is_none() && total > 0 {
166 state.list_state.select(Some(0));
167 }
168
169 if total > list_area.height as usize {
170 let visible = list_area.height as usize;
171 let max_offset = total.saturating_sub(visible);
172 let offset = state.list_state.offset();
173 let position = if max_offset > 0 {
174 offset * (total - 1) / max_offset
175 } else {
176 0
177 };
178 let mut scrollbar_state = default_scrollbar_state(total, position);
179 StatefulWidget::render(
180 default_scrollbar(),
181 scrollbar_area,
182 buf,
183 &mut scrollbar_state,
184 );
185 }
186 }
187}
188#[cfg(test)]
189mod tests;