Skip to main content

tui_lipan/widgets/scroll_view/
mod.rs

1//! Scroll view widget.
2
3pub mod layout;
4pub mod node;
5pub mod reconcile;
6pub(crate) mod utils;
7
8pub(crate) use self::layout::measure_scroll_view;
9pub(crate) use self::reconcile::{ScrollViewReconcile, reconcile_scroll_view};
10
11use crate::callback::Callback;
12use crate::core::element::{Element, ElementKind, Key};
13use crate::style::{Align, BorderStyle, Length, Padding, ScrollbarConfig, Style};
14use crate::widgets::internal::StackProps;
15
16pub(crate) use self::node::RememberedScrollAnchor;
17pub use self::node::ScrollViewNode;
18
19pub use crate::widgets::scroll::{
20    ScrollAxis, ScrollBehavior, ScrollChildExitDirection, ScrollChildVisibility, ScrollClip,
21    ScrollDistanceConfig, ScrollEvent, ScrollExitedChild, ScrollKeymap, ScrollMetrics,
22    ScrollRequest, ScrollTarget, ScrollViewportEvent, ScrollVisibleChild, ScrollWheelBehavior,
23    ScrollWheelConfig,
24};
25
26/// A scrollable vertical container.
27#[derive(Clone)]
28pub struct ScrollView {
29    /// Layout properties.
30    pub(crate) props: StackProps,
31    /// Row index scrolled from top (0 = at top).
32    pub(crate) offset: Option<usize>,
33    /// Horizontal content range to reveal with the smallest necessary scroll.
34    pub(crate) horizontal_reveal_range: Option<(usize, usize)>,
35    /// One-shot scroll request applied relative to the current viewport.
36    pub(crate) scroll_request: Option<ScrollRequest>,
37    /// Framework-owned semantic scroll target.
38    pub(crate) scroll_target: Option<ScrollTarget>,
39    /// How semantic scroll targets are applied.
40    pub(crate) scroll_behavior: ScrollBehavior,
41    /// Key bindings to move the viewport.
42    pub(crate) scroll_keys: ScrollKeymap,
43    /// Enable mouse wheel scrolling.
44    pub(crate) scroll_wheel: bool,
45    /// Widget-local mouse wheel step multiplier, overriding the app default when set.
46    pub(crate) scroll_wheel_multiplier: Option<u16>,
47    /// Widget-local horizontal (Shift+wheel) step multiplier. Falls back to
48    /// `scroll_wheel_multiplier`, then the app default, when unset.
49    pub(crate) h_scroll_wheel_multiplier: Option<u16>,
50    /// How mouse wheel deltas are applied.
51    pub(crate) scroll_wheel_behavior: ScrollWheelBehavior,
52    /// Allow PageUp/PageDown to target this view as an ambient fallback.
53    pub(crate) ambient_page_scroll: bool,
54    /// Whether the scroll view can receive focus.
55    pub(crate) focusable: bool,
56    /// Callback fired when the scroll offset changes.
57    pub(crate) on_scroll: Option<Callback<ScrollEvent>>,
58    /// Callback fired when the scrollbar is dragged/clicked.
59    pub(crate) on_scroll_to: Option<Callback<usize>>,
60    /// Callback fired when visible children or viewport metadata changes.
61    pub(crate) on_viewport_change: Option<Callback<ScrollViewportEvent>>,
62    /// Draw a vertical scrollbar when content overflows.
63    pub(crate) scrollbar: bool,
64    /// Scrollbar configuration.
65    pub(crate) scrollbar_config: ScrollbarConfig,
66    /// Show scroll indicators when content is clipped.
67    pub(crate) show_scroll_indicators: bool,
68    pub(crate) scroll_indicator_style: Style,
69    pub(crate) clip_mode: ScrollClip,
70    /// Hint for initial estimated height of unmeasured off-screen children.
71    /// Only used as the cold-start fallback before a running average of
72    /// measured children is available.
73    pub(crate) estimated_child_height: u16,
74    /// Stable key used for persisting scroll anchor state across remounts.
75    /// When set, this key is used instead of the element key for storing
76    /// and restoring remembered scroll anchors and bottom-pinning state.
77    /// Useful when the element key must change (e.g. to force cache rebuild)
78    /// but scroll position should be preserved.
79    pub(crate) scroll_state_key: Option<Key>,
80    /// Scroll axes enabled for this view.
81    pub(crate) axis: ScrollAxis,
82    /// Draw a horizontal scrollbar when content overflows horizontally.
83    pub(crate) h_scrollbar: bool,
84    /// Horizontal scrollbar configuration.
85    pub(crate) h_scrollbar_config: ScrollbarConfig,
86    /// Children.
87    pub(crate) children: Vec<Element>,
88}
89
90impl Default for ScrollView {
91    fn default() -> Self {
92        Self {
93            props: StackProps::default(),
94            offset: None,
95            horizontal_reveal_range: None,
96            scroll_request: None,
97            scroll_target: None,
98            scroll_behavior: ScrollBehavior::default(),
99            scroll_keys: ScrollKeymap::default(),
100            scroll_wheel: true,
101            scroll_wheel_multiplier: None,
102            h_scroll_wheel_multiplier: None,
103            scroll_wheel_behavior: ScrollWheelBehavior::default(),
104            ambient_page_scroll: false,
105            focusable: false,
106            on_scroll: None,
107            on_scroll_to: None,
108            on_viewport_change: None,
109            scrollbar: false,
110            scrollbar_config: ScrollbarConfig::default(),
111            show_scroll_indicators: false,
112            scroll_indicator_style: Style::default(),
113            clip_mode: ScrollClip::default(),
114            estimated_child_height: 3,
115            scroll_state_key: None,
116            axis: ScrollAxis::default(),
117            h_scrollbar: false,
118            h_scrollbar_config: ScrollbarConfig::default(),
119            children: Vec::new(),
120        }
121    }
122}
123
124impl ScrollView {
125    /// Create an empty scroll view.
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// Add a child.
131    pub fn child(mut self, child: impl Into<Element>) -> Self {
132        self.children.push(child.into());
133        self
134    }
135
136    /// Replace all children, discarding anything already added with
137    /// [`child`](Self::child). Call `child` repeatedly to append instead.
138    pub fn children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
139        self.children = children.into_iter().collect();
140        self
141    }
142
143    /// Set border.
144    pub fn border(mut self, border: bool) -> Self {
145        self.props.border = border;
146        self
147    }
148
149    /// Set border style.
150    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
151        self.props.border_style = border_style;
152        self
153    }
154
155    /// Set padding.
156    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
157        self.props.padding = padding.into();
158        self
159    }
160
161    /// Set base style.
162    pub fn style(mut self, style: Style) -> Self {
163        self.props.style = style;
164        self
165    }
166
167    /// Set gap.
168    pub fn gap(mut self, gap: u16) -> Self {
169        self.props.gap = gap;
170        self
171    }
172
173    /// Set requested width (cross-axis for a vertical `ScrollView`).
174    pub fn width(mut self, width: Length) -> Self {
175        self.props.width = width;
176        self
177    }
178
179    /// Set requested height (main-axis for a vertical `ScrollView`).
180    pub fn height(mut self, height: Length) -> Self {
181        self.props.height = height;
182        self
183    }
184
185    /// Set cross-axis alignment.
186    pub fn align(mut self, align: Align) -> Self {
187        self.props.align = align;
188        self
189    }
190
191    /// Set scroll offset (row index from top).
192    pub fn offset(mut self, offset: usize) -> Self {
193        self.offset = Some(offset);
194        self
195    }
196
197    /// Reveal a horizontal content range with the smallest necessary scroll.
198    ///
199    /// The request is reapplied when the range or viewport width changes. User scrolling remains
200    /// authoritative while both stay unchanged.
201    pub fn reveal_horizontal_range(mut self, start: usize, end: usize) -> Self {
202        self.horizontal_reveal_range = Some((start.min(end), start.max(end)));
203        self
204    }
205
206    /// Apply a one-shot scroll request relative to the current viewport.
207    ///
208    /// This is useful for command-driven navigation such as page up/down or
209    /// jump-to-top/bottom without continuously controlling the settled offset.
210    /// When set, it takes priority over `.offset(...)` but not over
211    /// `.scroll_to_key(...)`.
212    pub fn scroll_request(mut self, request: ScrollRequest) -> Self {
213        self.scroll_request = Some(request);
214        self
215    }
216
217    /// Scroll to a semantic target.
218    ///
219    /// Edge targets (`Top` / `Bottom`) resolve against the current content
220    /// extent and do not require sentinel children. Key targets preserve the
221    /// same behavior as [`Self::scroll_to_key`]. Target navigation uses
222    /// [`Self::scroll_behavior`].
223    pub fn scroll_to(mut self, target: ScrollTarget) -> Self {
224        self.scroll_target = Some(target);
225        self
226    }
227
228    /// Scroll to the start of the content.
229    pub fn scroll_to_top(self) -> Self {
230        self.scroll_to(ScrollTarget::Top)
231    }
232
233    /// Scroll to the end of the content.
234    pub fn scroll_to_bottom(self) -> Self {
235        self.scroll_to(ScrollTarget::Bottom)
236    }
237
238    /// Scroll so the first child subtree containing `key` is brought into view.
239    ///
240    /// This is useful for jump-to-result flows, such as scrolling a message list
241    /// to a matched entry after search. When set, it takes priority over
242    /// `.offset(...)`.
243    pub fn scroll_to_key(mut self, key: impl Into<Key>) -> Self {
244        self.scroll_target = Some(ScrollTarget::Key(key.into()));
245        self
246    }
247
248    /// Scroll to `offset` rows below the first child subtree containing `key`.
249    ///
250    /// This is useful when a keyed row contains a large auto-height child and
251    /// navigation needs to land inside that row, for example one auto-height
252    /// `DiffView` per file with global hunk navigation.
253    pub fn scroll_to_key_offset(mut self, key: impl Into<Key>, offset: usize) -> Self {
254        self.scroll_target = Some(ScrollTarget::key_offset(key, offset));
255        self
256    }
257
258    /// Configure how semantic target navigation is applied.
259    ///
260    /// This affects only framework-owned targets from `.scroll_to(...)`,
261    /// `.scroll_to_key(...)`, `.scroll_to_top()`, and `.scroll_to_bottom()`;
262    /// requests, controlled offsets, and user input remain immediate.
263    pub fn scroll_behavior(mut self, behavior: ScrollBehavior) -> Self {
264        self.scroll_behavior = behavior;
265        self
266    }
267
268    /// Animate semantic target navigation with `config`.
269    pub fn scroll_transition(mut self, config: crate::animation::TransitionConfig) -> Self {
270        self.scroll_behavior = ScrollBehavior::smooth(config);
271        self
272    }
273
274    /// Set a stable key for persisting scroll anchor state across remounts.
275    ///
276    /// When the element key must change (e.g. to force a layout cache rebuild
277    /// after toggling child visibility), the scroll position is normally lost
278    /// because the remembered anchor is stored under the old key. Setting a
279    /// stable `scroll_state_key` ensures the anchor survives key changes.
280    pub fn scroll_state_key(mut self, key: impl Into<Key>) -> Self {
281        self.scroll_state_key = Some(key.into());
282        self
283    }
284
285    /// Configure which keys move the viewport.
286    pub fn scroll_keys(mut self, keys: ScrollKeymap) -> Self {
287        self.scroll_keys = keys;
288        if keys != ScrollKeymap::NONE {
289            self.focusable = true;
290        }
291        self
292    }
293
294    /// Enable mouse wheel scrolling.
295    pub fn scroll_wheel(mut self, enabled: bool) -> Self {
296        self.scroll_wheel = enabled;
297        self
298    }
299
300    /// Override the app-wide mouse wheel step multiplier for this scroll view.
301    pub fn scroll_wheel_multiplier(mut self, multiplier: u16) -> Self {
302        self.scroll_wheel_multiplier = Some(multiplier.max(1));
303        self
304    }
305
306    /// Override the step multiplier for *horizontal* wheel panning (Shift+wheel).
307    ///
308    /// Horizontal scrolling moves in columns, which are finer-grained than the
309    /// rows used for vertical scrolling, so a larger horizontal step usually
310    /// feels better for wide content. Falls back to
311    /// [`Self::scroll_wheel_multiplier`], then the app-wide multiplier.
312    pub fn h_scroll_wheel_multiplier(mut self, multiplier: u16) -> Self {
313        self.h_scroll_wheel_multiplier = Some(multiplier.max(1));
314        self
315    }
316
317    /// Configure how mouse wheel input is applied.
318    pub fn scroll_wheel_behavior(mut self, behavior: ScrollWheelBehavior) -> Self {
319        self.scroll_wheel_behavior = behavior;
320        self
321    }
322
323    /// Enable or disable smooth inertial wheel scrolling with default physics.
324    pub fn smooth_wheel_scroll(mut self, enabled: bool) -> Self {
325        self.scroll_wheel_behavior = if enabled {
326            ScrollWheelBehavior::smooth_default()
327        } else {
328            ScrollWheelBehavior::Immediate
329        };
330        self
331    }
332
333    /// Enable smooth wheel scrolling and set the wheel acceleration impulse.
334    pub fn scroll_acceleration(mut self, acceleration: f32) -> Self {
335        let mut config = match self.scroll_wheel_behavior {
336            ScrollWheelBehavior::Immediate => ScrollWheelConfig::default(),
337            ScrollWheelBehavior::Smooth(config) => config,
338        };
339        config.acceleration = acceleration;
340        self.scroll_wheel_behavior = ScrollWheelBehavior::Smooth(config);
341        self
342    }
343
344    /// Allow PageUp/PageDown to target this scroll view even when it is not focused.
345    ///
346    /// This is an explicit fallback used only when normal focused-widget dispatch
347    /// and component `on_key` bubbling do not handle the page key.
348    pub fn ambient_page_scroll(mut self, enabled: bool) -> Self {
349        self.ambient_page_scroll = enabled;
350        self
351    }
352
353    /// Allow the scroll view to receive focus.
354    pub fn focusable(mut self, focusable: bool) -> Self {
355        self.focusable = focusable;
356        self
357    }
358
359    /// Callback fired on mouse wheel scrolling.
360    pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
361        self.on_scroll = Some(cb);
362        self
363    }
364
365    /// Callback fired on scrollbar interaction (drag/click).
366    pub fn on_scroll_to(mut self, cb: Callback<usize>) -> Self {
367        self.on_scroll_to = Some(cb);
368        self
369    }
370
371    /// Callback fired when visible children or viewport metadata changes.
372    pub fn on_viewport_change(mut self, cb: Callback<ScrollViewportEvent>) -> Self {
373        self.on_viewport_change = Some(cb);
374        self
375    }
376
377    /// Draw a vertical scrollbar.
378    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
379        self.scrollbar = scrollbar;
380        self
381    }
382
383    /// Set scrollbar configuration.
384    pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
385        self.scrollbar_config = config;
386        self
387    }
388
389    /// Enable "N more" scroll indicators when items are hidden.
390    pub fn show_scroll_indicators(mut self, show: bool) -> Self {
391        self.show_scroll_indicators = show;
392        self
393    }
394
395    /// Set style for scroll indicators.
396    pub fn scroll_indicator_style(mut self, style: Style) -> Self {
397        self.scroll_indicator_style = style;
398        self
399    }
400
401    /// Control how children are clipped against the viewport.
402    pub fn clip_mode(mut self, clip_mode: ScrollClip) -> Self {
403        self.clip_mode = clip_mode;
404        self
405    }
406
407    /// Hint for the initial estimated height of unmeasured off-screen children.
408    ///
409    /// Only used as the cold-start fallback before a running average of
410    /// measured children is available. Default: `3`.
411    pub fn estimated_child_height(mut self, height: u16) -> Self {
412        self.estimated_child_height = height;
413        self
414    }
415
416    /// Configure which scroll axes are active.
417    ///
418    /// Default is [`ScrollAxis::Vertical`] (historical behavior). Use
419    /// [`ScrollAxis::Both`] to enable horizontal panning for content wider than
420    /// the viewport.
421    pub fn axis(mut self, axis: ScrollAxis) -> Self {
422        self.axis = axis;
423        self
424    }
425
426    /// Draw a horizontal scrollbar when content overflows horizontally.
427    ///
428    /// Only effective when the axis includes horizontal scrolling.
429    pub fn h_scrollbar(mut self, h_scrollbar: bool) -> Self {
430        self.h_scrollbar = h_scrollbar;
431        self
432    }
433
434    /// Set horizontal scrollbar configuration.
435    pub fn h_scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
436        self.h_scrollbar_config = config;
437        self
438    }
439}
440
441impl From<ScrollView> for Element {
442    fn from(value: ScrollView) -> Self {
443        Element::new(ElementKind::ScrollView(Box::new(value)))
444    }
445}
446
447impl crate::layout::hash::LayoutHash for ScrollView {
448    fn layout_hash(
449        &self,
450        hasher: &mut impl std::hash::Hasher,
451        recurse: &dyn Fn(&Element) -> Option<u64>,
452    ) -> Option<()> {
453        use std::hash::Hash;
454
455        crate::layout::hash::hash_stack_props(&self.props, hasher);
456        self.scrollbar.hash(hasher);
457        self.scrollbar_config.variant.hash(hasher);
458        self.scrollbar_config.gap.hash(hasher);
459        self.show_scroll_indicators.hash(hasher);
460        self.axis.hash(hasher);
461        self.h_scrollbar.hash(hasher);
462        self.h_scrollbar_config.variant.hash(hasher);
463        self.h_scrollbar_config.gap.hash(hasher);
464        crate::layout::hash::hash_children(&self.children, hasher, recurse)
465    }
466}