Skip to main content

tui_lipan/widgets/scroll/
behavior.rs

1use std::time::Duration;
2
3use crate::animation::{Easing, TransitionConfig};
4use crate::core::element::Key;
5use crate::style::Rect;
6
7use super::action::ScrollMetrics;
8
9/// Distance-based timing for smooth programmatic scroll targets.
10///
11/// The resolved target distance is measured in terminal rows. The transition
12/// duration is computed as:
13///
14/// `min_duration + duration_per_row * distance_rows`, clamped to
15/// `min_duration..=max_duration`.
16#[derive(Clone, Copy, Debug)]
17pub struct ScrollDistanceConfig {
18    /// Minimum duration for non-zero target jumps.
19    pub min_duration: Duration,
20    /// Maximum duration cap for long target jumps.
21    pub max_duration: Duration,
22    /// Additional duration added per row of target distance.
23    pub duration_per_row: Duration,
24    /// Easing curve used by the generated transition.
25    pub easing: Easing,
26}
27
28impl ScrollDistanceConfig {
29    /// Create distance-based smooth-scroll timing.
30    pub const fn new(
31        min_duration: Duration,
32        max_duration: Duration,
33        duration_per_row: Duration,
34        easing: Easing,
35    ) -> Self {
36        Self {
37            min_duration,
38            max_duration,
39            duration_per_row,
40            easing,
41        }
42    }
43
44    /// Compute the transition duration for a row distance.
45    pub fn duration_for_distance(self, distance_rows: usize) -> Duration {
46        if distance_rows == 0 {
47            return Duration::ZERO;
48        }
49
50        let min_duration = self.min_duration.min(self.max_duration);
51        let max_duration = self.min_duration.max(self.max_duration);
52        let rows = distance_rows.min(u32::MAX as usize) as u32;
53        min_duration
54            .saturating_add(self.duration_per_row.saturating_mul(rows))
55            .min(max_duration)
56    }
57
58    /// Build the concrete transition config for a row distance.
59    pub fn transition_config_for_distance(self, distance_rows: usize) -> TransitionConfig {
60        TransitionConfig {
61            duration: self.duration_for_distance(distance_rows),
62            easing: self.easing,
63        }
64    }
65
66    /// Minimum transition config used when no distance is available.
67    pub const fn min_transition_config(self) -> TransitionConfig {
68        TransitionConfig {
69            duration: self.min_duration,
70            easing: self.easing,
71        }
72    }
73}
74
75impl Default for ScrollDistanceConfig {
76    fn default() -> Self {
77        Self {
78            min_duration: Duration::from_millis(120),
79            max_duration: Duration::from_millis(700),
80            duration_per_row: Duration::from_millis(8),
81            easing: Easing::EaseOutQuad,
82        }
83    }
84}
85
86/// How programmatic scroll targets are applied.
87///
88/// `Instant` preserves the historical behavior: target APIs snap directly to
89/// their resolved row. Smooth variants animate framework-owned target navigation
90/// while leaving controlled offsets and user input immediate.
91#[derive(Clone, Copy, Debug, Default)]
92pub enum ScrollBehavior {
93    /// Snap directly to the target row.
94    #[default]
95    Instant,
96    /// Animate to the target row with the provided transition timing.
97    Smooth(TransitionConfig),
98    /// Animate to the target row with duration derived from row distance.
99    SmoothDistance(ScrollDistanceConfig),
100}
101
102/// Physics parameters for opt-in smooth wheel scrolling.
103#[derive(Clone, Copy, Debug, PartialEq)]
104pub struct ScrollWheelConfig {
105    /// Velocity impulse added per wheel line, in content rows per second.
106    pub acceleration: f32,
107    /// Exponential velocity decay per second. Higher values stop sooner.
108    pub deceleration: f32,
109    /// Absolute velocity clamp, in content rows per second.
110    pub max_velocity: f32,
111    /// Velocity below this threshold stops the inertial animation.
112    pub stop_velocity: f32,
113}
114
115impl ScrollWheelConfig {
116    /// Create smooth wheel-scroll physics parameters.
117    pub const fn new(
118        acceleration: f32,
119        deceleration: f32,
120        max_velocity: f32,
121        stop_velocity: f32,
122    ) -> Self {
123        Self {
124            acceleration,
125            deceleration,
126            max_velocity,
127            stop_velocity,
128        }
129    }
130}
131
132impl Default for ScrollWheelConfig {
133    fn default() -> Self {
134        Self {
135            acceleration: 40.0,
136            deceleration: 12.0,
137            max_velocity: 320.0,
138            stop_velocity: 0.05,
139        }
140    }
141}
142
143/// How user wheel input is applied.
144#[derive(Clone, Copy, Debug, Default, PartialEq)]
145pub enum ScrollWheelBehavior {
146    /// Apply wheel deltas immediately as discrete line jumps.
147    #[default]
148    Immediate,
149    /// Add wheel deltas to an inertial velocity and decay smoothly over time.
150    Smooth(ScrollWheelConfig),
151}
152
153impl ScrollWheelBehavior {
154    /// Apply wheel deltas immediately as discrete line jumps.
155    pub const fn immediate() -> Self {
156        Self::Immediate
157    }
158
159    /// Add wheel deltas to an inertial velocity using `config`.
160    pub const fn smooth(config: ScrollWheelConfig) -> Self {
161        Self::Smooth(config)
162    }
163
164    /// Add wheel deltas to an inertial velocity using default physics.
165    pub fn smooth_default() -> Self {
166        Self::Smooth(ScrollWheelConfig::default())
167    }
168}
169
170/// Semantic target for framework-owned `ScrollView` navigation.
171#[derive(Clone, Debug, PartialEq, Eq, Hash)]
172pub enum ScrollTarget {
173    /// Scroll to the start of the content.
174    Top,
175    /// Scroll to the end of the content.
176    Bottom,
177    /// Scroll so the first child subtree containing this key is brought into view.
178    Key(Key),
179    /// Scroll so the first child subtree containing this key is brought into view,
180    /// then add `offset` rows from that child's top.
181    KeyOffset {
182        /// Key to search for in the first matching child subtree.
183        key: Key,
184        /// Row offset added to the matched child's top position.
185        offset: usize,
186    },
187}
188
189impl ScrollTarget {
190    /// Scroll to the start of the content.
191    pub const fn top() -> Self {
192        Self::Top
193    }
194
195    /// Scroll to the end of the content.
196    pub const fn bottom() -> Self {
197        Self::Bottom
198    }
199
200    /// Scroll so the first child subtree containing `key` is brought into view.
201    pub fn key(key: impl Into<Key>) -> Self {
202        Self::Key(key.into())
203    }
204
205    /// Scroll to `offset` rows below the first child subtree containing `key`.
206    pub fn key_offset(key: impl Into<Key>, offset: usize) -> Self {
207        Self::KeyOffset {
208            key: key.into(),
209            offset,
210        }
211    }
212}
213
214impl ScrollBehavior {
215    /// Snap directly to the target row.
216    pub const fn instant() -> Self {
217        Self::Instant
218    }
219
220    /// Animate target navigation with `config`.
221    pub const fn smooth(config: TransitionConfig) -> Self {
222        Self::Smooth(config)
223    }
224
225    /// Animate target navigation with [`TransitionConfig::default`].
226    pub fn smooth_default() -> Self {
227        Self::Smooth(TransitionConfig::default())
228    }
229
230    /// Animate target navigation with distance-based default timing.
231    pub fn smooth_adaptive() -> Self {
232        Self::SmoothDistance(ScrollDistanceConfig::default())
233    }
234
235    /// Animate target navigation with distance-based timing.
236    pub const fn smooth_distance(config: ScrollDistanceConfig) -> Self {
237        Self::SmoothDistance(config)
238    }
239
240    /// Return a representative transition config when this behavior is smooth.
241    ///
242    /// For distance-based behavior this returns the minimum-duration config.
243    /// Use [`Self::transition_config_for_distance`] when the target distance is
244    /// known.
245    pub const fn transition_config(self) -> Option<TransitionConfig> {
246        match self {
247            Self::Instant => None,
248            Self::Smooth(config) => Some(config),
249            Self::SmoothDistance(config) => Some(config.min_transition_config()),
250        }
251    }
252
253    /// Return the concrete transition config for a target distance.
254    pub fn transition_config_for_distance(self, distance_rows: usize) -> Option<TransitionConfig> {
255        match self {
256            Self::Instant => None,
257            Self::Smooth(config) => Some(config),
258            Self::SmoothDistance(config) => {
259                Some(config.transition_config_for_distance(distance_rows))
260            }
261        }
262    }
263}
264
265/// A scroll event.
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
267pub struct ScrollEvent {
268    /// New scroll offset (row index).
269    pub offset: usize,
270    /// Scroll metrics from the current viewport.
271    pub metrics: ScrollMetrics,
272}
273
274/// Visibility classification for a child in a [`ScrollView`](crate::widgets::ScrollView) viewport.
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
276pub enum ScrollChildVisibility {
277    /// The whole immediate child is visible in the effective viewport.
278    FullyVisible,
279    /// Only part of the immediate child is visible in the effective viewport.
280    PartiallyVisible,
281}
282
283/// Direction in which a previously visible child exited the viewport.
284#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
285pub enum ScrollChildExitDirection {
286    /// The child is now fully above the viewport.
287    Above,
288    /// The child is now fully below the viewport.
289    Below,
290    /// The child identity no longer exists or no longer has measurable geometry.
291    Removed,
292}
293
294/// Snapshot of one immediate [`ScrollView`](crate::widgets::ScrollView) child visible in the effective viewport.
295#[derive(Clone, Debug, PartialEq, Eq, Hash)]
296pub struct ScrollVisibleChild {
297    /// Immediate child index in the scroll view.
298    pub index: usize,
299    /// Immediate child key, when present.
300    pub key: Option<Key>,
301    /// Child rect relative to scroll content before offset.
302    pub content_rect: Rect,
303    /// Child rect relative to the effective child viewport after offset and indicators.
304    pub viewport_rect: Rect,
305    /// Clipped child portion in the effective child viewport.
306    pub visible_rect: Rect,
307    /// Number of visible rows for this child.
308    pub visible_height: u16,
309    /// Rows clipped above the effective viewport.
310    pub clipped_above: u16,
311    /// Rows clipped below the effective viewport.
312    pub clipped_below: u16,
313    /// Whether the child is fully or partially visible.
314    pub visibility: ScrollChildVisibility,
315}
316
317/// Previously visible child that exited the viewport.
318#[derive(Clone, Debug, PartialEq, Eq, Hash)]
319pub struct ScrollExitedChild {
320    /// Last visible snapshot for the child.
321    pub child: ScrollVisibleChild,
322    /// Exit direction relative to the current viewport.
323    pub direction: ScrollChildExitDirection,
324}
325
326/// Viewport-change event for visible immediate [`ScrollView`](crate::widgets::ScrollView) children.
327#[derive(Clone, Debug, PartialEq, Eq, Hash)]
328pub struct ScrollViewportEvent {
329    /// Current row offset.
330    pub offset: usize,
331    /// Scroll metrics from the current viewport.
332    pub metrics: ScrollMetrics,
333    /// Effective viewport width available to children.
334    pub viewport_width: u16,
335    /// Total number of immediate children.
336    pub children_len: usize,
337    /// First visible immediate child index, if any.
338    pub first_visible_index: Option<usize>,
339    /// Last visible immediate child index, if any.
340    pub last_visible_index: Option<usize>,
341    /// Visible immediate children after clipping.
342    pub visible: Vec<ScrollVisibleChild>,
343    /// Children whose identity became visible since the last emitted snapshot.
344    /// Children that remain visible but change partial/full clipping stay in [`Self::visible`].
345    pub entered: Vec<ScrollVisibleChild>,
346    /// Children whose identity exited since the last emitted snapshot.
347    /// Children that remain visible but change partial/full clipping stay in [`Self::visible`].
348    pub exited: Vec<ScrollExitedChild>,
349    /// Whether a top scroll indicator consumes a row.
350    pub top_indicator: bool,
351    /// Whether a bottom scroll indicator consumes a row.
352    pub bottom_indicator: bool,
353    /// Count displayed by the bottom indicator.
354    pub bottom_count: usize,
355}