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