1use std::io;
29
30use color_eyre::Result;
31use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
32use ratatui::crossterm::execute;
33use ratatui::layout::{Constraint, Layout, Rect};
34use ratatui::style::{Color, Style, Stylize};
35use ratatui::text::Line;
36use ratatui::widgets::{Block, Borders, Paragraph};
37use ratatui::DefaultTerminal;
38use tui_scrollbar::{
39 ScrollBar, ScrollBarArrows, ScrollBarInteraction, ScrollCommand, ScrollLengths, ScrollMetrics,
40 SUBCELL,
41};
42
43const KEY_STEP: usize = 1;
44const TITLE_FG: Color = Color::Rgb(196, 206, 224);
45const TITLE_BG: Color = Color::Rgb(32, 43, 64);
46const BLOCK_FG: Color = Color::Rgb(196, 206, 224);
47const BLOCK_BG: Color = Color::Rgb(13, 23, 38);
48const SCROLLBAR_TRACK_BG: Color = Color::Rgb(40, 40, 40);
49const SCROLLBAR_THUMB_BG: Color = SCROLLBAR_TRACK_BG;
50const SCROLLBAR_THUMB_FG: Color = Color::Rgb(224, 224, 224);
51const SCROLLBAR_ARROW_FG: Color = Color::Rgb(224, 224, 224);
52
53fn main() -> Result<()> {
54 color_eyre::install()?;
55 let mut terminal = ratatui::init();
56 execute!(io::stdout(), event::EnableMouseCapture)?;
57 let result = App::new().run(&mut terminal);
58 execute!(io::stdout(), event::DisableMouseCapture)?;
59 ratatui::restore();
60 result
61}
62
63#[derive(Debug, Default)]
64struct App {
65 state: AppState,
67 layout: Option<LayoutState>,
69 vertical_offset: usize,
71 horizontal_offset: usize,
73 vertical_interaction: ScrollBarInteraction,
75 horizontal_interaction: ScrollBarInteraction,
77}
78
79#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
80enum AppState {
81 #[default]
82 Running,
84 Quit,
86}
87
88#[derive(Debug, Clone, Copy)]
89struct LayoutState {
90 content: Rect,
92 vertical_bar: Rect,
94 horizontal_bar: Rect,
96}
97
98impl App {
99 fn new() -> Self {
101 Self {
102 state: AppState::Running,
103 layout: None,
104 vertical_offset: 0,
105 horizontal_offset: 0,
106 vertical_interaction: ScrollBarInteraction::new(),
107 horizontal_interaction: ScrollBarInteraction::new(),
108 }
109 }
110
111 fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
113 while self.state == AppState::Running {
114 terminal.draw(|frame| self.render(frame))?;
115 self.handle_events()?;
116 }
117 Ok(())
118 }
119
120 fn render(&mut self, frame: &mut ratatui::Frame) {
122 let area = frame.area();
123 if area.width < 2 || area.height < 2 {
124 return;
125 }
126
127 let title = "tui-scrollbar - mouse scroll demo";
128 let block = Block::new()
129 .borders(Borders::TOP)
130 .border_style(Style::new().fg(TITLE_FG).bg(TITLE_BG))
131 .style(Style::new().fg(BLOCK_FG).bg(BLOCK_BG))
132 .title(
133 Line::from(title)
134 .centered()
135 .fg(TITLE_FG)
136 .bg(TITLE_BG)
137 .bold(),
138 );
139 frame.render_widget(&block, area);
140
141 let content_area = Rect {
142 y: area.y.saturating_add(1),
143 height: area.height.saturating_sub(1),
144 ..area
145 };
146 let help = "Arrows: move | Wheel: scroll | Drag: thumb | q/Esc: quit";
147 let help_area = Rect {
148 x: content_area.x.saturating_add(1),
149 y: content_area.y,
150 width: content_area.width.saturating_sub(1),
151 height: 1,
152 };
153 if help_area.width > 0 {
154 frame.render_widget(
155 Paragraph::new(help).style(Style::new().fg(TITLE_FG)),
156 help_area,
157 );
158 }
159 let content_area = Rect {
160 y: content_area.y.saturating_add(1),
161 height: content_area.height.saturating_sub(1),
162 ..content_area
163 };
164
165 let [content_row, bar_row] = content_area.layout(&Layout::vertical([
167 Constraint::Fill(1),
168 Constraint::Length(1),
169 ]));
170 let [content, vertical_bar] = content_row.layout(&Layout::horizontal([
171 Constraint::Fill(1),
172 Constraint::Length(1),
173 ]));
174 let [horizontal_bar, _corner] = bar_row.layout(&Layout::horizontal([
175 Constraint::Fill(1),
176 Constraint::Length(1),
177 ]));
178
179 self.layout = Some(LayoutState {
180 content,
181 vertical_bar,
182 horizontal_bar,
183 });
184
185 let (h_metrics, v_metrics) = self.metrics_for_layout(content);
187 self.horizontal_offset = self.horizontal_offset.min(h_metrics.max_offset());
188 self.vertical_offset = self.vertical_offset.min(v_metrics.max_offset());
189
190 let horizontal_lengths = ScrollLengths {
191 content_len: h_metrics.content_len(),
192 viewport_len: h_metrics.viewport_len(),
193 };
194 let track_style = Style::new().bg(SCROLLBAR_TRACK_BG);
195 let thumb_style = Style::new().fg(SCROLLBAR_THUMB_FG).bg(SCROLLBAR_THUMB_BG);
196 let arrow_style = Style::new().fg(SCROLLBAR_ARROW_FG).bg(SCROLLBAR_TRACK_BG);
197 let horizontal = ScrollBar::horizontal(horizontal_lengths)
198 .arrows(ScrollBarArrows::Both)
199 .offset(self.horizontal_offset)
200 .scroll_step(SUBCELL)
201 .track_style(track_style)
202 .thumb_style(thumb_style)
203 .arrow_style(arrow_style);
204 let vertical_lengths = ScrollLengths {
205 content_len: v_metrics.content_len(),
206 viewport_len: v_metrics.viewport_len(),
207 };
208 let vertical = ScrollBar::vertical(vertical_lengths)
209 .arrows(ScrollBarArrows::Both)
210 .offset(self.vertical_offset)
211 .scroll_step(SUBCELL)
212 .track_style(track_style)
213 .thumb_style(thumb_style)
214 .arrow_style(arrow_style);
215
216 frame.render_widget(&horizontal, horizontal_bar);
217 frame.render_widget(&vertical, vertical_bar);
218 }
219
220 fn handle_events(&mut self) -> Result<()> {
222 match event::read()? {
223 Event::Key(key) => {
224 if key.kind == KeyEventKind::Press {
225 match key.code {
226 KeyCode::Char('q') | KeyCode::Esc => self.state = AppState::Quit,
227 KeyCode::Up => self.handle_key_scroll(0, -(KEY_STEP as isize)),
228 KeyCode::Down => self.handle_key_scroll(0, KEY_STEP as isize),
229 KeyCode::Left => self.handle_key_scroll(-(KEY_STEP as isize), 0),
230 KeyCode::Right => self.handle_key_scroll(KEY_STEP as isize, 0),
231 _ => {}
232 }
233 }
234 }
235 Event::Mouse(event) => {
236 self.handle_mouse_event(event);
237 }
238 _ => {}
239 }
240 Ok(())
241 }
242
243 fn handle_key_scroll(&mut self, dx: isize, dy: isize) {
245 let Some(layout) = self.layout else {
246 return;
247 };
248 let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
249 self.horizontal_offset =
250 Self::apply_delta(self.horizontal_offset, dx, h_metrics.max_offset());
251 self.vertical_offset = Self::apply_delta(self.vertical_offset, dy, v_metrics.max_offset());
252 }
253
254 fn handle_mouse_event(&mut self, event: event::MouseEvent) {
256 let Some(layout) = self.layout else {
257 return;
258 };
259 let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
260 let horizontal = self.horizontal_scrollbar(h_metrics);
261 let vertical = self.vertical_scrollbar(v_metrics);
262
263 if let Some(command) = horizontal.handle_mouse_event(
264 layout.horizontal_bar,
265 event,
266 &mut self.horizontal_interaction,
267 ) {
268 self.apply_command(command, true);
269 }
270 if let Some(command) =
271 vertical.handle_mouse_event(layout.vertical_bar, event, &mut self.vertical_interaction)
272 {
273 self.apply_command(command, false);
274 }
275 }
276
277 fn apply_command(&mut self, command: ScrollCommand, is_horizontal: bool) {
279 let ScrollCommand::SetOffset(offset) = command;
280 if is_horizontal {
281 self.horizontal_offset = offset;
282 } else {
283 self.vertical_offset = offset;
284 }
285 }
286
287 fn horizontal_scrollbar(&self, metrics: ScrollMetrics) -> ScrollBar {
289 let lengths = ScrollLengths {
290 content_len: metrics.content_len(),
291 viewport_len: metrics.viewport_len(),
292 };
293 ScrollBar::horizontal(lengths)
294 .arrows(ScrollBarArrows::Both)
295 .offset(self.horizontal_offset)
296 .scroll_step(SUBCELL)
297 }
298
299 fn vertical_scrollbar(&self, metrics: ScrollMetrics) -> ScrollBar {
301 let lengths = ScrollLengths {
302 content_len: metrics.content_len(),
303 viewport_len: metrics.viewport_len(),
304 };
305 ScrollBar::vertical(lengths)
306 .arrows(ScrollBarArrows::Both)
307 .offset(self.vertical_offset)
308 .scroll_step(SUBCELL)
309 }
310
311 fn metrics_for_layout(&self, content: Rect) -> (ScrollMetrics, ScrollMetrics) {
313 let h_cells = content.width.max(1) as usize;
315 let v_cells = content.height.max(1) as usize;
316 let h_content = h_cells.saturating_mul(SUBCELL).max(1);
317 let v_content = v_cells.saturating_mul(SUBCELL).max(1);
318 let h_viewport = h_content.saturating_sub(100).max(1);
319 let v_viewport = v_content.saturating_sub(100).max(1);
320 (
321 ScrollMetrics::new(
322 ScrollLengths {
323 content_len: h_content,
324 viewport_len: h_viewport,
325 },
326 self.horizontal_offset,
327 content.width,
328 ),
329 ScrollMetrics::new(
330 ScrollLengths {
331 content_len: v_content,
332 viewport_len: v_viewport,
333 },
334 self.vertical_offset,
335 content.height,
336 ),
337 )
338 }
339
340 fn apply_delta(current: usize, delta: isize, max: usize) -> usize {
342 if delta < 0 {
343 current.saturating_sub(delta.unsigned_abs())
344 } else {
345 current.saturating_add(delta as usize).min(max)
346 }
347 }
348}