use std::io;
use color_eyre::Result;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::crossterm::execute;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style, Stylize};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::DefaultTerminal;
use tui_scrollbar::{
ScrollBar, ScrollBarArrows, ScrollBarInteraction, ScrollCommand, ScrollLengths, ScrollMetrics,
SUBCELL,
};
const KEY_STEP: usize = 1;
const TITLE_FG: Color = Color::Rgb(196, 206, 224);
const TITLE_BG: Color = Color::Rgb(32, 43, 64);
const BLOCK_FG: Color = Color::Rgb(196, 206, 224);
const BLOCK_BG: Color = Color::Rgb(13, 23, 38);
const SCROLLBAR_TRACK_BG: Color = Color::Rgb(40, 40, 40);
const SCROLLBAR_THUMB_BG: Color = SCROLLBAR_TRACK_BG;
const SCROLLBAR_THUMB_FG: Color = Color::Rgb(224, 224, 224);
const SCROLLBAR_ARROW_FG: Color = Color::Rgb(224, 224, 224);
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
execute!(io::stdout(), event::EnableMouseCapture)?;
let result = App::new().run(&mut terminal);
execute!(io::stdout(), event::DisableMouseCapture)?;
ratatui::restore();
result
}
#[derive(Debug, Default)]
struct App {
state: AppState,
layout: Option<LayoutState>,
vertical_offset: usize,
horizontal_offset: usize,
vertical_interaction: ScrollBarInteraction,
horizontal_interaction: ScrollBarInteraction,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quit,
}
#[derive(Debug, Clone, Copy)]
struct LayoutState {
content: Rect,
vertical_bar: Rect,
horizontal_bar: Rect,
}
impl App {
fn new() -> Self {
Self {
state: AppState::Running,
layout: None,
vertical_offset: 0,
horizontal_offset: 0,
vertical_interaction: ScrollBarInteraction::new(),
horizontal_interaction: ScrollBarInteraction::new(),
}
}
fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
while self.state == AppState::Running {
terminal.draw(|frame| self.render(frame))?;
self.handle_events()?;
}
Ok(())
}
fn render(&mut self, frame: &mut ratatui::Frame) {
let area = frame.area();
if area.width < 2 || area.height < 2 {
return;
}
let title = "tui-scrollbar - mouse scroll demo";
let block = Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(TITLE_FG).bg(TITLE_BG))
.style(Style::new().fg(BLOCK_FG).bg(BLOCK_BG))
.title(
Line::from(title)
.centered()
.fg(TITLE_FG)
.bg(TITLE_BG)
.bold(),
);
frame.render_widget(&block, area);
let content_area = Rect {
y: area.y.saturating_add(1),
height: area.height.saturating_sub(1),
..area
};
let help = "Arrows: move | Wheel: scroll | Drag: thumb | q/Esc: quit";
let help_area = Rect {
x: content_area.x.saturating_add(1),
y: content_area.y,
width: content_area.width.saturating_sub(1),
height: 1,
};
if help_area.width > 0 {
frame.render_widget(
Paragraph::new(help).style(Style::new().fg(TITLE_FG)),
help_area,
);
}
let content_area = Rect {
y: content_area.y.saturating_add(1),
height: content_area.height.saturating_sub(1),
..content_area
};
let [content_row, bar_row] = content_area.layout(&Layout::vertical([
Constraint::Fill(1),
Constraint::Length(1),
]));
let [content, vertical_bar] = content_row.layout(&Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(1),
]));
let [horizontal_bar, _corner] = bar_row.layout(&Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(1),
]));
self.layout = Some(LayoutState {
content,
vertical_bar,
horizontal_bar,
});
let (h_metrics, v_metrics) = self.metrics_for_layout(content);
self.horizontal_offset = self.horizontal_offset.min(h_metrics.max_offset());
self.vertical_offset = self.vertical_offset.min(v_metrics.max_offset());
let horizontal_lengths = ScrollLengths {
content_len: h_metrics.content_len(),
viewport_len: h_metrics.viewport_len(),
};
let track_style = Style::new().bg(SCROLLBAR_TRACK_BG);
let thumb_style = Style::new().fg(SCROLLBAR_THUMB_FG).bg(SCROLLBAR_THUMB_BG);
let arrow_style = Style::new().fg(SCROLLBAR_ARROW_FG).bg(SCROLLBAR_TRACK_BG);
let horizontal = ScrollBar::horizontal(horizontal_lengths)
.arrows(ScrollBarArrows::Both)
.offset(self.horizontal_offset)
.scroll_step(SUBCELL)
.track_style(track_style)
.thumb_style(thumb_style)
.arrow_style(arrow_style);
let vertical_lengths = ScrollLengths {
content_len: v_metrics.content_len(),
viewport_len: v_metrics.viewport_len(),
};
let vertical = ScrollBar::vertical(vertical_lengths)
.arrows(ScrollBarArrows::Both)
.offset(self.vertical_offset)
.scroll_step(SUBCELL)
.track_style(track_style)
.thumb_style(thumb_style)
.arrow_style(arrow_style);
frame.render_widget(&horizontal, horizontal_bar);
frame.render_widget(&vertical, vertical_bar);
}
fn handle_events(&mut self) -> Result<()> {
match event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.state = AppState::Quit,
KeyCode::Up => self.handle_key_scroll(0, -(KEY_STEP as isize)),
KeyCode::Down => self.handle_key_scroll(0, KEY_STEP as isize),
KeyCode::Left => self.handle_key_scroll(-(KEY_STEP as isize), 0),
KeyCode::Right => self.handle_key_scroll(KEY_STEP as isize, 0),
_ => {}
}
}
}
Event::Mouse(event) => {
self.handle_mouse_event(event);
}
_ => {}
}
Ok(())
}
fn handle_key_scroll(&mut self, dx: isize, dy: isize) {
let Some(layout) = self.layout else {
return;
};
let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
self.horizontal_offset =
Self::apply_delta(self.horizontal_offset, dx, h_metrics.max_offset());
self.vertical_offset = Self::apply_delta(self.vertical_offset, dy, v_metrics.max_offset());
}
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
let Some(layout) = self.layout else {
return;
};
let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
let horizontal = self.horizontal_scrollbar(h_metrics);
let vertical = self.vertical_scrollbar(v_metrics);
if let Some(command) = horizontal.handle_mouse_event(
layout.horizontal_bar,
event,
&mut self.horizontal_interaction,
) {
self.apply_command(command, true);
}
if let Some(command) =
vertical.handle_mouse_event(layout.vertical_bar, event, &mut self.vertical_interaction)
{
self.apply_command(command, false);
}
}
fn apply_command(&mut self, command: ScrollCommand, is_horizontal: bool) {
let ScrollCommand::SetOffset(offset) = command;
if is_horizontal {
self.horizontal_offset = offset;
} else {
self.vertical_offset = offset;
}
}
fn horizontal_scrollbar(&self, metrics: ScrollMetrics) -> ScrollBar {
let lengths = ScrollLengths {
content_len: metrics.content_len(),
viewport_len: metrics.viewport_len(),
};
ScrollBar::horizontal(lengths)
.arrows(ScrollBarArrows::Both)
.offset(self.horizontal_offset)
.scroll_step(SUBCELL)
}
fn vertical_scrollbar(&self, metrics: ScrollMetrics) -> ScrollBar {
let lengths = ScrollLengths {
content_len: metrics.content_len(),
viewport_len: metrics.viewport_len(),
};
ScrollBar::vertical(lengths)
.arrows(ScrollBarArrows::Both)
.offset(self.vertical_offset)
.scroll_step(SUBCELL)
}
fn metrics_for_layout(&self, content: Rect) -> (ScrollMetrics, ScrollMetrics) {
let h_cells = content.width.max(1) as usize;
let v_cells = content.height.max(1) as usize;
let h_content = h_cells.saturating_mul(SUBCELL).max(1);
let v_content = v_cells.saturating_mul(SUBCELL).max(1);
let h_viewport = h_content.saturating_sub(100).max(1);
let v_viewport = v_content.saturating_sub(100).max(1);
(
ScrollMetrics::new(
ScrollLengths {
content_len: h_content,
viewport_len: h_viewport,
},
self.horizontal_offset,
content.width,
),
ScrollMetrics::new(
ScrollLengths {
content_len: v_content,
viewport_len: v_viewport,
},
self.vertical_offset,
content.height,
),
)
}
fn apply_delta(current: usize, delta: isize, max: usize) -> usize {
if delta < 0 {
current.saturating_sub(delta.unsigned_abs())
} else {
current.saturating_add(delta as usize).min(max)
}
}
}