pub struct ScrollMetrics { /* private fields */ }Expand description
Precomputed values for proportional scrollbars.
All positions are tracked in subcell units (1/8 of a terminal cell). Use this type to compute thumb length, travel, and hit testing without rendering anything. The inputs are:
content_lenandviewport_lenin logical units (zero treated as 1)track_cellsin terminal cells
Implementations§
Source§impl ScrollMetrics
impl ScrollMetrics
Sourcepub fn from_lengths(
lengths: ScrollLengths,
offset: usize,
track_cells: u16,
) -> Self
pub fn from_lengths( lengths: ScrollLengths, offset: usize, track_cells: u16, ) -> Self
Build metrics using a crate::ScrollLengths helper.
Sourcepub fn new(lengths: ScrollLengths, offset: usize, track_cells: u16) -> Self
pub fn new(lengths: ScrollLengths, offset: usize, track_cells: u16) -> Self
Build metrics for the given content and viewport lengths.
The track_cells parameter is the number of terminal cells available for the track
(height for vertical scrollbars, width for horizontal). The lengths are logical units.
When content_len is smaller than viewport_len, the thumb fills the track to indicate no
scrolling. Zero lengths are treated as 1.
Examples found in repository?
187fn build_metrics(track_cells: usize, desired_thumb_cells: usize) -> ScrollMetrics {
188 let track_len = track_cells.saturating_mul(SUBCELL);
189 let viewport_len = track_len.max(1);
190 let desired_thumb_len = desired_thumb_cells.saturating_mul(SUBCELL).max(1);
191 let content_len =
192 ((track_len as u128) * (viewport_len as u128) / (desired_thumb_len as u128)) as usize;
193 let content_len = content_len.max(viewport_len.saturating_add(1));
194 ScrollMetrics::new(
195 ScrollLengths {
196 content_len,
197 viewport_len,
198 },
199 0,
200 track_cells as u16,
201 )
202}More examples
319 fn metrics_for_layout(&self, content: Rect) -> (ScrollMetrics, ScrollMetrics) {
320 // Use subcell units so wheel/drag updates line up with the fractional renderer.
321 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 }Sourcepub const fn content_len(&self) -> usize
pub const fn content_len(&self) -> usize
Returns the current content length in logical units.
Examples found in repository?
127fn render_horizontal_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
128 for (index, area) in cells.iter().enumerate() {
129 let [label_area, bar_area] = area.layout(&Layout::horizontal([
130 Constraint::Length(2),
131 Constraint::Fill(1),
132 ]));
133 if bar_area.width == 0 {
134 continue;
135 }
136 let metrics = build_metrics(bar_area.width as usize, 6);
137 let (label, thumb_start) = step_entry(&metrics, index);
138 let label = (label % 8).to_string();
139 let offset = metrics.offset_for_thumb_start(thumb_start);
140 let lengths = ScrollLengths {
141 content_len: metrics.content_len(),
142 viewport_len: metrics.viewport_len(),
143 };
144 let scrollbar = ScrollBar::horizontal(lengths)
145 .arrows(ScrollBarArrows::Both)
146 .offset(offset);
147 render_label(frame, label_area, &label);
148 frame.render_widget(&scrollbar, bar_area);
149 }
150}
151
152/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
153fn render_vertical_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
154 for (index, area) in cells.iter().enumerate() {
155 let [label_area, bar_area] = area.layout(&Layout::vertical([
156 Constraint::Length(1),
157 Constraint::Fill(1),
158 ]));
159 if bar_area.height == 0 {
160 continue;
161 }
162 let metrics = build_metrics(bar_area.height as usize, 3);
163 let (label, thumb_start) = step_entry(&metrics, index);
164 let label = (label % 8).to_string();
165 let offset = metrics.offset_for_thumb_start(thumb_start);
166 let lengths = ScrollLengths {
167 content_len: metrics.content_len(),
168 viewport_len: metrics.viewport_len(),
169 };
170 let scrollbar = ScrollBar::vertical(lengths)
171 .arrows(ScrollBarArrows::Both)
172 .offset(offset);
173 render_label(frame, label_area, &label);
174 frame.render_widget(&scrollbar, bar_area);
175 }
176}More examples
123 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 // Split out the bottom row and right column for the scrollbars.
168 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 // Keep offsets valid when the terminal is resized.
188 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 /// Handles keyboard and mouse events, updating offsets as needed.
223 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 /// Handles keyboard input, updating offsets or exiting as needed.
239 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 /// Applies a keyboard delta to the scrollbar offsets.
251 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 /// Handles crossterm mouse events using the scrollbar helpers.
262 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 /// Applies a scroll command to the current axis offset.
285 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 /// Builds a horizontal scrollbar from the current metrics.
295 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 /// Builds a vertical scrollbar from the current metrics.
307 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 }Sourcepub const fn viewport_len(&self) -> usize
pub const fn viewport_len(&self) -> usize
Returns the current viewport length in logical units.
Examples found in repository?
127fn render_horizontal_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
128 for (index, area) in cells.iter().enumerate() {
129 let [label_area, bar_area] = area.layout(&Layout::horizontal([
130 Constraint::Length(2),
131 Constraint::Fill(1),
132 ]));
133 if bar_area.width == 0 {
134 continue;
135 }
136 let metrics = build_metrics(bar_area.width as usize, 6);
137 let (label, thumb_start) = step_entry(&metrics, index);
138 let label = (label % 8).to_string();
139 let offset = metrics.offset_for_thumb_start(thumb_start);
140 let lengths = ScrollLengths {
141 content_len: metrics.content_len(),
142 viewport_len: metrics.viewport_len(),
143 };
144 let scrollbar = ScrollBar::horizontal(lengths)
145 .arrows(ScrollBarArrows::Both)
146 .offset(offset);
147 render_label(frame, label_area, &label);
148 frame.render_widget(&scrollbar, bar_area);
149 }
150}
151
152/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
153fn render_vertical_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
154 for (index, area) in cells.iter().enumerate() {
155 let [label_area, bar_area] = area.layout(&Layout::vertical([
156 Constraint::Length(1),
157 Constraint::Fill(1),
158 ]));
159 if bar_area.height == 0 {
160 continue;
161 }
162 let metrics = build_metrics(bar_area.height as usize, 3);
163 let (label, thumb_start) = step_entry(&metrics, index);
164 let label = (label % 8).to_string();
165 let offset = metrics.offset_for_thumb_start(thumb_start);
166 let lengths = ScrollLengths {
167 content_len: metrics.content_len(),
168 viewport_len: metrics.viewport_len(),
169 };
170 let scrollbar = ScrollBar::vertical(lengths)
171 .arrows(ScrollBarArrows::Both)
172 .offset(offset);
173 render_label(frame, label_area, &label);
174 frame.render_widget(&scrollbar, bar_area);
175 }
176}More examples
123 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 // Split out the bottom row and right column for the scrollbars.
168 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 // Keep offsets valid when the terminal is resized.
188 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 /// Handles keyboard and mouse events, updating offsets as needed.
223 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 /// Handles keyboard input, updating offsets or exiting as needed.
239 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 /// Applies a keyboard delta to the scrollbar offsets.
251 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 /// Handles crossterm mouse events using the scrollbar helpers.
262 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 /// Applies a scroll command to the current axis offset.
285 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 /// Builds a horizontal scrollbar from the current metrics.
295 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 /// Builds a vertical scrollbar from the current metrics.
307 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 }Sourcepub const fn track_cells(&self) -> usize
pub const fn track_cells(&self) -> usize
Returns the track length in terminal cells.
Sourcepub const fn thumb_start(&self) -> usize
pub const fn thumb_start(&self) -> usize
Returns the thumb start position in subcells.
Sourcepub const fn max_offset(&self) -> usize
pub const fn max_offset(&self) -> usize
Returns the maximum scrollable offset in subcells.
Examples found in repository?
123 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 // Split out the bottom row and right column for the scrollbars.
168 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 // Keep offsets valid when the terminal is resized.
188 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 /// Handles keyboard and mouse events, updating offsets as needed.
223 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 /// Handles keyboard input, updating offsets or exiting as needed.
239 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 /// Applies a keyboard delta to the scrollbar offsets.
251 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 }Sourcepub const fn thumb_travel(&self) -> usize
pub const fn thumb_travel(&self) -> usize
Returns the maximum thumb travel in subcells.
Sourcepub const fn thumb_range(&self) -> Range<usize>
pub const fn thumb_range(&self) -> Range<usize>
Returns the thumb range in subcell coordinates.
Sourcepub const fn hit_test(&self, position: usize) -> HitTest
pub const fn hit_test(&self, position: usize) -> HitTest
Returns whether a subcell position hits the thumb or the track.
Sourcepub fn thumb_start_for_offset(&self, offset: usize) -> usize
pub fn thumb_start_for_offset(&self, offset: usize) -> usize
Converts an offset (in subcells) to a thumb start position (in subcells).
Larger offsets move the thumb toward the end of the track, clamped to the maximum travel.
Sourcepub fn offset_for_thumb_start(&self, thumb_start: usize) -> usize
pub fn offset_for_thumb_start(&self, thumb_start: usize) -> usize
Converts a thumb start position (in subcells) to an offset (in subcells).
Thumb positions beyond the end of travel are clamped to the maximum offset.
Examples found in repository?
127fn render_horizontal_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
128 for (index, area) in cells.iter().enumerate() {
129 let [label_area, bar_area] = area.layout(&Layout::horizontal([
130 Constraint::Length(2),
131 Constraint::Fill(1),
132 ]));
133 if bar_area.width == 0 {
134 continue;
135 }
136 let metrics = build_metrics(bar_area.width as usize, 6);
137 let (label, thumb_start) = step_entry(&metrics, index);
138 let label = (label % 8).to_string();
139 let offset = metrics.offset_for_thumb_start(thumb_start);
140 let lengths = ScrollLengths {
141 content_len: metrics.content_len(),
142 viewport_len: metrics.viewport_len(),
143 };
144 let scrollbar = ScrollBar::horizontal(lengths)
145 .arrows(ScrollBarArrows::Both)
146 .offset(offset);
147 render_label(frame, label_area, &label);
148 frame.render_widget(&scrollbar, bar_area);
149 }
150}
151
152/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
153fn render_vertical_steps(frame: &mut ratatui::Frame, cells: Vec<Rect>) {
154 for (index, area) in cells.iter().enumerate() {
155 let [label_area, bar_area] = area.layout(&Layout::vertical([
156 Constraint::Length(1),
157 Constraint::Fill(1),
158 ]));
159 if bar_area.height == 0 {
160 continue;
161 }
162 let metrics = build_metrics(bar_area.height as usize, 3);
163 let (label, thumb_start) = step_entry(&metrics, index);
164 let label = (label % 8).to_string();
165 let offset = metrics.offset_for_thumb_start(thumb_start);
166 let lengths = ScrollLengths {
167 content_len: metrics.content_len(),
168 viewport_len: metrics.viewport_len(),
169 };
170 let scrollbar = ScrollBar::vertical(lengths)
171 .arrows(ScrollBarArrows::Both)
172 .offset(offset);
173 render_label(frame, label_area, &label);
174 frame.render_widget(&scrollbar, bar_area);
175 }
176}Trait Implementations§
Source§impl Clone for ScrollMetrics
impl Clone for ScrollMetrics
Source§fn clone(&self) -> ScrollMetrics
fn clone(&self) -> ScrollMetrics
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for ScrollMetrics
Source§impl Debug for ScrollMetrics
impl Debug for ScrollMetrics
impl Eq for ScrollMetrics
Source§impl PartialEq for ScrollMetrics
impl PartialEq for ScrollMetrics
Source§fn eq(&self, other: &ScrollMetrics) -> bool
fn eq(&self, other: &ScrollMetrics) -> bool
self and other values to be equal, and is used by ==.impl StructuralPartialEq for ScrollMetrics
Auto Trait Implementations§
impl Freeze for ScrollMetrics
impl RefUnwindSafe for ScrollMetrics
impl Send for ScrollMetrics
impl Sync for ScrollMetrics
impl Unpin for ScrollMetrics
impl UnsafeUnpin for ScrollMetrics
impl UnwindSafe for ScrollMetrics
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more