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}