tui_scrollbar/input.rs
1//! Input types and interaction state for scrollbars.
2//!
3//! ## Design notes
4//!
5//! These types are intentionally backend-agnostic and small. The widget does not own scroll state;
6//! it returns a [`ScrollCommand`] so the application can decide how to apply offsets. This keeps
7//! the API compatible with any event loop and makes it easy to test.
8
9/// Action requested by a pointer or wheel event.
10///
11/// Apply these to your stored offsets.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ScrollCommand {
14 /// Update the content offset in the same logical units you supplied.
15 SetOffset(usize),
16}
17
18/// Axis for scroll wheel events.
19///
20/// The scrollbar ignores wheel events that do not match its orientation.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ScrollAxis {
23 /// Wheel scroll in the vertical direction.
24 Vertical,
25 /// Wheel scroll in the horizontal direction.
26 Horizontal,
27}
28
29/// Pointer button used for interaction.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum PointerButton {
32 /// Primary pointer button (usually left mouse button).
33 Primary,
34}
35
36/// Kind of pointer interaction.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum PointerEventKind {
39 /// Pointer pressed down.
40 Down,
41 /// Pointer moved while pressed down.
42 Drag,
43 /// Pointer released.
44 Up,
45}
46
47/// Pointer input in terminal cell coordinates.
48///
49/// Use this to describe a pointer action relative to the scrollbar area.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct PointerEvent {
52 /// Column of the event, in terminal cells.
53 pub column: u16,
54 /// Row of the event, in terminal cells.
55 pub row: u16,
56 /// Event kind.
57 pub kind: PointerEventKind,
58 /// Pointer button.
59 pub button: PointerButton,
60}
61
62/// Scroll wheel input with an axis and a signed delta.
63///
64/// Positive deltas scroll down/right, negative deltas scroll up/left. The `column` and `row` are
65/// used to ignore wheel events outside the scrollbar area.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct ScrollWheel {
68 /// Axis the wheel is scrolling.
69 pub axis: ScrollAxis,
70 /// Signed delta. Positive values scroll down/right.
71 pub delta: isize,
72 /// Column where the wheel event occurred.
73 pub column: u16,
74 /// Row where the wheel event occurred.
75 pub row: u16,
76}
77
78/// Backend-agnostic input event for a scrollbar.
79///
80/// Use this in input handling when you want to stay backend-agnostic.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ScrollEvent {
83 /// Pointer down/drag/up events.
84 Pointer(PointerEvent),
85 /// Scroll wheel input.
86 ScrollWheel(ScrollWheel),
87}
88
89/// Drag state that should persist between frames.
90///
91/// Store this in your app state so drags remain active across draw calls.
92#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
93pub struct ScrollBarInteraction {
94 pub(crate) drag_state: DragState,
95}
96
97/// Internal drag capture state stored by [`ScrollBarInteraction`].
98#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
99pub(crate) enum DragState {
100 /// No active drag.
101 #[default]
102 Idle,
103 /// A drag is active; `grab_offset` is in subcells.
104 Dragging { grab_offset: usize },
105}
106
107impl ScrollBarInteraction {
108 /// Creates a fresh interaction state with no active drag.
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 /// Starts a drag by recording the grab offset in subcells.
114 ///
115 /// This keeps the pointer anchored to the same point within the thumb while dragging.
116 pub(crate) fn start_drag(&mut self, grab_offset: usize) {
117 self.drag_state = DragState::Dragging { grab_offset };
118 }
119
120 /// Stops any active drag and returns to the idle state.
121 pub(crate) fn stop_drag(&mut self) {
122 self.drag_state = DragState::Idle;
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn starts_idle() {
132 let interaction = ScrollBarInteraction::new();
133 assert_eq!(interaction.drag_state, DragState::Idle);
134 }
135
136 #[test]
137 fn records_grab_offset_on_start_drag() {
138 let mut interaction = ScrollBarInteraction::new();
139 interaction.start_drag(6);
140 assert_eq!(
141 interaction.drag_state,
142 DragState::Dragging { grab_offset: 6 }
143 );
144 }
145
146 #[test]
147 fn resets_state_on_stop_drag() {
148 let mut interaction = ScrollBarInteraction::new();
149 interaction.start_drag(3);
150 interaction.stop_drag();
151 assert_eq!(interaction.drag_state, DragState::Idle);
152 }
153
154 // ScrollViewState is in tui-scrollview; only exercise scrollbar input here.
155}