ratatui_toolkit/clickable_scrollbar/
mod.rs1use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
6use ratatui::buffer::Buffer;
7use ratatui::layout::Rect;
8use ratatui::style::Style;
9use ratatui::symbols;
10use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget};
11
12#[derive(Debug, Default, Clone)]
16pub struct ClickableScrollbar<'a> {
17 orientation: ScrollbarOrientation,
18 scrollbar: Scrollbar<'a>,
19}
20
21#[derive(Debug, Clone)]
25pub struct ClickableScrollbarState {
26 pub area: Rect,
28 pub orientation: ScrollbarOrientation,
30 pub offset: usize,
32 pub page_len: usize,
34 pub max_offset: usize,
36 pub scroll_by: Option<usize>,
38 drag_active: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ScrollbarEvent {
45 None,
47 Up(usize),
49 Down(usize),
51 Position(usize),
53}
54
55impl<'a> ClickableScrollbar<'a> {
56 pub fn new(orientation: ScrollbarOrientation) -> Self {
57 Self {
58 orientation: orientation.clone(),
59 scrollbar: Scrollbar::new(orientation),
60 }
61 }
62
63 pub fn vertical() -> Self {
65 Self::new(ScrollbarOrientation::VerticalRight)
66 }
67
68 pub fn horizontal() -> Self {
70 Self::new(ScrollbarOrientation::HorizontalBottom)
71 }
72
73 pub fn style(mut self, style: Style) -> Self {
75 self.scrollbar = self.scrollbar.style(style);
76 self
77 }
78
79 pub fn thumb_symbol(mut self, symbol: &'a str) -> Self {
81 self.scrollbar = self.scrollbar.thumb_symbol(symbol);
82 self
83 }
84
85 pub fn thumb_style(mut self, style: Style) -> Self {
87 self.scrollbar = self.scrollbar.thumb_style(style);
88 self
89 }
90
91 pub fn track_symbol(mut self, symbol: Option<&'a str>) -> Self {
93 self.scrollbar = self.scrollbar.track_symbol(symbol);
94 self
95 }
96
97 pub fn track_style(mut self, style: Style) -> Self {
99 self.scrollbar = self.scrollbar.track_style(style);
100 self
101 }
102
103 pub fn begin_symbol(mut self, symbol: Option<&'a str>) -> Self {
105 self.scrollbar = self.scrollbar.begin_symbol(symbol);
106 self
107 }
108
109 pub fn begin_style(mut self, style: Style) -> Self {
111 self.scrollbar = self.scrollbar.begin_style(style);
112 self
113 }
114
115 pub fn end_symbol(mut self, symbol: Option<&'a str>) -> Self {
117 self.scrollbar = self.scrollbar.end_symbol(symbol);
118 self
119 }
120
121 pub fn end_style(mut self, style: Style) -> Self {
123 self.scrollbar = self.scrollbar.end_style(style);
124 self
125 }
126
127 pub fn symbols(mut self, symbols: symbols::scrollbar::Set) -> Self {
129 self.scrollbar = self.scrollbar.symbols(symbols);
130 self
131 }
132}
133
134impl<'a> StatefulWidget for ClickableScrollbar<'a> {
135 type State = ClickableScrollbarState;
136
137 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
138 state.area = area;
139 state.orientation = self.orientation;
140
141 if area.is_empty() {
142 return;
143 }
144
145 let mut scrollbar_state = ScrollbarState::new(state.max_offset)
146 .position(state.offset)
147 .viewport_content_length(state.page_len);
148
149 self.scrollbar.render(area, buf, &mut scrollbar_state);
150 }
151}
152
153impl Default for ClickableScrollbarState {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl ClickableScrollbarState {
160 pub fn new() -> Self {
162 Self {
163 area: Rect::default(),
164 orientation: ScrollbarOrientation::VerticalRight,
165 offset: 0,
166 page_len: 0,
167 max_offset: 0,
168 scroll_by: None,
169 drag_active: false,
170 }
171 }
172
173 pub fn set_content(mut self, content_len: usize, page_len: usize) -> Self {
175 self.page_len = page_len;
176 self.max_offset = content_len.saturating_sub(page_len);
177 self
178 }
179
180 pub fn position(mut self, offset: usize) -> Self {
182 self.offset = offset.min(self.max_offset);
183 self
184 }
185
186 pub fn offset(&self) -> usize {
188 self.offset
189 }
190
191 pub fn set_offset(&mut self, offset: usize) -> bool {
193 let old = self.offset;
194 self.offset = offset.min(self.max_offset);
195 old != self.offset
196 }
197
198 pub fn scroll_up(&mut self, n: usize) -> bool {
200 let old = self.offset;
201 self.offset = self.offset.saturating_sub(n);
202 old != self.offset
203 }
204
205 pub fn scroll_down(&mut self, n: usize) -> bool {
207 let old = self.offset;
208 self.offset = (self.offset + n).min(self.max_offset);
209 old != self.offset
210 }
211
212 pub fn scroll_increment(&self) -> usize {
214 self.scroll_by
215 .unwrap_or_else(|| (self.page_len / 10).max(1))
216 }
217
218 pub fn handle_mouse_event(&mut self, event: &MouseEvent) -> ScrollbarEvent {
220 let (col, row) = (event.column, event.row);
221
222 if !self.area.contains((col, row).into()) {
223 if self.drag_active {
224 self.drag_active = false;
225 }
226 return ScrollbarEvent::None;
227 }
228
229 match event.kind {
230 MouseEventKind::ScrollDown => {
231 if self.is_vertical() {
232 ScrollbarEvent::Down(self.scroll_increment())
233 } else {
234 ScrollbarEvent::None
235 }
236 }
237 MouseEventKind::ScrollUp => {
238 if self.is_vertical() {
239 ScrollbarEvent::Up(self.scroll_increment())
240 } else {
241 ScrollbarEvent::None
242 }
243 }
244 MouseEventKind::Down(MouseButton::Left) => {
245 self.drag_active = true;
246 let pos = self.map_position_to_offset(col, row);
247 ScrollbarEvent::Position(pos)
248 }
249 MouseEventKind::Drag(MouseButton::Left) if self.drag_active => {
250 let pos = self.map_position_to_offset(col, row);
251 ScrollbarEvent::Position(pos)
252 }
253 MouseEventKind::Up(MouseButton::Left) => {
254 self.drag_active = false;
255 ScrollbarEvent::None
256 }
257 _ => ScrollbarEvent::None,
258 }
259 }
260
261 fn map_position_to_offset(&self, col: u16, row: u16) -> usize {
263 if self.is_vertical() {
264 let pos = row.saturating_sub(self.area.y).saturating_sub(1) as usize;
265 let span = self.area.height.saturating_sub(2) as usize;
266
267 if span > 0 {
268 (self.max_offset * pos) / span
269 } else {
270 0
271 }
272 } else {
273 let pos = col.saturating_sub(self.area.x).saturating_sub(1) as usize;
274 let span = self.area.width.saturating_sub(2) as usize;
275
276 if span > 0 {
277 (self.max_offset * pos) / span
278 } else {
279 0
280 }
281 }
282 }
283
284 fn is_vertical(&self) -> bool {
286 matches!(
287 self.orientation,
288 ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft
289 )
290 }
291}