Skip to main content

fui/node/
scroll_box.rs

1use super::*;
2use crate::animation::AnimationTiming;
3use crate::ffi::{Orientation, Unit};
4use crate::persisted::{store_scroll_offset, try_load_scroll_offset};
5use crate::signal::Subscription;
6use crate::transitions::NodeTransitions;
7use std::cell::{Cell, RefCell};
8use std::rc::{Rc, Weak};
9
10const DEFAULT_SCROLLBAR_GUTTER: f32 = 0.0;
11const OVERFLOW_TOLERANCE: f32 = 0.5;
12
13fn clamp(value: f32, min: f32, max: f32) -> f32 {
14    if value < min {
15        return min;
16    }
17    if value > max {
18        return max;
19    }
20    value
21}
22
23struct ScrollBoxInner {
24    root: FlexBox,
25    scroll_state: ScrollState,
26    viewport: ScrollView,
27    top_row: FlexBox,
28    bottom_row: FlexBox,
29    vertical_gutter: FlexBox,
30    corner: FlexBox,
31    vertical_scrollbar: ScrollBar,
32    horizontal_scrollbar: ScrollBar,
33    vertical_visibility: Cell<ScrollBarVisibility>,
34    horizontal_visibility: Cell<ScrollBarVisibility>,
35    vertical_scroll_enabled: Cell<bool>,
36    horizontal_scroll_enabled: Cell<bool>,
37    scrollbar_gutter: Cell<f32>,
38    persist_scroll: Cell<bool>,
39    persisted_restore_pending: Cell<bool>,
40    subscriptions: RefCell<Vec<Subscription>>,
41}
42
43#[derive(Clone)]
44pub struct ScrollBox {
45    inner: Rc<ScrollBoxInner>,
46}
47
48impl ScrollBox {
49    pub fn new() -> Self {
50        Self::with_parts(ScrollState::new(), ScrollView::new())
51    }
52
53    pub fn with_parts(scroll_state: ScrollState, viewport: ScrollView) -> Self {
54        let root = column();
55        let viewport = viewport
56            .bind_scroll_state(scroll_state.clone())
57            .fill_size()
58            .clone();
59        let vertical_gutter = flex_box();
60        vertical_gutter
61            .width(DEFAULT_SCROLLBAR_GUTTER, Unit::Pixel)
62            .fill_height()
63            .on_pointer_down(|_event| {});
64        let vertical_scrollbar = ScrollBar::new(scroll_state.clone(), Orientation::Vertical);
65        let corner = flex_box();
66        corner
67            .width(0.0, Unit::Pixel)
68            .height(0.0, Unit::Pixel)
69            .on_pointer_down(|_event| {});
70        let horizontal_scrollbar = ScrollBar::new(scroll_state.clone(), Orientation::Horizontal);
71        let top_row = row();
72        top_row
73            .fill_size()
74            .on_pointer_down(|_event| {})
75            .child(&viewport)
76            .child(&vertical_gutter)
77            .child(&vertical_scrollbar.render());
78        let bottom_row = row();
79        bottom_row
80            .fill_width()
81            .on_pointer_down(|_event| {})
82            .child(&horizontal_scrollbar.render())
83            .child(&corner);
84        root.child(&top_row).child(&bottom_row);
85
86        let inner = Rc::new(ScrollBoxInner {
87            root,
88            scroll_state,
89            viewport,
90            top_row,
91            bottom_row,
92            vertical_gutter,
93            corner,
94            vertical_scrollbar,
95            horizontal_scrollbar,
96            vertical_visibility: Cell::new(ScrollBarVisibility::Auto),
97            horizontal_visibility: Cell::new(ScrollBarVisibility::Auto),
98            vertical_scroll_enabled: Cell::new(true),
99            horizontal_scroll_enabled: Cell::new(true),
100            scrollbar_gutter: Cell::new(DEFAULT_SCROLLBAR_GUTTER),
101            persist_scroll: Cell::new(true),
102            persisted_restore_pending: Cell::new(false),
103            subscriptions: RefCell::new(Vec::new()),
104        });
105        let scroll_box = Self { inner };
106        scroll_box.attach_listeners();
107        scroll_box.refresh_chrome();
108        scroll_box
109    }
110
111    pub fn scroll_state(&self) -> ScrollState {
112        self.inner.scroll_state.clone()
113    }
114
115    pub fn viewport(&self) -> ScrollView {
116        self.inner.viewport.clone()
117    }
118
119    pub fn vertical_scrollbar(&self) -> ScrollBar {
120        self.inner.vertical_scrollbar.clone()
121    }
122
123    pub fn horizontal_scrollbar(&self) -> ScrollBar {
124        self.inner.horizontal_scrollbar.clone()
125    }
126
127    pub fn scroll_enabled_x(&self, enabled: bool) -> &Self {
128        self.inner.horizontal_scroll_enabled.set(enabled);
129        self.inner.viewport.scroll_enabled_x(enabled);
130        self.refresh_chrome();
131        self
132    }
133
134    pub fn scroll_enabled_y(&self, enabled: bool) -> &Self {
135        self.inner.vertical_scroll_enabled.set(enabled);
136        self.inner.viewport.scroll_enabled_y(enabled);
137        self.refresh_chrome();
138        self
139    }
140
141    pub fn smooth_scrolling(&self, smooth_scrolling: bool) -> &Self {
142        self.inner.viewport.smooth_scrolling(smooth_scrolling);
143        self
144    }
145
146    pub fn persist_scroll(&self, persist: bool) -> &Self {
147        self.inner.persist_scroll.set(persist);
148        if !persist {
149            self.inner.persisted_restore_pending.set(false);
150        }
151        self
152    }
153
154    pub fn scroll_offset(&self, x: f32, y: f32) -> &Self {
155        self.inner.viewport.scroll_offset(x, y);
156        self
157    }
158
159    pub fn transitions(&self, transitions: Option<NodeTransitions>) -> &Self {
160        self.inner.viewport.transitions(transitions);
161        self
162    }
163
164    pub fn scroll_content_size(&self, width: f32, height: f32) -> &Self {
165        self.inner.viewport.scroll_content_size(width, height);
166        self
167    }
168
169    pub fn scroll_to(&self, x: f32, y: f32) -> &Self {
170        self.inner.viewport.scroll_to(x, y);
171        self
172    }
173
174    pub fn scroll_to_animated(&self, x: f32, y: f32, timing: AnimationTiming) -> &Self {
175        self.inner.viewport.scroll_to_animated(x, y, timing);
176        self
177    }
178
179    pub fn set_runtime_scroll_offset(&self, x: f32, y: f32) {
180        self.inner.viewport.set_runtime_scroll_offset(x, y);
181    }
182
183    pub fn vertical_scrollbar_visibility(&self, mode: ScrollBarVisibility) -> &Self {
184        self.inner.vertical_visibility.set(mode);
185        self.refresh_chrome();
186        self
187    }
188
189    pub fn horizontal_scrollbar_visibility(&self, mode: ScrollBarVisibility) -> &Self {
190        self.inner.horizontal_visibility.set(mode);
191        self.refresh_chrome();
192        self
193    }
194
195    pub fn scrollbar_gutter(&self, value: f32) -> &Self {
196        self.inner.scrollbar_gutter.set(value.max(0.0));
197        self.refresh_chrome();
198        self
199    }
200
201    pub fn scrollbar_style(&self, style: ScrollBarStyle) -> &Self {
202        style.apply_to(&self.inner.vertical_scrollbar);
203        style.apply_to(&self.inner.horizontal_scrollbar);
204        self.refresh_chrome();
205        self
206    }
207
208    fn attach_listeners(&self) {
209        let weak = Rc::downgrade(&self.inner);
210        self.inner
211            .vertical_scrollbar
212            .set_chrome_metric_changed_handler(move || {
213                if let Some(inner) = Weak::upgrade(&weak) {
214                    ScrollBox { inner }.refresh_chrome();
215                }
216            });
217        let weak = Rc::downgrade(&self.inner);
218        self.inner
219            .horizontal_scrollbar
220            .set_chrome_metric_changed_handler(move || {
221                if let Some(inner) = Weak::upgrade(&weak) {
222                    ScrollBox { inner }.refresh_chrome();
223                }
224            });
225
226        let weak = Rc::downgrade(&self.inner);
227        self.inner
228            .subscriptions
229            .borrow_mut()
230            .push(self.inner.scroll_state.subscribe_offset_x(move || {
231                if let Some(inner) = Weak::upgrade(&weak) {
232                    let scroll_box = ScrollBox { inner };
233                    scroll_box.store_persisted_scroll_offset_if_needed();
234                    scroll_box.try_restore_persisted_scroll_offset();
235                }
236            }));
237        let weak = Rc::downgrade(&self.inner);
238        self.inner
239            .subscriptions
240            .borrow_mut()
241            .push(self.inner.scroll_state.subscribe_offset_y(move || {
242                if let Some(inner) = Weak::upgrade(&weak) {
243                    let scroll_box = ScrollBox { inner };
244                    scroll_box.store_persisted_scroll_offset_if_needed();
245                    scroll_box.try_restore_persisted_scroll_offset();
246                }
247            }));
248        let weak = Rc::downgrade(&self.inner);
249        self.inner.subscriptions.borrow_mut().push(
250            self.inner.scroll_state.subscribe_content_width(move || {
251                if let Some(inner) = Weak::upgrade(&weak) {
252                    let scroll_box = ScrollBox { inner };
253                    scroll_box.refresh_chrome();
254                    scroll_box.try_restore_persisted_scroll_offset();
255                }
256            }),
257        );
258        let weak = Rc::downgrade(&self.inner);
259        self.inner.subscriptions.borrow_mut().push(
260            self.inner.scroll_state.subscribe_content_height(move || {
261                if let Some(inner) = Weak::upgrade(&weak) {
262                    let scroll_box = ScrollBox { inner };
263                    scroll_box.refresh_chrome();
264                    scroll_box.try_restore_persisted_scroll_offset();
265                }
266            }),
267        );
268        let weak = Rc::downgrade(&self.inner);
269        self.inner.subscriptions.borrow_mut().push(
270            self.inner.scroll_state.subscribe_viewport_width(move || {
271                if let Some(inner) = Weak::upgrade(&weak) {
272                    let scroll_box = ScrollBox { inner };
273                    scroll_box.refresh_chrome();
274                    scroll_box.bind_scroll_chrome();
275                    scroll_box.try_restore_persisted_scroll_offset();
276                }
277            }),
278        );
279        let weak = Rc::downgrade(&self.inner);
280        self.inner.subscriptions.borrow_mut().push(
281            self.inner.scroll_state.subscribe_viewport_height(move || {
282                if let Some(inner) = Weak::upgrade(&weak) {
283                    let scroll_box = ScrollBox { inner };
284                    scroll_box.refresh_chrome();
285                    scroll_box.bind_scroll_chrome();
286                    scroll_box.try_restore_persisted_scroll_offset();
287                }
288            }),
289        );
290    }
291
292    fn bind_scroll_chrome(&self) {
293        let viewport_handle = self.inner.viewport.handle().raw();
294        self.inner
295            .vertical_scrollbar
296            .bind_scroll_handle(viewport_handle);
297        self.inner
298            .horizontal_scrollbar
299            .bind_scroll_handle(viewport_handle);
300        self.inner
301            .root
302            .bind_scroll_proxy_target_handle(viewport_handle);
303        self.inner
304            .top_row
305            .bind_scroll_proxy_target_handle(viewport_handle);
306        self.inner
307            .bottom_row
308            .bind_scroll_proxy_target_handle(viewport_handle);
309        self.inner
310            .vertical_gutter
311            .bind_scroll_proxy_target_handle(viewport_handle);
312        self.inner
313            .corner
314            .bind_scroll_proxy_target_handle(viewport_handle);
315        self.bind_content_scroll_proxy_targets();
316    }
317
318    fn bind_content_scroll_proxy_targets(&self) {
319        let viewport_handle = self.inner.viewport.handle().raw();
320        let children = self.inner.viewport.retained_node_ref().children();
321        for child in children {
322            child.bind_scroll_proxy_target_handle(viewport_handle);
323        }
324    }
325
326    fn refresh_chrome(&self) {
327        let vertical_rail_thickness =
328            self.inner.scrollbar_gutter.get() + self.inner.vertical_scrollbar.thickness();
329        let horizontal_rail_thickness = self.inner.horizontal_scrollbar.thickness();
330        let show_vertical = self.should_show(
331            self.inner.vertical_visibility.get(),
332            self.inner.vertical_scroll_enabled.get(),
333            self.inner.scroll_state.content_height(),
334            self.inner.scroll_state.viewport_height(),
335        );
336        let show_horizontal = self.should_show(
337            self.inner.horizontal_visibility.get(),
338            self.inner.horizontal_scroll_enabled.get(),
339            self.inner.scroll_state.content_width(),
340            self.inner.scroll_state.viewport_width(),
341        );
342        self.inner.vertical_scrollbar.chrome_visible(show_vertical);
343        self.inner
344            .horizontal_scrollbar
345            .chrome_visible(show_horizontal);
346
347        self.inner.vertical_gutter.width(
348            if show_vertical {
349                self.inner.scrollbar_gutter.get()
350            } else {
351                0.0
352            },
353            Unit::Pixel,
354        );
355        self.inner.vertical_scrollbar.render().width(
356            if show_vertical {
357                self.inner.vertical_scrollbar.thickness()
358            } else {
359                0.0
360            },
361            Unit::Pixel,
362        );
363        let horizontal_rail_height = if show_horizontal {
364            horizontal_rail_thickness
365        } else {
366            0.0
367        };
368        self.inner
369            .bottom_row
370            .height(horizontal_rail_height, Unit::Pixel);
371        self.inner.horizontal_scrollbar.render().height(
372            if show_horizontal {
373                self.inner.horizontal_scrollbar.thickness()
374            } else {
375                0.0
376            },
377            Unit::Pixel,
378        );
379        let vertical_rail_width = if show_vertical {
380            vertical_rail_thickness
381        } else {
382            0.0
383        };
384        self.inner.corner.width(vertical_rail_width, Unit::Pixel);
385        self.inner
386            .corner
387            .height(horizontal_rail_height, Unit::Pixel);
388    }
389
390    fn should_show(
391        &self,
392        mode: ScrollBarVisibility,
393        enabled: bool,
394        content_size: f32,
395        viewport_size: f32,
396    ) -> bool {
397        if !enabled || mode == ScrollBarVisibility::Never {
398            return false;
399        }
400        if mode == ScrollBarVisibility::Always {
401            return true;
402        }
403        if viewport_size <= 0.0 || content_size <= 0.0 {
404            return false;
405        }
406        content_size > viewport_size + OVERFLOW_TOLERANCE
407    }
408
409    fn store_persisted_scroll_offset_if_needed(&self) {
410        if !self.inner.persist_scroll.get() {
411            return;
412        }
413        let Some(node_id) = self.inner.root.retained_node_ref().node_id() else {
414            return;
415        };
416        if node_id.is_empty() {
417            return;
418        }
419        store_scroll_offset(
420            &node_id,
421            self.inner.scroll_state.offset_x(),
422            self.inner.scroll_state.offset_y(),
423        );
424    }
425
426    fn try_restore_persisted_scroll_offset(&self) -> bool {
427        if !self.inner.persisted_restore_pending.get() || !self.inner.persist_scroll.get() {
428            return false;
429        }
430        let Some(node_id) = self.inner.root.retained_node_ref().node_id() else {
431            self.inner.persisted_restore_pending.set(false);
432            return false;
433        };
434        if node_id.is_empty() {
435            self.inner.persisted_restore_pending.set(false);
436            return false;
437        }
438        let Some(restored) = try_load_scroll_offset(&node_id) else {
439            self.inner.persisted_restore_pending.set(false);
440            return false;
441        };
442        let max_x = (self.inner.scroll_state.content_width()
443            - self.inner.scroll_state.viewport_width())
444        .max(0.0);
445        let max_y = (self.inner.scroll_state.content_height()
446            - self.inner.scroll_state.viewport_height())
447        .max(0.0);
448        let can_restore_x = !self.inner.horizontal_scroll_enabled.get()
449            || self.inner.scroll_state.viewport_width() > 0.0;
450        let can_restore_y = !self.inner.vertical_scroll_enabled.get()
451            || self.inner.scroll_state.viewport_height() > 0.0;
452        if !can_restore_x || !can_restore_y {
453            return false;
454        }
455        self.inner.persisted_restore_pending.set(false);
456        self.set_runtime_scroll_offset(
457            clamp(restored.x, 0.0, max_x),
458            clamp(restored.y, 0.0, max_y),
459        );
460        true
461    }
462}
463
464impl Default for ScrollBox {
465    fn default() -> Self {
466        Self::new()
467    }
468}
469
470impl Node for ScrollBox {
471    fn retained_node_ref(&self) -> NodeRef {
472        let scroll_box = self.clone();
473        self.inner
474            .root
475            .retained_node_ref()
476            .with_build_callback(move || scroll_box.build())
477    }
478
479    fn build(&self) {
480        self.ensure_handle();
481        self.inner.root.build_self();
482        self.build_children();
483        self.inner
484            .persisted_restore_pending
485            .set(self.inner.persist_scroll.get());
486        self.bind_scroll_chrome();
487        self.refresh_chrome();
488        let _ = self.try_restore_persisted_scroll_offset();
489    }
490
491    fn build_self(&self) {
492        self.inner.root.build_self();
493    }
494}
495
496impl HasFlexBoxRoot for ScrollBox {
497    fn flex_box_root(&self) -> &FlexBox {
498        &self.inner.root
499    }
500
501    fn append_flex_box_surface_child(&self, child: NodeRef) {
502        self.inner
503            .viewport
504            .retained_node_ref()
505            .append_child_ref(&child);
506        self.bind_content_scroll_proxy_targets();
507    }
508}
509
510impl ThemeBindable for ScrollBox {
511    fn theme_binding_node(&self) -> NodeRef {
512        self.inner.root.retained_node_ref()
513    }
514
515    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
516        let weak = Rc::downgrade(&self.inner);
517        Box::new(move || {
518            Some(ScrollBox {
519                inner: weak.upgrade()?,
520            })
521        })
522    }
523}