Skip to main content

fission_core/ui/widgets/
scroll.rs

1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::{traits::Lower, Node};
3use fission_ir::{
4    op::{FlexDirection, LayoutOp, Op},
5    NodeId,
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(Box::new(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<NodeId>,
30    /// The scrollable content.
31    pub child: Option<Box<Node>>,
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    pub fn into_node(self) -> Node {
48        Node::Scroll(self)
49    }
50}
51
52impl Default for Scroll {
53    fn default() -> Self {
54        Self {
55            id: None,
56            child: None,
57            direction: FlexDirection::Column,
58            width: None,
59            height: None,
60            show_scrollbar: false,
61            flex_grow: 0.0,
62            flex_shrink: 0.0,
63        }
64    }
65}
66
67impl Lower for Scroll {
68    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
69        let layout_id = self.id.unwrap_or_else(|| cx.next_node_id());
70        
71        cx.push_scope(layout_id);
72
73        let mut builder = NodeBuilder::new(
74            layout_id,
75            Op::Layout(LayoutOp::Scroll {
76                direction: self.direction,
77                show_scrollbar: self.show_scrollbar,
78                width: self.width,
79                height: self.height,
80                min_width: None,
81                max_width: None,
82                min_height: None,
83                max_height: None,
84                padding: [0.0; 4],
85                flex_grow: self.flex_grow,
86                flex_shrink: self.flex_shrink,
87            }),
88        );
89        if let Some(child) = &self.child {
90            // Wrap content in a non-shrinking Box to ensure it overflows the viewport
91            // allowing scrolling to work.
92            let content_id = cx.next_node_id();
93            let mut content_box = NodeBuilder::new(
94                content_id,
95                Op::Layout(LayoutOp::Box {
96                    width: None,
97                    height: None,
98                    min_width: None,
99                    max_width: None,
100                    min_height: None,
101                    max_height: None,
102                    padding: [0.0; 4],
103                    flex_grow: 0.0,
104                    flex_shrink: 0.0,
105                    aspect_ratio: None,
106                })
107            );
108            content_box.add_child(child.lower(cx));
109            builder.add_child(content_box.build(cx));
110        }
111
112        cx.pop_scope();
113
114        builder.build(cx)
115    }
116}