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}