1use color_eyre::Result;
21use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
22use ratatui::layout::{Constraint, Layout, Rect};
23use ratatui::widgets::Paragraph;
24use ratatui::DefaultTerminal;
25use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths, ScrollMetrics, SUBCELL};
26
27fn main() -> Result<()> {
28 color_eyre::install()?;
29 let terminal = ratatui::init();
30 let result = App::new().run(terminal);
31 ratatui::restore();
32 result
33}
34
35#[derive(Debug, Default)]
36struct App {
37 state: AppState,
39}
40
41#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
42enum AppState {
43 #[default]
44 Running,
46 Quit,
48}
49
50impl App {
51 const fn new() -> Self {
53 Self {
54 state: AppState::Running,
55 }
56 }
57
58 fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
60 while self.state == AppState::Running {
61 terminal.draw(|frame| {
62 render_scrollbars(frame.area(), frame);
63 })?;
64 self.handle_events()?;
65 }
66 Ok(())
67 }
68
69 fn handle_events(&mut self) -> Result<()> {
71 if let Event::Key(key) = event::read()? {
72 if key.kind == KeyEventKind::Press
73 && matches!(key.code, KeyCode::Char('q') | KeyCode::Esc)
74 {
75 self.state = AppState::Quit;
76 }
77 }
78 Ok(())
79 }
80}
81
82fn render_scrollbars(area: Rect, frame: &mut ratatui::Frame) {
84 if area.height < 8 {
85 return;
86 }
87
88 let title = "Fractional scrollbar steps (q/Esc to quit)";
89 frame.render_widget(Paragraph::new(title), area);
90
91 let content_area = Rect {
92 y: area.y.saturating_add(1),
93 height: area.height.saturating_sub(1),
94 ..area
95 };
96 if content_area.height == 0 {
97 return;
98 }
99
100 let min_left_width = 12;
102 let max_right_width = 68;
103 let right_width = max_right_width.min(content_area.width.saturating_sub(min_left_width));
104 let [left_column, right_column] = content_area.layout(&Layout::horizontal([
105 Constraint::Fill(1),
106 Constraint::Length(right_width),
107 ]));
108
109 let max_rows = left_column.height as usize;
111 let row_count = 34.min(max_rows);
112 let left_cells =
113 left_column.layout_vec(&Layout::vertical(vec![Constraint::Length(1); row_count]));
114
115 let bar_width = if right_column.width >= 68 { 2 } else { 1 };
117 let max_cols = (right_column.width / bar_width) as usize;
118 let col_count = 34.min(max_cols);
119 let right_cells =
120 right_column.layout_vec(&Layout::horizontal(vec![
121 Constraint::Length(bar_width);
122 col_count
123 ]));
124
125 render_horizontal_steps(frame, left_cells);
126 render_vertical_steps(frame, right_cells);
127}
128
129fn render_horizontal_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
131 for (index, area) in cells.iter().enumerate() {
132 let [label_area, bar_area] = area.layout(&Layout::horizontal([
133 Constraint::Length(2),
134 Constraint::Fill(1),
135 ]));
136 if bar_area.width == 0 {
137 continue;
138 }
139 let metrics = build_metrics(bar_area.width as usize, 6);
140 let (label, thumb_start) = step_entry(&metrics, index);
141 let label = (label % 8).to_string();
142 let offset = metrics.offset_for_thumb_start(thumb_start);
143 let lengths = ScrollLengths {
144 content_len: metrics.content_len(),
145 viewport_len: metrics.viewport_len(),
146 };
147 let scrollbar = ScrollBar::horizontal(lengths)
148 .arrows(ScrollBarArrows::Both)
149 .offset(offset);
150 render_label(frame, label_area, &label);
151 frame.render_widget(&scrollbar, bar_area);
152 }
153}
154
155fn render_vertical_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
157 for (index, area) in cells.iter().enumerate() {
158 let [label_area, bar_area] = area.layout(&Layout::vertical([
159 Constraint::Length(1),
160 Constraint::Fill(1),
161 ]));
162 if bar_area.height == 0 {
163 continue;
164 }
165 let metrics = build_metrics(bar_area.height as usize, 3);
166 let (label, thumb_start) = step_entry(&metrics, index);
167 let label = (label % 8).to_string();
168 let offset = metrics.offset_for_thumb_start(thumb_start);
169 let lengths = ScrollLengths {
170 content_len: metrics.content_len(),
171 viewport_len: metrics.viewport_len(),
172 };
173 let scrollbar = ScrollBar::vertical(lengths)
174 .arrows(ScrollBarArrows::Both)
175 .offset(offset);
176 render_label(frame, label_area, &label);
177 frame.render_widget(&scrollbar, bar_area);
178 }
179}
180
181fn render_label(frame: &mut ratatui::Frame, area: Rect, label: &str) {
183 if area.width == 0 || area.height == 0 {
184 return;
185 }
186 frame.render_widget(Paragraph::new(label), area);
187}
188
189fn build_metrics(track_cells: usize, desired_thumb_cells: usize) -> ScrollMetrics {
191 let track_len = track_cells.saturating_mul(SUBCELL);
192 let viewport_len = track_len.max(1);
193 let desired_thumb_len = desired_thumb_cells.saturating_mul(SUBCELL).max(1);
194 let content_len =
195 ((track_len as u128) * (viewport_len as u128) / (desired_thumb_len as u128)) as usize;
196 let content_len = content_len.max(viewport_len.saturating_add(1));
197 ScrollMetrics::new(
198 ScrollLengths {
199 content_len,
200 viewport_len,
201 },
202 0,
203 track_cells as u16,
204 )
205}
206
207fn step_entry(metrics: &ScrollMetrics, index: usize) -> (usize, usize) {
209 let max_start = metrics.thumb_travel();
210 let local = index % 17;
211 if index < 17 {
212 (local, local.min(max_start))
213 } else {
214 let base = max_start.saturating_sub(16);
215 (local, base.saturating_add(local).min(max_start))
216 }
217}