telar_ui_core/
scroll_region.rs1use std::cell::RefCell;
14use std::mem::ManuallyDrop;
15
16use geometry_core::Rect;
17use layout_core::NodeId;
18use reactive_core::RwSignal;
19
20use crate::context::{absolute_rect, is_descendant_of};
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub struct ScrollRegionId(u64);
25
26struct ScrollRegion {
27 id: ScrollRegionId,
28 content: NodeId,
31 offset_x: RwSignal<f32>,
32 offset_y: RwSignal<f32>,
33}
34
35thread_local! {
37 static REGIONS: ManuallyDrop<RefCell<Vec<ScrollRegion>>> = ManuallyDrop::new(RefCell::new(Vec::new()));
38 static NEXT_ID: ManuallyDrop<RefCell<u64>> = ManuallyDrop::new(RefCell::new(0));
39}
40
41pub fn register_scroll_region(
43 content: NodeId,
44 offset_x: RwSignal<f32>,
45 offset_y: RwSignal<f32>,
46) -> ScrollRegionId {
47 let id = NEXT_ID.with(|n| {
48 let mut n = n.borrow_mut();
49 *n += 1;
50 ScrollRegionId(*n)
51 });
52 REGIONS.with(|regions| {
53 regions.borrow_mut().push(ScrollRegion {
54 id,
55 content,
56 offset_x,
57 offset_y,
58 })
59 });
60 id
61}
62
63pub fn unregister_scroll_region(id: ScrollRegionId) {
65 REGIONS.with(|regions| regions.borrow_mut().retain(|r| r.id != id));
66}
67
68pub fn visible_rect(node: NodeId) -> Option<Rect> {
74 let mut rect = absolute_rect(node)?;
75 REGIONS.with(|regions| {
76 for region in regions.borrow().iter() {
77 if is_descendant_of(node, region.content) {
78 rect.x -= region.offset_x.peek();
79 rect.y -= region.offset_y.peek();
80 }
81 }
82 });
83 Some(rect)
84}
85
86#[cfg(test)]
87mod tests {
88 use layout_core::{AvailableSpace, LayoutStyle, SizeDimension};
89 use reactive_core::signal;
90
91 use super::*;
92 use crate::container::Container;
93 use crate::context::{compute_layout, new_container, new_leaf, reset_layout_runtime};
94 use crate::layout_item::LayoutItem;
95
96 fn reset() {
97 REGIONS.with(|regions| regions.borrow_mut().clear());
98 }
99
100 #[test]
103 fn a_scrolled_node_reports_its_on_screen_position() {
104 reset_layout_runtime();
105 reset();
106 let (trigger, _r) = new_leaf(LayoutStyle::new().width(50.0).height(20.0)).unwrap();
107 let content = new_container(
108 LayoutStyle::new()
109 .flex_column()
110 .width(SizeDimension::Percent(1.0)),
111 &[trigger],
112 )
113 .unwrap();
114 compute_layout(
115 content,
116 AvailableSpace::Definite(300.0),
117 AvailableSpace::Definite(300.0),
118 )
119 .unwrap();
120
121 let laid_out = absolute_rect(trigger).unwrap();
122 assert_eq!(
123 visible_rect(trigger),
124 Some(laid_out),
125 "unscrolled: identical"
126 );
127
128 let (x, y) = (signal(0.0f32), signal(120.0f32));
129 let id = register_scroll_region(content, x.clone(), y.clone());
130 let shifted = visible_rect(trigger).unwrap();
131 assert_eq!(
132 shifted.y,
133 laid_out.y - 120.0,
134 "scrolled down 120px, so it is drawn 120px higher"
135 );
136 assert_eq!(shifted.x, laid_out.x);
137 assert_eq!(
138 shifted.width, laid_out.width,
139 "scrolling moves a node, it does not resize it"
140 );
141
142 y.set(0.0);
144 assert_eq!(visible_rect(trigger), Some(laid_out));
145 y.set(80.0);
146 unregister_scroll_region(id);
147 assert_eq!(visible_rect(trigger), Some(laid_out));
148 }
149
150 #[test]
152 fn nested_scroll_offsets_accumulate() {
153 reset_layout_runtime();
154 reset();
155 let inner_leaf =
156 Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
157 let trigger = inner_leaf.layout_node();
158 let inner = new_container(LayoutStyle::new().flex_column(), &[trigger]).unwrap();
159 let outer = new_container(LayoutStyle::new().flex_column(), &[inner]).unwrap();
160 compute_layout(
161 outer,
162 AvailableSpace::Definite(300.0),
163 AvailableSpace::Definite(300.0),
164 )
165 .unwrap();
166 let laid_out = absolute_rect(trigger).unwrap();
167
168 register_scroll_region(outer, signal(0.0), signal(30.0));
169 register_scroll_region(inner, signal(5.0), signal(7.0));
170 let shifted = visible_rect(trigger).unwrap();
171 assert_eq!(shifted.y, laid_out.y - 37.0);
172 assert_eq!(shifted.x, laid_out.x - 5.0);
173 }
174
175 #[test]
178 fn a_node_outside_the_region_is_unaffected() {
179 reset_layout_runtime();
180 reset();
181 let (inside, _a) = new_leaf(LayoutStyle::new().width(10.0).height(10.0)).unwrap();
182 let (outside, _b) = new_leaf(LayoutStyle::new().width(10.0).height(10.0)).unwrap();
183 let scrolled = new_container(LayoutStyle::new().flex_column(), &[inside]).unwrap();
184 let root = new_container(LayoutStyle::new().flex_column(), &[scrolled, outside]).unwrap();
185 compute_layout(
186 root,
187 AvailableSpace::Definite(200.0),
188 AvailableSpace::Definite(200.0),
189 )
190 .unwrap();
191
192 let outside_before = absolute_rect(outside).unwrap();
193 register_scroll_region(scrolled, signal(0.0), signal(50.0));
194 assert_eq!(visible_rect(outside), Some(outside_before));
195 assert_eq!(
196 visible_rect(inside).unwrap().y,
197 absolute_rect(inside).unwrap().y - 50.0
198 );
199 }
200}