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?
190fn 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}More examples
312 fn metrics_for_layout(&self, content: Rect) -> (ScrollMetrics, ScrollMetrics) {
313 // Use subcell units so wheel/drag updates line up with the fractional renderer.
314 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 }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?
130fn 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
155/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
156fn 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}More examples
121 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 // Split out the bottom row and right column for the scrollbars.
166 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 // Keep offsets valid when the terminal is resized.
186 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 /// Handles keyboard and mouse events, updating offsets as needed.
221 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 /// Applies a keyboard delta to the scrollbar offsets.
244 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 /// Handles crossterm mouse events using the scrollbar helpers.
255 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 /// Applies a scroll command to the current axis offset.
278 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 /// Builds a horizontal scrollbar from the current metrics.
288 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 /// Builds a vertical scrollbar from the current metrics.
300 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 }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?
130fn 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
155/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
156fn 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}More examples
121 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 // Split out the bottom row and right column for the scrollbars.
166 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 // Keep offsets valid when the terminal is resized.
186 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 /// Handles keyboard and mouse events, updating offsets as needed.
221 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 /// Applies a keyboard delta to the scrollbar offsets.
244 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 /// Handles crossterm mouse events using the scrollbar helpers.
255 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 /// Applies a scroll command to the current axis offset.
278 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 /// Builds a horizontal scrollbar from the current metrics.
288 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 /// Builds a vertical scrollbar from the current metrics.
300 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 }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?
121 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 // Split out the bottom row and right column for the scrollbars.
166 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 // Keep offsets valid when the terminal is resized.
186 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 /// Handles keyboard and mouse events, updating offsets as needed.
221 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 /// Applies a keyboard delta to the scrollbar offsets.
244 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 }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?
130fn 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
155/// Draws vertical scrollbars that sweep every 1/8th thumb position, left to right.
156fn 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}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