1use ratatui_core::layout::Rect;
11
12use super::{ArrowHit, ArrowLayout, ScrollBar, ScrollBarOrientation, TrackClickBehavior};
13use crate::ScrollLengths;
14#[cfg(any(feature = "crossterm_0_28", feature = "crossterm_0_29"))]
15use crate::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
16use crate::input::{
17 DragState, PointerButton, PointerEvent, PointerEventKind, ScrollAxis, ScrollBarInteraction,
18 ScrollCommand, ScrollEvent, ScrollWheel,
19};
20use crate::metrics::{HitTest, SUBCELL, ScrollMetrics};
21
22impl ScrollBar {
23 pub fn handle_event(
55 &self,
56 area: Rect,
57 event: ScrollEvent,
58 interaction: &mut ScrollBarInteraction,
59 ) -> Option<ScrollCommand> {
60 if area.width == 0 || area.height == 0 {
61 return None;
62 }
63
64 let layout = self.arrow_layout(area);
65 let lengths = ScrollLengths {
66 content_len: self.content_len,
67 viewport_len: self.viewport_len,
68 };
69 let track_cells = match self.orientation {
70 ScrollBarOrientation::Vertical => layout.track_area.height,
71 ScrollBarOrientation::Horizontal => layout.track_area.width,
72 };
73 let metrics = ScrollMetrics::new(lengths, self.offset, track_cells);
74
75 match event {
76 ScrollEvent::Pointer(event) => {
77 if let Some(command) =
78 self.handle_arrow_pointer(&layout, metrics, event, interaction)
79 {
80 return Some(command);
81 }
82 self.handle_pointer_event(layout.track_area, metrics, event, interaction)
83 }
84 ScrollEvent::ScrollWheel(event) => self.handle_scroll_wheel(area, metrics, event),
85 }
86 }
87
88 #[cfg(any(feature = "crossterm_0_28", feature = "crossterm_0_29"))]
89 pub fn handle_mouse_event(
95 &self,
96 area: Rect,
97 event: MouseEvent,
98 interaction: &mut ScrollBarInteraction,
99 ) -> Option<ScrollCommand> {
100 let event = match event.kind {
101 MouseEventKind::Down(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
102 column: event.column,
103 row: event.row,
104 kind: PointerEventKind::Down,
105 button: PointerButton::Primary,
106 })),
107 MouseEventKind::Up(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
108 column: event.column,
109 row: event.row,
110 kind: PointerEventKind::Up,
111 button: PointerButton::Primary,
112 })),
113 MouseEventKind::Drag(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
114 column: event.column,
115 row: event.row,
116 kind: PointerEventKind::Drag,
117 button: PointerButton::Primary,
118 })),
119 MouseEventKind::ScrollUp => Some(ScrollEvent::ScrollWheel(ScrollWheel {
120 axis: ScrollAxis::Vertical,
121 delta: -1,
122 column: event.column,
123 row: event.row,
124 })),
125 MouseEventKind::ScrollDown => Some(ScrollEvent::ScrollWheel(ScrollWheel {
126 axis: ScrollAxis::Vertical,
127 delta: 1,
128 column: event.column,
129 row: event.row,
130 })),
131 MouseEventKind::ScrollLeft => Some(ScrollEvent::ScrollWheel(ScrollWheel {
132 axis: ScrollAxis::Horizontal,
133 delta: -1,
134 column: event.column,
135 row: event.row,
136 })),
137 MouseEventKind::ScrollRight => Some(ScrollEvent::ScrollWheel(ScrollWheel {
138 axis: ScrollAxis::Horizontal,
139 delta: 1,
140 column: event.column,
141 row: event.row,
142 })),
143 _ => None,
144 };
145
146 event.and_then(|event| self.handle_event(area, event, interaction))
147 }
148
149 fn handle_pointer_event(
151 &self,
152 area: Rect,
153 metrics: ScrollMetrics,
154 event: PointerEvent,
155 interaction: &mut ScrollBarInteraction,
156 ) -> Option<ScrollCommand> {
157 if event.button != PointerButton::Primary {
158 return None;
159 }
160
161 match event.kind {
162 PointerEventKind::Down => {
163 let cell_index = axis_cell_index(area, event.column, event.row, self.orientation)?;
164 let position = cell_index
165 .saturating_mul(SUBCELL)
166 .saturating_add(SUBCELL / 2);
167 if metrics.thumb_len() == 0 {
168 return None;
169 }
170 match metrics.hit_test(position) {
171 HitTest::Thumb => {
172 let grab_offset = position.saturating_sub(metrics.thumb_start());
173 interaction.start_drag(grab_offset);
174 None
175 }
176 HitTest::Track => {
177 interaction.stop_drag();
178 self.handle_track_click(metrics, position)
179 }
180 }
181 }
182 PointerEventKind::Drag => match interaction.drag_state {
183 DragState::Idle => None,
184 DragState::Dragging { grab_offset } => {
185 let cell_index =
186 axis_cell_index_clamped(area, event.column, event.row, self.orientation)?;
187 let position = cell_index
188 .saturating_mul(SUBCELL)
189 .saturating_add(SUBCELL / 2);
190 let thumb_start = position.saturating_sub(grab_offset);
191 Some(ScrollCommand::SetOffset(
192 metrics.offset_for_thumb_start(thumb_start),
193 ))
194 }
195 },
196 PointerEventKind::Up => {
197 interaction.stop_drag();
198 None
199 }
200 }
201 }
202
203 fn handle_track_click(&self, metrics: ScrollMetrics, position: usize) -> Option<ScrollCommand> {
205 if metrics.max_offset() == 0 {
206 return None;
207 }
208
209 match self.track_click_behavior {
210 TrackClickBehavior::Page => {
211 let thumb_end = metrics.thumb_start().saturating_add(metrics.thumb_len());
212 if position < metrics.thumb_start() {
213 Some(ScrollCommand::SetOffset(
214 metrics.offset().saturating_sub(metrics.viewport_len()),
215 ))
216 } else if position >= thumb_end {
217 Some(ScrollCommand::SetOffset(
218 (metrics.offset() + metrics.viewport_len()).min(metrics.max_offset()),
219 ))
220 } else {
221 None
222 }
223 }
224 TrackClickBehavior::JumpToClick => {
225 let half_thumb = metrics.thumb_len() / 2;
226 let thumb_start = position.saturating_sub(half_thumb);
227 Some(ScrollCommand::SetOffset(
228 metrics.offset_for_thumb_start(thumb_start),
229 ))
230 }
231 }
232 }
233
234 fn handle_scroll_wheel(
236 &self,
237 _area: Rect,
238 metrics: ScrollMetrics,
239 event: ScrollWheel,
240 ) -> Option<ScrollCommand> {
241 let matches_axis = matches!(
242 (self.orientation, event.axis),
243 (ScrollBarOrientation::Vertical, ScrollAxis::Vertical)
244 | (ScrollBarOrientation::Horizontal, ScrollAxis::Horizontal)
245 );
246
247 if !matches_axis {
248 return None;
249 }
250
251 let step = self.scroll_step.max(1) as isize;
252 let delta = event.delta.saturating_mul(step);
253 let max_offset = metrics.max_offset() as isize;
254 let next = (metrics.offset() as isize).saturating_add(delta);
255 let next = next.clamp(0, max_offset);
256 Some(ScrollCommand::SetOffset(next as usize))
257 }
258
259 fn handle_arrow_pointer(
261 &self,
262 layout: &ArrowLayout,
263 metrics: ScrollMetrics,
264 event: PointerEvent,
265 interaction: &mut ScrollBarInteraction,
266 ) -> Option<ScrollCommand> {
267 if event.button != PointerButton::Primary || event.kind != PointerEventKind::Down {
268 return None;
269 }
270
271 let hit = self.arrow_hit(layout, event)?;
272 if metrics.max_offset() == 0 {
273 return None;
274 }
275
276 interaction.stop_drag();
277 let step = self.scroll_step.max(1) as isize;
278 let delta = match hit {
279 ArrowHit::Start => -step,
280 ArrowHit::End => step,
281 };
282 let max_offset = metrics.max_offset() as isize;
283 let next = (metrics.offset() as isize).saturating_add(delta);
284 let next = next.clamp(0, max_offset);
285 Some(ScrollCommand::SetOffset(next as usize))
286 }
287
288 fn arrow_hit(&self, layout: &ArrowLayout, event: PointerEvent) -> Option<ArrowHit> {
290 if let Some((x, y)) = layout.start
291 && event.column == x
292 && event.row == y
293 {
294 return Some(ArrowHit::Start);
295 }
296 if let Some((x, y)) = layout.end
297 && event.column == x
298 && event.row == y
299 {
300 return Some(ArrowHit::End);
301 }
302 None
303 }
304}
305
306fn axis_cell_index(
308 area: Rect,
309 column: u16,
310 row: u16,
311 orientation: ScrollBarOrientation,
312) -> Option<usize> {
313 match orientation {
314 ScrollBarOrientation::Vertical => {
315 if row < area.y || row >= area.y.saturating_add(area.height) {
316 None
317 } else {
318 Some(row.saturating_sub(area.y) as usize)
319 }
320 }
321 ScrollBarOrientation::Horizontal => {
322 if column < area.x || column >= area.x.saturating_add(area.width) {
323 None
324 } else {
325 Some(column.saturating_sub(area.x) as usize)
326 }
327 }
328 }
329}
330
331fn axis_cell_index_clamped(
333 area: Rect,
334 column: u16,
335 row: u16,
336 orientation: ScrollBarOrientation,
337) -> Option<usize> {
338 match orientation {
339 ScrollBarOrientation::Vertical => {
340 if area.height == 0 {
341 return None;
342 }
343 let end = area.y.saturating_add(area.height).saturating_sub(1);
344 let row = row.clamp(area.y, end);
345 Some(row.saturating_sub(area.y) as usize)
346 }
347 ScrollBarOrientation::Horizontal => {
348 if area.width == 0 {
349 return None;
350 }
351 let end = area.x.saturating_add(area.width).saturating_sub(1);
352 let column = column.clamp(area.x, end);
353 Some(column.saturating_sub(area.x) as usize)
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use ratatui_core::layout::Rect;
361
362 use super::*;
363 use crate::{ScrollBarArrows, ScrollLengths};
364
365 #[test]
366 fn pages_when_clicking_track() {
367 let lengths = ScrollLengths {
368 content_len: 100,
369 viewport_len: 20,
370 };
371 let scrollbar = ScrollBar::vertical(lengths)
372 .arrows(ScrollBarArrows::None)
373 .offset(40);
374 let area = Rect::new(0, 0, 1, 10);
375 let event = ScrollEvent::Pointer(PointerEvent {
376 column: 0,
377 row: 0,
378 kind: PointerEventKind::Down,
379 button: PointerButton::Primary,
380 });
381 let expected = 20;
382 let mut interaction = ScrollBarInteraction::default();
383 assert_eq!(
384 scrollbar.handle_event(area, event, &mut interaction),
385 Some(ScrollCommand::SetOffset(expected))
386 );
387 }
388
389 #[test]
390 fn updates_offset_while_dragging() {
391 let lengths = ScrollLengths {
392 content_len: 16,
393 viewport_len: 8,
394 };
395 let scrollbar = ScrollBar::vertical(lengths)
396 .arrows(ScrollBarArrows::None)
397 .offset(0);
398 let area = Rect::new(0, 0, 1, 4);
399 let mut interaction = ScrollBarInteraction::default();
400 let down = ScrollEvent::Pointer(PointerEvent {
401 column: 0,
402 row: 0,
403 kind: PointerEventKind::Down,
404 button: PointerButton::Primary,
405 });
406 assert_eq!(scrollbar.handle_event(area, down, &mut interaction), None);
407
408 let drag = ScrollEvent::Pointer(PointerEvent {
409 column: 0,
410 row: 1,
411 kind: PointerEventKind::Drag,
412 button: PointerButton::Primary,
413 });
414 assert_eq!(
415 scrollbar.handle_event(area, drag, &mut interaction),
416 Some(ScrollCommand::SetOffset(4))
417 );
418 }
419
420 #[test]
421 fn applies_scroll_step_to_wheel() {
422 let lengths = ScrollLengths {
423 content_len: 100,
424 viewport_len: 20,
425 };
426 let scrollbar = ScrollBar::vertical(lengths)
427 .arrows(ScrollBarArrows::None)
428 .offset(40)
429 .scroll_step(3);
430 let area = Rect::new(0, 0, 1, 10);
431 let mut interaction = ScrollBarInteraction::default();
432 let event = ScrollEvent::ScrollWheel(ScrollWheel {
433 axis: ScrollAxis::Vertical,
434 delta: 1,
435 column: 0,
436 row: 0,
437 });
438 assert_eq!(
439 scrollbar.handle_event(area, event, &mut interaction),
440 Some(ScrollCommand::SetOffset(43))
441 );
442 }
443
444 #[test]
445 fn steps_offset_when_clicking_arrows() {
446 let lengths = ScrollLengths {
447 content_len: 100,
448 viewport_len: 20,
449 };
450 let scrollbar = ScrollBar::vertical(lengths)
451 .arrows(ScrollBarArrows::Both)
452 .offset(10)
453 .scroll_step(5);
454 let area = Rect::new(0, 0, 1, 5);
455 let mut interaction = ScrollBarInteraction::default();
456 let up = ScrollEvent::Pointer(PointerEvent {
457 column: 0,
458 row: 0,
459 kind: PointerEventKind::Down,
460 button: PointerButton::Primary,
461 });
462 assert_eq!(
463 scrollbar.handle_event(area, up, &mut interaction),
464 Some(ScrollCommand::SetOffset(5))
465 );
466
467 let down = ScrollEvent::Pointer(PointerEvent {
468 column: 0,
469 row: 4,
470 kind: PointerEventKind::Down,
471 button: PointerButton::Primary,
472 });
473 assert_eq!(
474 scrollbar.handle_event(area, down, &mut interaction),
475 Some(ScrollCommand::SetOffset(15))
476 );
477 }
478
479 #[test]
480 fn ignores_scroll_wheel_on_other_axis() {
481 let lengths = ScrollLengths {
482 content_len: 100,
483 viewport_len: 20,
484 };
485 let scrollbar = ScrollBar::vertical(lengths);
486 let area = Rect::new(0, 0, 1, 5);
487 let mut interaction = ScrollBarInteraction::default();
488 let event = ScrollEvent::ScrollWheel(ScrollWheel {
489 axis: ScrollAxis::Horizontal,
490 delta: 1,
491 column: 0,
492 row: 2,
493 });
494 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
495 }
496
497 #[test]
498 fn applies_negative_scroll_wheel_delta() {
499 let lengths = ScrollLengths {
500 content_len: 100,
501 viewport_len: 20,
502 };
503 let scrollbar = ScrollBar::vertical(lengths).offset(10).scroll_step(2);
504 let area = Rect::new(0, 0, 1, 5);
505 let event = ScrollEvent::ScrollWheel(ScrollWheel {
506 axis: ScrollAxis::Vertical,
507 delta: -1,
508 column: 0,
509 row: 2,
510 });
511 let mut interaction = ScrollBarInteraction::default();
512 assert_eq!(
513 scrollbar.handle_event(area, event, &mut interaction),
514 Some(ScrollCommand::SetOffset(8))
515 );
516 }
517
518 #[test]
519 fn jumps_toward_track_click() {
520 let lengths = ScrollLengths {
521 content_len: 8,
522 viewport_len: 4,
523 };
524 let scrollbar = ScrollBar::vertical(lengths)
525 .arrows(ScrollBarArrows::None)
526 .track_click_behavior(TrackClickBehavior::JumpToClick);
527 let area = Rect::new(0, 0, 1, 4);
528 let event = ScrollEvent::Pointer(PointerEvent {
529 column: 0,
530 row: 2,
531 kind: PointerEventKind::Down,
532 button: PointerButton::Primary,
533 });
534 let expected = 3;
535 let mut interaction = ScrollBarInteraction::default();
536 assert_eq!(
537 scrollbar.handle_event(area, event, &mut interaction),
538 Some(ScrollCommand::SetOffset(expected))
539 );
540 }
541
542 #[test]
543 fn clears_drag_on_pointer_up() {
544 let lengths = ScrollLengths {
545 content_len: 100,
546 viewport_len: 20,
547 };
548 let scrollbar = ScrollBar::vertical(lengths);
549 let area = Rect::new(0, 0, 1, 5);
550 let mut interaction = ScrollBarInteraction::default();
551 interaction.start_drag(3);
552 let event = ScrollEvent::Pointer(PointerEvent {
553 column: 0,
554 row: 1,
555 kind: PointerEventKind::Up,
556 button: PointerButton::Primary,
557 });
558 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
559 assert_eq!(interaction.drag_state, DragState::Idle);
560 }
561
562 #[test]
563 fn ignores_pointer_events_outside_track() {
564 let lengths = ScrollLengths {
565 content_len: 100,
566 viewport_len: 20,
567 };
568 let scrollbar = ScrollBar::vertical(lengths);
569 let area = Rect::new(0, 0, 1, 5);
570 let event = ScrollEvent::Pointer(PointerEvent {
571 column: 0,
572 row: 6,
573 kind: PointerEventKind::Down,
574 button: PointerButton::Primary,
575 });
576 let mut interaction = ScrollBarInteraction::default();
577 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
578 }
579
580 #[test]
581 fn ignores_arrow_clicks_when_max_offset_zero() {
582 let lengths = ScrollLengths {
583 content_len: 10,
584 viewport_len: 10,
585 };
586 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
587 let area = Rect::new(0, 0, 1, 5);
588 let event = ScrollEvent::Pointer(PointerEvent {
589 column: 0,
590 row: 0,
591 kind: PointerEventKind::Down,
592 button: PointerButton::Primary,
593 });
594 let mut interaction = ScrollBarInteraction::default();
595 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
596 }
597
598 #[test]
599 fn stops_drag_on_track_click() {
600 let lengths = ScrollLengths {
601 content_len: 10,
602 viewport_len: 5,
603 };
604 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::None);
605 let area = Rect::new(0, 0, 1, 4);
606 let mut interaction = ScrollBarInteraction::default();
607 interaction.start_drag(2);
608 let event = ScrollEvent::Pointer(PointerEvent {
609 column: 0,
610 row: 3,
611 kind: PointerEventKind::Down,
612 button: PointerButton::Primary,
613 });
614 assert_eq!(
615 scrollbar.handle_event(area, event, &mut interaction),
616 Some(ScrollCommand::SetOffset(5))
617 );
618 assert_eq!(interaction.drag_state, DragState::Idle);
619 }
620
621 #[test]
622 fn returns_none_when_clicking_inside_thumb_in_page_mode() {
623 let lengths = ScrollLengths {
624 content_len: 100,
625 viewport_len: 20,
626 };
627 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::None);
628 let area = Rect::new(0, 0, 1, 10);
629 let mut interaction = ScrollBarInteraction::default();
630 let event = ScrollEvent::Pointer(PointerEvent {
631 column: 0,
632 row: 0,
633 kind: PointerEventKind::Down,
634 button: PointerButton::Primary,
635 });
636 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
637 }
638}