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