1use ratatui_core::layout::Rect;
11
12use super::{ArrowHit, ArrowLayout, ScrollBar, ScrollBarOrientation, TrackClickBehavior};
13#[cfg(any(feature = "crossterm_0_28", feature = "crossterm_0_29"))]
14use crate::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
15use crate::input::{
16 DragState, PointerButton, PointerEvent, PointerEventKind, ScrollAxis, ScrollBarInteraction,
17 ScrollCommand, ScrollEvent, ScrollWheel,
18};
19use crate::metrics::{HitTest, ScrollMetrics, SUBCELL};
20use crate::ScrollLengths;
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 if event.column == x && event.row == y {
292 return Some(ArrowHit::Start);
293 }
294 }
295 if let Some((x, y)) = layout.end {
296 if event.column == x && event.row == y {
297 return Some(ArrowHit::End);
298 }
299 }
300 None
301 }
302}
303
304fn axis_cell_index(
306 area: Rect,
307 column: u16,
308 row: u16,
309 orientation: ScrollBarOrientation,
310) -> Option<usize> {
311 match orientation {
312 ScrollBarOrientation::Vertical => {
313 if row < area.y || row >= area.y.saturating_add(area.height) {
314 None
315 } else {
316 Some(row.saturating_sub(area.y) as usize)
317 }
318 }
319 ScrollBarOrientation::Horizontal => {
320 if column < area.x || column >= area.x.saturating_add(area.width) {
321 None
322 } else {
323 Some(column.saturating_sub(area.x) as usize)
324 }
325 }
326 }
327}
328
329fn axis_cell_index_clamped(
331 area: Rect,
332 column: u16,
333 row: u16,
334 orientation: ScrollBarOrientation,
335) -> Option<usize> {
336 match orientation {
337 ScrollBarOrientation::Vertical => {
338 if area.height == 0 {
339 return None;
340 }
341 let end = area.y.saturating_add(area.height).saturating_sub(1);
342 let row = row.clamp(area.y, end);
343 Some(row.saturating_sub(area.y) as usize)
344 }
345 ScrollBarOrientation::Horizontal => {
346 if area.width == 0 {
347 return None;
348 }
349 let end = area.x.saturating_add(area.width).saturating_sub(1);
350 let column = column.clamp(area.x, end);
351 Some(column.saturating_sub(area.x) as usize)
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use ratatui_core::layout::Rect;
359
360 use super::*;
361 use crate::{ScrollBarArrows, ScrollLengths};
362
363 #[test]
364 fn pages_when_clicking_track() {
365 let lengths = ScrollLengths {
366 content_len: 100,
367 viewport_len: 20,
368 };
369 let scrollbar = ScrollBar::vertical(lengths)
370 .arrows(ScrollBarArrows::None)
371 .offset(40);
372 let area = Rect::new(0, 0, 1, 10);
373 let event = ScrollEvent::Pointer(PointerEvent {
374 column: 0,
375 row: 0,
376 kind: PointerEventKind::Down,
377 button: PointerButton::Primary,
378 });
379 let expected = 20;
380 let mut interaction = ScrollBarInteraction::default();
381 assert_eq!(
382 scrollbar.handle_event(area, event, &mut interaction),
383 Some(ScrollCommand::SetOffset(expected))
384 );
385 }
386
387 #[test]
388 fn updates_offset_while_dragging() {
389 let lengths = ScrollLengths {
390 content_len: 16,
391 viewport_len: 8,
392 };
393 let scrollbar = ScrollBar::vertical(lengths)
394 .arrows(ScrollBarArrows::None)
395 .offset(0);
396 let area = Rect::new(0, 0, 1, 4);
397 let mut interaction = ScrollBarInteraction::default();
398 let down = ScrollEvent::Pointer(PointerEvent {
399 column: 0,
400 row: 0,
401 kind: PointerEventKind::Down,
402 button: PointerButton::Primary,
403 });
404 assert_eq!(scrollbar.handle_event(area, down, &mut interaction), None);
405
406 let drag = ScrollEvent::Pointer(PointerEvent {
407 column: 0,
408 row: 1,
409 kind: PointerEventKind::Drag,
410 button: PointerButton::Primary,
411 });
412 assert_eq!(
413 scrollbar.handle_event(area, drag, &mut interaction),
414 Some(ScrollCommand::SetOffset(4))
415 );
416 }
417
418 #[test]
419 fn applies_scroll_step_to_wheel() {
420 let lengths = ScrollLengths {
421 content_len: 100,
422 viewport_len: 20,
423 };
424 let scrollbar = ScrollBar::vertical(lengths)
425 .arrows(ScrollBarArrows::None)
426 .offset(40)
427 .scroll_step(3);
428 let area = Rect::new(0, 0, 1, 10);
429 let mut interaction = ScrollBarInteraction::default();
430 let event = ScrollEvent::ScrollWheel(ScrollWheel {
431 axis: ScrollAxis::Vertical,
432 delta: 1,
433 column: 0,
434 row: 0,
435 });
436 assert_eq!(
437 scrollbar.handle_event(area, event, &mut interaction),
438 Some(ScrollCommand::SetOffset(43))
439 );
440 }
441
442 #[test]
443 fn steps_offset_when_clicking_arrows() {
444 let lengths = ScrollLengths {
445 content_len: 100,
446 viewport_len: 20,
447 };
448 let scrollbar = ScrollBar::vertical(lengths)
449 .arrows(ScrollBarArrows::Both)
450 .offset(10)
451 .scroll_step(5);
452 let area = Rect::new(0, 0, 1, 5);
453 let mut interaction = ScrollBarInteraction::default();
454 let up = ScrollEvent::Pointer(PointerEvent {
455 column: 0,
456 row: 0,
457 kind: PointerEventKind::Down,
458 button: PointerButton::Primary,
459 });
460 assert_eq!(
461 scrollbar.handle_event(area, up, &mut interaction),
462 Some(ScrollCommand::SetOffset(5))
463 );
464
465 let down = ScrollEvent::Pointer(PointerEvent {
466 column: 0,
467 row: 4,
468 kind: PointerEventKind::Down,
469 button: PointerButton::Primary,
470 });
471 assert_eq!(
472 scrollbar.handle_event(area, down, &mut interaction),
473 Some(ScrollCommand::SetOffset(15))
474 );
475 }
476
477 #[test]
478 fn ignores_scroll_wheel_on_other_axis() {
479 let lengths = ScrollLengths {
480 content_len: 100,
481 viewport_len: 20,
482 };
483 let scrollbar = ScrollBar::vertical(lengths);
484 let area = Rect::new(0, 0, 1, 5);
485 let mut interaction = ScrollBarInteraction::default();
486 let event = ScrollEvent::ScrollWheel(ScrollWheel {
487 axis: ScrollAxis::Horizontal,
488 delta: 1,
489 column: 0,
490 row: 2,
491 });
492 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
493 }
494
495 #[test]
496 fn applies_negative_scroll_wheel_delta() {
497 let lengths = ScrollLengths {
498 content_len: 100,
499 viewport_len: 20,
500 };
501 let scrollbar = ScrollBar::vertical(lengths).offset(10).scroll_step(2);
502 let area = Rect::new(0, 0, 1, 5);
503 let event = ScrollEvent::ScrollWheel(ScrollWheel {
504 axis: ScrollAxis::Vertical,
505 delta: -1,
506 column: 0,
507 row: 2,
508 });
509 let mut interaction = ScrollBarInteraction::default();
510 assert_eq!(
511 scrollbar.handle_event(area, event, &mut interaction),
512 Some(ScrollCommand::SetOffset(8))
513 );
514 }
515
516 #[test]
517 fn jumps_toward_track_click() {
518 let lengths = ScrollLengths {
519 content_len: 8,
520 viewport_len: 4,
521 };
522 let scrollbar = ScrollBar::vertical(lengths)
523 .arrows(ScrollBarArrows::None)
524 .track_click_behavior(TrackClickBehavior::JumpToClick);
525 let area = Rect::new(0, 0, 1, 4);
526 let event = ScrollEvent::Pointer(PointerEvent {
527 column: 0,
528 row: 2,
529 kind: PointerEventKind::Down,
530 button: PointerButton::Primary,
531 });
532 let expected = 3;
533 let mut interaction = ScrollBarInteraction::default();
534 assert_eq!(
535 scrollbar.handle_event(area, event, &mut interaction),
536 Some(ScrollCommand::SetOffset(expected))
537 );
538 }
539
540 #[test]
541 fn clears_drag_on_pointer_up() {
542 let lengths = ScrollLengths {
543 content_len: 100,
544 viewport_len: 20,
545 };
546 let scrollbar = ScrollBar::vertical(lengths);
547 let area = Rect::new(0, 0, 1, 5);
548 let mut interaction = ScrollBarInteraction::default();
549 interaction.start_drag(3);
550 let event = ScrollEvent::Pointer(PointerEvent {
551 column: 0,
552 row: 1,
553 kind: PointerEventKind::Up,
554 button: PointerButton::Primary,
555 });
556 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
557 assert_eq!(interaction.drag_state, DragState::Idle);
558 }
559
560 #[test]
561 fn ignores_pointer_events_outside_track() {
562 let lengths = ScrollLengths {
563 content_len: 100,
564 viewport_len: 20,
565 };
566 let scrollbar = ScrollBar::vertical(lengths);
567 let area = Rect::new(0, 0, 1, 5);
568 let event = ScrollEvent::Pointer(PointerEvent {
569 column: 0,
570 row: 6,
571 kind: PointerEventKind::Down,
572 button: PointerButton::Primary,
573 });
574 let mut interaction = ScrollBarInteraction::default();
575 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
576 }
577
578 #[test]
579 fn ignores_arrow_clicks_when_max_offset_zero() {
580 let lengths = ScrollLengths {
581 content_len: 10,
582 viewport_len: 10,
583 };
584 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
585 let area = Rect::new(0, 0, 1, 5);
586 let event = ScrollEvent::Pointer(PointerEvent {
587 column: 0,
588 row: 0,
589 kind: PointerEventKind::Down,
590 button: PointerButton::Primary,
591 });
592 let mut interaction = ScrollBarInteraction::default();
593 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
594 }
595
596 #[test]
597 fn stops_drag_on_track_click() {
598 let lengths = ScrollLengths {
599 content_len: 10,
600 viewport_len: 5,
601 };
602 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::None);
603 let area = Rect::new(0, 0, 1, 4);
604 let mut interaction = ScrollBarInteraction::default();
605 interaction.start_drag(2);
606 let event = ScrollEvent::Pointer(PointerEvent {
607 column: 0,
608 row: 3,
609 kind: PointerEventKind::Down,
610 button: PointerButton::Primary,
611 });
612 assert_eq!(
613 scrollbar.handle_event(area, event, &mut interaction),
614 Some(ScrollCommand::SetOffset(5))
615 );
616 assert_eq!(interaction.drag_state, DragState::Idle);
617 }
618
619 #[test]
620 fn returns_none_when_clicking_inside_thumb_in_page_mode() {
621 let lengths = ScrollLengths {
622 content_len: 100,
623 viewport_len: 20,
624 };
625 let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::None);
626 let area = Rect::new(0, 0, 1, 10);
627 let mut interaction = ScrollBarInteraction::default();
628 let event = ScrollEvent::Pointer(PointerEvent {
629 column: 0,
630 row: 0,
631 kind: PointerEventKind::Down,
632 button: PointerButton::Primary,
633 });
634 assert_eq!(scrollbar.handle_event(area, event, &mut interaction), None);
635 }
636}