1use std::sync::Arc;
2
3use crate::binding::RatioLens;
4use crate::prelude::*;
5
6pub(crate) const SCROLL_SENSITIVITY: f32 = 20.0;
7
8pub enum ScrollEvent {
10 SetX(f32),
12 SetY(f32),
14 ScrollX(f32),
16 ScrollY(f32),
18 ChildGeo(f32, f32),
20}
21
22#[derive(Lens, Data, Clone)]
24pub struct ScrollView {
25 pub scroll_x: f32,
27 pub scroll_y: f32,
29
30 #[lens(ignore)]
32 pub on_scroll: Option<Arc<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
33
34 pub inner_width: f32,
36 pub inner_height: f32,
38 pub container_width: f32,
40 pub container_height: f32,
42 pub scroll_to_cursor: bool,
44 pub show_horizontal_scrollbar: bool,
46 pub show_vertical_scrollbar: bool,
48}
49
50impl ScrollView {
51 pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
53 where
54 F: 'static + FnOnce(&mut Context),
55 {
56 Self {
57 scroll_to_cursor: false,
58 scroll_x: 0.0,
59 scroll_y: 0.0,
60 on_scroll: None,
61 inner_width: 0.0,
62 inner_height: 0.0,
63 container_width: 0.0,
64 container_height: 0.0,
65 show_horizontal_scrollbar: true,
66 show_vertical_scrollbar: true,
67 }
68 .build(cx, move |cx| {
69 ScrollContent::new(cx, content);
70
71 Binding::new(cx, ScrollView::show_vertical_scrollbar, |cx, show_scrollbar| {
72 if show_scrollbar.get(cx) {
73 Scrollbar::new(
74 cx,
75 ScrollView::scroll_y,
76 RatioLens::new(ScrollView::container_height, ScrollView::inner_height),
77 Orientation::Vertical,
78 |cx, value| {
79 cx.emit(ScrollEvent::SetY(value));
80 },
81 )
82 .position_type(PositionType::Absolute)
83 .scroll_to_cursor(Self::scroll_to_cursor);
84 }
85 });
86
87 Binding::new(cx, ScrollView::show_vertical_scrollbar, |cx, show_scrollbar| {
88 if show_scrollbar.get(cx) {
89 Scrollbar::new(
90 cx,
91 ScrollView::scroll_x,
92 RatioLens::new(ScrollView::container_width, ScrollView::inner_width),
93 Orientation::Horizontal,
94 |cx, value| {
95 cx.emit(ScrollEvent::SetX(value));
96 },
97 )
98 .position_type(PositionType::Absolute)
99 .scroll_to_cursor(Self::scroll_to_cursor);
100 }
101 });
102 })
103 .bind(ScrollView::root, |mut handle, data| {
104 let data = data.get(&handle);
105 let scale_factor = handle.context().scale_factor();
106 let top = ((data.inner_height - data.container_height) * data.scroll_y).round()
107 / scale_factor;
108 let left =
109 ((data.inner_width - data.container_width) * data.scroll_x).round() / scale_factor;
110 handle.horizontal_scroll(-left.abs()).vertical_scroll(-top.abs());
111 })
112 .toggle_class(
113 "h-scroll",
114 ScrollView::root.map(|data| data.container_width < data.inner_width),
115 )
116 .toggle_class(
117 "v-scroll",
118 ScrollView::root.map(|data| data.container_height < data.inner_height),
119 )
120 }
121
122 fn reset(&mut self) {
123 if self.inner_width == self.container_width {
124 self.scroll_x = 0.0;
125 }
126
127 if self.inner_height == self.container_height {
128 self.scroll_y = 0.0;
129 }
130 }
131}
132
133impl View for ScrollView {
134 fn element(&self) -> Option<&'static str> {
135 Some("scrollview")
136 }
137
138 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
139 event.map(|scroll_update, meta| {
140 match scroll_update {
141 ScrollEvent::ScrollX(f) => {
142 self.scroll_x = (self.scroll_x + *f).clamp(0.0, 1.0);
143
144 if let Some(callback) = &self.on_scroll {
145 (callback)(cx, self.scroll_x, self.scroll_y);
146 }
147 }
148
149 ScrollEvent::ScrollY(f) => {
150 self.scroll_y = (self.scroll_y + *f).clamp(0.0, 1.0);
151 if let Some(callback) = &self.on_scroll {
152 (callback)(cx, self.scroll_x, self.scroll_y);
153 }
154 }
155
156 ScrollEvent::SetX(f) => {
157 self.scroll_x = *f;
158 if let Some(callback) = &self.on_scroll {
159 (callback)(cx, self.scroll_x, self.scroll_y);
160 }
161 }
162
163 ScrollEvent::SetY(f) => {
164 self.scroll_y = *f;
165 if let Some(callback) = &self.on_scroll {
166 (callback)(cx, self.scroll_x, self.scroll_y);
167 }
168 }
169
170 ScrollEvent::ChildGeo(w, h) => {
171 let bounds = cx.bounds();
172 let scale_factor = cx.scale_factor();
173
174 if self.inner_width != 0.0
175 && self.inner_height != 0.0
176 && self.container_width != 0.0
177 && self.container_height != 0.0
178 {
179 let top = ((self.inner_height - self.container_height) * self.scroll_y)
180 .round()
181 / scale_factor;
182 let left = ((self.inner_width - self.container_width) * self.scroll_x)
183 .round()
184 / scale_factor;
185
186 self.container_width = bounds.width();
187 self.container_height = bounds.height();
188 self.inner_width = *w;
189 self.inner_height = *h;
190
191 self.scroll_y = ((top * scale_factor)
192 / (self.inner_height - self.container_height))
193 .clamp(0.0, 1.0);
194 self.scroll_x = ((left * scale_factor)
195 / (self.inner_width - self.container_width))
196 .clamp(0.0, 1.0);
197
198 if let Some(callback) = &self.on_scroll {
199 (callback)(cx, self.scroll_x, self.scroll_y);
200 }
201
202 self.reset();
203 }
204
205 self.inner_width = *w;
206 self.inner_height = *h;
207 self.reset();
208 }
209 }
210
211 meta.consume();
214 });
215
216 event.map(|window_event, meta| match window_event {
217 WindowEvent::GeometryChanged(geo) => {
218 if geo.contains(GeoChanged::WIDTH_CHANGED)
219 || geo.contains(GeoChanged::HEIGHT_CHANGED)
220 {
221 let bounds = cx.bounds();
222 let scale_factor = cx.scale_factor();
223
224 if self.inner_width != 0.0
225 && self.inner_height != 0.0
226 && self.container_width != 0.0
227 && self.container_height != 0.0
228 {
229 let top = ((self.inner_height - self.container_height) * self.scroll_y)
230 .round()
231 / scale_factor;
232 let left = ((self.inner_width - self.container_width) * self.scroll_x)
233 .round()
234 / scale_factor;
235
236 self.container_width = bounds.width();
237 self.container_height = bounds.height();
238
239 self.scroll_y = ((top * scale_factor)
240 / (self.inner_height - self.container_height))
241 .clamp(0.0, 1.0);
242 self.scroll_x = ((left * scale_factor)
243 / (self.inner_width - self.container_width))
244 .clamp(0.0, 1.0);
245 if let Some(callback) = &self.on_scroll {
246 (callback)(cx, self.scroll_x, self.scroll_y);
247 }
248
249 self.reset();
250 }
251
252 self.container_width = bounds.width();
253 self.container_height = bounds.height();
254 }
255 }
256
257 WindowEvent::MouseScroll(x, y) => {
258 cx.set_active(true);
259 let (x, y) = if cx.modifiers.shift() { (-*y, -*x) } else { (-*x, -*y) };
260
261 if x != 0.0 && self.inner_width > self.container_width {
263 let negative_space = self.inner_width - self.container_width;
264 if negative_space != 0.0 {
265 let logical_delta = x * SCROLL_SENSITIVITY / negative_space;
266 cx.emit(ScrollEvent::ScrollX(logical_delta));
267 }
268 meta.consume();
270 }
271 if y != 0.0 && self.inner_height > self.container_height {
272 let negative_space = self.inner_height - self.container_height;
273 if negative_space != 0.0 {
274 let logical_delta = y * SCROLL_SENSITIVITY / negative_space;
275 cx.emit(ScrollEvent::ScrollY(logical_delta));
276 }
277 meta.consume();
279 }
280 }
281
282 WindowEvent::MouseOut => {
283 cx.set_active(false);
284 }
285
286 _ => {}
287 });
288 }
289}
290
291impl Handle<'_, ScrollView> {
292 pub fn on_scroll(
294 self,
295 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
296 ) -> Self {
297 self.modify(|scrollview: &mut ScrollView| scrollview.on_scroll = Some(Arc::new(callback)))
298 }
299
300 pub fn scroll_to_cursor(self, scroll_to_cursor: bool) -> Self {
302 self.modify(|scrollview: &mut ScrollView| scrollview.scroll_to_cursor = scroll_to_cursor)
303 }
304
305 pub fn scroll_x(self, scrollx: impl Res<f32>) -> Self {
307 self.bind(scrollx, |handle, scrollx| {
308 let sx = scrollx.get(&handle);
309 handle.modify(|scrollview| scrollview.scroll_x = sx);
310 })
311 }
312
313 pub fn scroll_y(self, scrollx: impl Res<f32>) -> Self {
315 self.bind(scrollx, |handle, scrolly| {
316 let sy = scrolly.get(&handle);
317 handle.modify(|scrollview| scrollview.scroll_y = sy);
318 })
319 }
320
321 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool>) -> Self {
323 self.bind(flag, |handle, show_scrollbar| {
324 let s = show_scrollbar.get(&handle);
325 handle.modify(|scrollview| scrollview.show_horizontal_scrollbar = s);
326 })
327 }
328
329 pub fn show_vertical_scrollbar(self, flag: impl Res<bool>) -> Self {
331 self.bind(flag, |handle, show_scrollbar| {
332 let s = show_scrollbar.get(&handle);
333 handle.modify(|scrollview| scrollview.show_vertical_scrollbar = s);
334 })
335 }
336}
337
338struct ScrollContent {}
339
340impl ScrollContent {
341 pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
342 Self {}.build(cx, content)
343 }
344}
345
346impl View for ScrollContent {
347 fn element(&self) -> Option<&'static str> {
348 Some("scroll-content")
349 }
350
351 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
352 event.map(|window_event, _| match window_event {
353 WindowEvent::GeometryChanged(geo) => {
354 if geo.contains(GeoChanged::WIDTH_CHANGED)
355 || geo.contains(GeoChanged::HEIGHT_CHANGED)
356 {
357 let bounds = cx.bounds();
358 cx.emit(ScrollEvent::ChildGeo(bounds.w, bounds.h));
360 }
361 }
362
363 _ => {}
364 });
365 }
366}