Skip to main content

fission_core/ui/widgets/
scroll.rs

1use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
2use crate::ui::{traits::InternalLower, Widget};
3use fission_ir::{
4    op::{FlexDirection, LayoutOp, Op},
5    WidgetId,
6};
7use serde::{Deserialize, Serialize};
8
9/// A scrollable container that clips its child and tracks scroll offset.
10///
11/// Scroll direction can be horizontal (`FlexDirection::Row`) or vertical
12/// (`FlexDirection::Column`). The runtime manages scroll state automatically
13/// in response to pointer scroll events.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// Scroll {
19///     direction: FlexDirection::Column,
20///     show_scrollbar: true,
21///     flex_grow: 1.0,
22///     child: Some(long_content),
23///     ..Default::default()
24/// }
25/// ```
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Scroll {
28    /// Explicit node identity (used for scroll-offset tracking).
29    pub id: Option<WidgetId>,
30    /// The scrollable content.
31    pub child: Option<Widget>,
32    /// Scroll axis: `Column` for vertical, `Row` for horizontal.
33    pub direction: FlexDirection,
34    /// Fixed width in layout points.
35    pub width: Option<f32>,
36    /// Fixed height in layout points.
37    pub height: Option<f32>,
38    /// Whether to render a scrollbar indicator.
39    pub show_scrollbar: bool,
40    /// Flex grow factor.
41    pub flex_grow: f32,
42    /// Flex shrink factor.
43    pub flex_shrink: f32,
44}
45
46impl Scroll {}
47
48impl Default for Scroll {
49    fn default() -> Self {
50        Self {
51            id: None,
52            child: None,
53            direction: FlexDirection::Column,
54            width: None,
55            height: None,
56            show_scrollbar: true,
57            flex_grow: 0.0,
58            flex_shrink: 0.0,
59        }
60    }
61}
62
63impl InternalLower for Scroll {
64    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
65        let layout_id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
66
67        cx.push_scope(layout_id);
68
69        let mut builder = InternalIrBuilder::new(
70            layout_id,
71            Op::Layout(LayoutOp::Scroll {
72                direction: self.direction,
73                show_scrollbar: self.show_scrollbar,
74                width: self.width,
75                height: self.height,
76                min_width: None,
77                max_width: None,
78                min_height: None,
79                max_height: None,
80                padding: [0.0; 4],
81                flex_grow: self.flex_grow,
82                flex_shrink: self.flex_shrink,
83            }),
84        );
85        if let Some(child) = &self.child {
86            // Wrap content in a non-shrinking Box to ensure it overflows the viewport
87            // allowing scrolling to work.
88            let content_id = cx.next_node_id();
89            let mut content_box = InternalIrBuilder::new(
90                content_id,
91                Op::Layout(LayoutOp::Box {
92                    width: None,
93                    height: None,
94                    min_width: None,
95                    max_width: None,
96                    min_height: None,
97                    max_height: None,
98                    padding: [0.0; 4],
99                    flex_grow: 0.0,
100                    flex_shrink: 0.0,
101                    aspect_ratio: None,
102                }),
103            );
104            content_box.add_child(child.lower(cx));
105            builder.add_child(content_box.build(cx));
106        }
107
108        cx.pop_scope();
109
110        builder.build(cx)
111    }
112}