Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/scrollable.rs
StratoSDK / crates / strato-ui-core / src / elements / scrollable.rs
1use super::{
2 AfterLayoutContext, AppContext, Axis, Element, Event, EventContext, Fill, LayoutContext,
3 PaintContext, Point, SizeConstraint, Vector2FExt, ZIndex,
4};
5use crate::elements::F32Ext;
6use crate::event::ModifiersState;
7pub use crate::scene::CornerRadius;
8use crate::units::{IntoPixels, Pixels};
9use crate::ClipBounds;
10use crate::{event::DispatchedEvent, scene::Radius};
11 
12use pathfinder_color::ColorU;
13use pathfinder_geometry::{
14 rect::RectF,
15 vector::{vec2f, Vector2F},
16};
17use std::mem;
18use std::sync::{Arc, Mutex, MutexGuard};
19 
20pub const LEFT_PADDING: f32 = 2.;
21const RIGHT_PADDING: f32 = 2.;
22const MINIMUM_HEIGHT: f32 = 20.;
23 
24/// The number of pixels-per-line when dealing with a cocoa scroll event
25/// that lacks precision (i.e. [`hasPreciseScrollingDeltas`](https://developer.apple.com/documentation/appkit/nsevent/1525758-hasprecisescrollingdeltas?language=objc))
26/// is false. While some mouse devices provide finer scroll deltas
27/// (in pixels), other generic devices don't and we thus have to convert the
28/// provided non-precise scroll deltas (which are in terms of lines) into pixels.
29///
30/// While we could use the application line-height to calculate the number of pixels,
31/// this requires us to couple the scrolling APIs with `Lines`, which doesn't apply
32/// for horizontal scrolling.
33///
34/// We also decided to not use [`CGEventSourceGetPixelsPerLine`](https://developer.apple.com/documentation/coregraphics/1408775-cgeventsourcegetpixelsperline)
35/// because it defaults to ~10 pixels per line, which makes scrolling feel slow compared to other applications.
36///
37/// The value we chose is inspired by the value that Chromium and Flutter use:
38/// - https://chromium.googlesource.com/chromium/src/+/9306606fbbd1ebf51cfe23ea6bcfa19a1ff43363/ui/events/cocoa/events_mac.mm#158
39/// - https://github.com/flutter/engine/blob/cc925b0021330759e18960e1ccbd7e55dec3c375/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm#L768-L775.
40///
41/// TODO: currently, this constant reflects the value that makes sense for MacOS (cocoa) scroll events.
42/// Ideally, we should hide this implementation detail at the platform level and have consumers
43/// solely operate with pixel-based scroll events.
44const NUM_PIXELS_PER_LINE: Pixels = Pixels::new(40.);
45 
46#[derive(Clone, Default)]
47pub struct ScrollState {
48 pub started: Option<f32>,
49 pub hovered: bool,
50 pub child_hovered: bool,
51}
52 
53pub type ScrollStateHandle = Arc<Mutex<ScrollState>>;
54 
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct ScrollData {
57 /// The number of pixels that the child element has been scrolled from its start.
58 /// For a vertically scrollable element, this is equivalent to
59 /// [`scrollTop`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop).
60 /// For a horizontally scrollable element, this is equivalent to
61 /// [`scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft).
62 pub scroll_start: Pixels,
63 
64 /// The number of pixels of the child element that are visible in the currently scrolled region.
65 pub visible_px: Pixels,
66 
67 /// The size of the scrollable element's content.
68 /// This is not necessarily the child element's size (e.g. if the child is viewported).
69 /// For a vertically scrollable element, this is equivalent to
70 /// [`scrollHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight).
71 /// For a horizontally scrollable element, this is equivalent to
72 /// [`scrollWidth`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth).
73 pub total_size: Pixels,
74}
75 
76pub trait ScrollableElement: Element {
77 /// Returns scrolling data that the child computes and that the [`Scrollable`]
78 /// uses to update its internal state. If the child is scrollable
79 /// (i.e. the child has been laid out), this must be [`Some`].
80 fn scroll_data(&self, app: &AppContext) -> Option<ScrollData>;
81 
82 /// Scrolls the element by the given `delta` (in pixels).
83 fn scroll(&mut self, delta: Pixels, ctx: &mut EventContext);
84 
85 /// By default, scrollable elements are responsible for their own wheel handling.
86 /// Override to return true if you want the parent scrollable to handle the wheel.
87 fn should_handle_scroll_wheel(&self) -> bool {
88 false
89 }
90 
91 fn finish_scrollable(self) -> Box<dyn ScrollableElement>
92 where
93 Self: 'static + Sized,
94 {
95 Box::new(self)
96 }
97}
98 
99/// An enum inspired by scrollbar-width css property.
100/// It includes 2 basic sizes.
101///
102/// See [mdn](https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width).
103///
104/// # Examples
105/// ```
106/// use strato_ui_core::elements::ScrollbarWidth;
107///
108/// // Default width of 8.
109/// let y = ScrollbarWidth::Auto;
110///
111/// // Width of 0. to make the scrollbar invisible
112/// let z = ScrollbarWidth::None;
113/// ```
114#[derive(Default, Clone, Copy, Debug)]
115pub enum ScrollbarWidth {
116 #[default]
117 Auto,
118 None,
119 Custom(f32),
120}
121 
122impl ScrollbarWidth {
123 pub const fn as_f32(&self) -> f32 {
124 match *self {
125 ScrollbarWidth::Auto => 8.,
126 ScrollbarWidth::None => 0.,
127 ScrollbarWidth::Custom(width) => width,
128 }
129 }
130}
131 
132/// A generic element to handle scrolling of an underlying element.
133/// Delegates to the underlying child element to update child-specific
134/// scrolling parameters.
135///
136/// Supports both vertical and horizontal scrolling via the [`Scrollable::vertical`]
137/// and [`Scrollable::horizontal`] APIs, respectively.
138pub struct Scrollable {
139 axis: Axis,
140 child: Box<dyn ScrollableElement>,
141 state: ScrollStateHandle,
142 origin: Option<Point>,
143 
144 /// The size of the [`Scrollable`], as determined during layout.
145 scrollable_size: Option<Vector2F>,
146 
147 /// The color of the scrollbar thumb when not hovered/active.
148 nonactive_scrollbar_thumb_background: Fill,
149 /// The color of the scrollbar thumb when hovered/active.
150 active_scrollbar_thumb_background: Fill,
151 /// The color of the scrollbar track.
152 scrollbar_track_background: Fill,
153 
154 /// The size of the scrollbar in pixels.
155 scrollbar_size: ScrollbarWidth,
156 /// The bounds of the whole scrollbar gutter.
157 scrollbar_track_bounds: Option<RectF>,
158 /// The relative position of the thumb within the scrollbar.
159 scrollbar_position_percentage: Option<f32>,
160 /// The relative height of the thumb compared to the whole scrollbar.
161 scrollbar_size_percentage: Option<f32>,
162 /// The bounds for the scrollbar thumb.
163 scrollbar_thumb_bounds: Option<RectF>,
164 /// The origin for the scrollbar thumb.
165 scrollbar_thumb_origin: Option<Vector2F>,
166 /// Padding between child element and the scrollbar.
167 padding_between_child_and_scrollbar: f32,
168 /// Padding after the scrollbar.
169 padding_after_scrollbar: f32,
170 
171 // This is a short-term solution for properly handling events on stacks. A stack will always
172 // put its children on higher z-indexes than its origin, so a hit test using the standard
173 // `z_index` method would always result in the event being covered (by the children of the
174 // stack). Instead, we track the upper-bound of z-indexes _contained by_ the child element.
175 // Then we use that upper bound to do the hit testing, which means a parent will always get
176 // events from its children, regardless of whether they are stacks or not.
177 child_max_z_index: Option<ZIndex>,
178 
179 // The scrollbar is the runway for the draggable scrollbar. By default the scollbox renders to
180 // the side of the child element. This setting makes the scrollbar render over the child instead.
181 overlayed_scrollbar: bool,
182}
183 
184impl Scrollable {
185 #[allow(clippy::too_many_arguments)]
186 fn new(
187 axis: Axis,
188 state: ScrollStateHandle,
189 child: Box<dyn ScrollableElement>,
190 scrollbar_size: ScrollbarWidth,
191 nonactive_scrollbar_thumb_background: Fill,
192 active_scrollbar_thumb_background: Fill,
193 scrollbar_track_background: Fill,
194 ) -> Self {
195 Self {
196 axis,
197 child,
198 scrollbar_size,
199 nonactive_scrollbar_thumb_background,
200 active_scrollbar_thumb_background,
201 scrollbar_track_background,
202 state,
203 origin: None,
204 scrollable_size: None,
205 scrollbar_track_bounds: None,
206 scrollbar_position_percentage: None,
207 scrollbar_size_percentage: None,
208 scrollbar_thumb_bounds: None,
209 scrollbar_thumb_origin: None,
210 padding_between_child_and_scrollbar: LEFT_PADDING,
211 padding_after_scrollbar: RIGHT_PADDING,
212 child_max_z_index: None,
213 overlayed_scrollbar: false,
214 }
215 }
216 
217 /// Creates a vertically scrollable element.
218 #[allow(clippy::too_many_arguments)]
219 pub fn vertical(
220 state: ScrollStateHandle,
221 child: Box<dyn ScrollableElement>,
222 scrollbar_size: ScrollbarWidth,
223 nonactive_scrollbar_thumb_background: Fill,
224 active_scrollbar_thumb_background: Fill,
225 scrollbar_track_background: Fill,
226 ) -> Self {
227 Self::new(
228 Axis::Vertical,
229 state,
230 child,
231 scrollbar_size,
232 nonactive_scrollbar_thumb_background,
233 active_scrollbar_thumb_background,
234 scrollbar_track_background,
235 )
236 }
237 
238 /// Creates a horizontally scrollable element.
239 #[allow(clippy::too_many_arguments)]
240 pub fn horizontal(
241 state: ScrollStateHandle,
242 child: Box<dyn ScrollableElement>,
243 scrollbar_size: ScrollbarWidth,
244 nonactive_scrollbar_thumb_background: Fill,
245 active_scrollbar_thumb_background: Fill,
246 scrollbar_track_background: Fill,
247 ) -> Self {
248 Self::new(
249 Axis::Horizontal,
250 state,
251 child,
252 scrollbar_size,
253 nonactive_scrollbar_thumb_background,
254 active_scrollbar_thumb_background,
255 scrollbar_track_background,
256 )
257 }
258 
259 /// Sets the padding between the child element and the scrollbar.
260 pub fn with_padding_start(mut self, padding_start: f32) -> Self {
261 self.padding_between_child_and_scrollbar = padding_start;
262 self
263 }
264 
265 /// Sets the padding after the scrollbar.
266 pub fn with_padding_end(mut self, padding_end: f32) -> Self {
267 self.padding_after_scrollbar = padding_end;
268 self
269 }
270 
271 pub fn with_overlayed_scrollbar(mut self) -> Self {
272 self.overlayed_scrollbar = true;
273 self
274 }
275 
276 fn state(&mut self) -> MutexGuard<'_, ScrollState> {
277 self.state.lock().unwrap()
278 }
279 
280 fn mouse_dragged(&mut self, position: Vector2F, ctx: &mut EventContext, app: &AppContext) {
281 let previous_dragging_position = self.state().started;
282 if let Some(previous_dragging_position) = previous_dragging_position {
283 let position_along_axis = position.along(self.axis);
284 self.start_scrolling(position);
285 self.jump_to_position(
286 previous_dragging_position.into_pixels(),
287 position_along_axis.into_pixels(),
288 ctx,
289 app,
290 );
291 }
292 }
293 
294 fn jump_to_position(
295 &mut self,
296 previous_position_along_axis: Pixels,
297 new_position_along_axis: Pixels,
298 ctx: &mut EventContext,
299 app: &AppContext,
300 ) {
301 let total_size = self.total_size(app);
302 let scroll_start = self.scroll_start(app);
303 let scroll_remaining = self.scroll_remaining(app);
304 
305 // We need to use the original scrollbar size before resizing to calculate the scroll speed.
306 let scrollbar_size_percentage_before_resize =
307 (total_size - scroll_start - scroll_remaining) / total_size;
308 
309 // We don't want to update the scroll position if you're scrolled to the top and the cursor is above
310 // the element or if you're scrolled to the bottom and the cursor is below the element.
311 // TODO(kevin): Do we need the scroll_start <= 0 check?
312 if (scroll_remaining <= Pixels::zero()
313 && new_position_along_axis > previous_position_along_axis)
314 || (scroll_start <= Pixels::zero()
315 && previous_position_along_axis > new_position_along_axis)
316 {
317 return;
318 }
319 
320 let delta = previous_position_along_axis - new_position_along_axis;
321 
322 // The scroll speed should be proportional to the total number of lines.
323 // Assume we have moved the scrollbar by a distance x, the number of lines scrolled
324 // should be calculated by x / total_height * total_number_of_lines.
325 self.child
326 .scroll(delta / scrollbar_size_percentage_before_resize, ctx);
327 }
328 
329 fn mousewheel(&mut self, delta: Vector2F, precise: bool, ctx: &mut EventContext) {
330 if self
331 .scrollbar_size_percentage
332 .expect("should be set at event dispatching time")
333 < 1.
334 {
335 let delta_along_axis = delta.along(self.axis);
336 if precise {
337 self.child.scroll(delta_along_axis.into_pixels(), ctx);
338 } else {
339 // If the scroll was not `precise`, we need to convert the delta (which is
340 // actually in terms of `Lines`) to the right number of `Pixels`.
341 // See the comment on [`SCROLLBAR_PIXELS_PER_COCOA_TICK`] for more details.
342 self.child.scroll(
343 (delta_along_axis * NUM_PIXELS_PER_LINE.as_f32()).into_pixels(),
344 ctx,
345 );
346 }
347 }
348 }
349 
350 /// Returns the child's [`ScrollData`], assuming the child has been laid out.
351 fn scroll_data(&self, app: &AppContext) -> ScrollData {
352 self.child
353 .scroll_data(app)
354 .expect("ScrollData should be some to be scrollable")
355 }
356 
357 fn scroll_start(&self, app: &AppContext) -> Pixels {
358 self.scroll_data(app).scroll_start
359 }
360 
361 /// The number of pixels that the child is still scrollable (biased towards its end).
362 /// For example, for a vertically scrollable element, this would be the number of pixels
363 /// that the child can still be scrolled down.
364 fn scroll_remaining(&self, app: &AppContext) -> Pixels {
365 let scroll_data = self.scroll_data(app);
366 scroll_data.total_size - scroll_data.scroll_start - scroll_data.visible_px
367 }
368 
369 fn total_size(&self, app: &AppContext) -> Pixels {
370 self.scroll_data(app).total_size
371 }
372 
373 fn start_scrolling(&mut self, position: Vector2F) {
374 self.state().started = Some(position.along(self.axis));
375 }
376 
377 fn end_scrolling(&mut self) {
378 self.state().started = None
379 }
380 
381 /// Returns the `original_size` that has its inverted axis dimension changed to `dimension_along_inverted_axis`.
382 fn size_along_inverted_axis(
383 &self,
384 original_size: Vector2F,
385 dimension_along_inverted_axis: f32,
386 ) -> Vector2F {
387 match self.axis {
388 Axis::Horizontal => vec2f(original_size.x(), dimension_along_inverted_axis),
389 Axis::Vertical => vec2f(dimension_along_inverted_axis, original_size.y()),
390 }
391 }
392}
393 
394impl Element for Scrollable {
395 fn layout(
396 &mut self,
397 constraint: SizeConstraint,
398 ctx: &mut LayoutContext,
399 app: &AppContext,
400 ) -> Vector2F {
401 let scrollbar_size = self.scrollbar_size.as_f32().along(self.axis.invert());
402 let padding = (self.padding_between_child_and_scrollbar + self.padding_after_scrollbar)
403 .along(self.axis.invert());
404 
405 let child_constraint = if self.overlayed_scrollbar {
406 // If the scrollbar is overlayed, the child can span the entire constraint.
407 SizeConstraint {
408 min: constraint.min.max(Vector2F::zero()),
409 max: constraint.max.max(Vector2F::zero()),
410 }
411 } else {
412 // If the scrollbar is not overlayed, we must save room for the scrollbar.
413 SizeConstraint {
414 min: (constraint.min - scrollbar_size - padding).max(Vector2F::zero()),
415 max: (constraint.max - scrollbar_size - padding).max(Vector2F::zero()),
416 }
417 };
418 
419 let child_size = self.child.layout(child_constraint, ctx, app);
420 debug_assert!(
421 child_size.y().is_finite(),
422 "Scrollable's child should not have infinite height"
423 );
424 debug_assert!(
425 child_size.x().is_finite(),
426 "Scrollable's child should not have infinite width"
427 );
428 
429 // If the scrollbar is not overlayed, we add back its size to get the overall size
430 // of the scrollable element.
431 let size = if self.overlayed_scrollbar {
432 child_size
433 } else {
434 child_size + scrollbar_size + padding
435 };
436 
437 self.scrollable_size = Some(size);
438 size
439 }
440 
441 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
442 self.child.after_layout(ctx, app);
443 
444 let scroll_data = self.scroll_data(app);
445 let total_size = scroll_data.total_size;
446 
447 let minimum_size_percentage =
448 (MINIMUM_HEIGHT / self.scrollable_size.unwrap().along(self.axis)).min(1.);
449 let size_percentage =
450 (scroll_data.visible_px / total_size).max(minimum_size_percentage.into_pixels());
451 
452 self.scrollbar_size_percentage = Some(size_percentage.as_f32());
453 
454 // The scrollbar position is calculated with the ratio between scroll top and scroll bottom.
455 let scroll_start = self.scroll_start(app);
456 let scroll_remaining = self.scroll_remaining(app);
457 
458 let scrollbar_position_percentage = scroll_start / (scroll_start + scroll_remaining);
459 self.scrollbar_position_percentage = Some(scrollbar_position_percentage.as_f32());
460 }
461 
462 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
463 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
464 self.child.paint(origin, ctx, app);
465 let scrollable_size = self
466 .scrollable_size
467 .expect("size should have been set during layout");
468 
469 // The origin of the scrollbar track is the maximum coordinate (along the inverted axis)
470 // subtracted by the size of the scrollbar. For example, for a vertically scrollable element,
471 // the origin will be the maximum x coordinate subtracted by the size of the scrollbar.
472 let scrollbar_track_length = self.scrollbar_size.as_f32()
473 + self.padding_between_child_and_scrollbar
474 + self.padding_after_scrollbar;
475 let scrollbar_track_origin = origin + scrollable_size.project_onto(self.axis.invert())
476 - scrollbar_track_length.along(self.axis.invert());
477 let scrollbar_track_size =
478 self.size_along_inverted_axis(scrollable_size, scrollbar_track_length);
479 
480 let scrollbar_track_bounds = RectF::new(scrollbar_track_origin, scrollbar_track_size);
481 self.scrollbar_track_bounds = Some(scrollbar_track_bounds);
482 
483 // If the scrollbar is overlayed over the child, it should be at a higher z-index.
484 if self.overlayed_scrollbar {
485 ctx.scene
486 .start_layer(ClipBounds::BoundedBy(scrollbar_track_bounds));
487 }
488 let scrollbar = ctx
489 .scene
490 .draw_rect_with_hit_recording(scrollbar_track_bounds);
491 
492 // If the scrollbar is overlayed, make it transparent. If neither the scrollbar nor the child
493 // is hovered, make it have no fill.
494 if !self.state().hovered && !self.state().child_hovered {
495 scrollbar.with_background(Fill::None);
496 } else if self.overlayed_scrollbar {
497 scrollbar.with_background(Fill::Solid(ColorU::transparent_black()));
498 } else {
499 scrollbar.with_background(self.scrollbar_track_background);
500 }
501 
502 let scrollbar_size_percentage = self.scrollbar_size_percentage.unwrap();
503 let scrollbar_position_percentage = self.scrollbar_position_percentage.unwrap();
504 if scrollbar_size_percentage < 1. {
505 let scrollbar_thumb_size = self.size_along_inverted_axis(
506 scrollable_size * scrollbar_size_percentage,
507 self.scrollbar_size.as_f32(),
508 );
509 let scrollbar_thumb_origin = scrollbar_track_origin
510 + self.size_along_inverted_axis(
511 (scrollable_size - scrollbar_thumb_size) * scrollbar_position_percentage,
512 self.padding_between_child_and_scrollbar,
513 );
514 
515 self.scrollbar_thumb_bounds =
516 Some(RectF::new(scrollbar_thumb_origin, scrollbar_thumb_size));
517 self.scrollbar_thumb_origin = Some(scrollbar_thumb_origin);
518 
519 let hovered = self.state().hovered;
520 let child_hovered = self.state().child_hovered;
521 let background = if hovered {
522 self.active_scrollbar_thumb_background
523 } else if child_hovered {
524 self.nonactive_scrollbar_thumb_background
525 } else {
526 Fill::None
527 };
528 
529 ctx.scene
530 .draw_rect_with_hit_recording(RectF::new(
531 scrollbar_thumb_origin,
532 scrollbar_thumb_size,
533 ))
534 .with_background(background)
535 .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)));
536 } else {
537 self.scrollbar_thumb_origin = Some(origin);
538 self.scrollbar_thumb_bounds = Some(RectF::new(vec2f(0., 0.), vec2f(0., 0.)));
539 }
540 
541 // See comment above about the layering of the scrollbar and scrollbar.
542 if self.overlayed_scrollbar {
543 ctx.scene.stop_layer();
544 }
545 
546 self.child_max_z_index = Some(ctx.scene.max_active_z_index());
547 }
548 
549 fn dispatch_event(
550 &mut self,
551 event: &DispatchedEvent,
552 ctx: &mut EventContext,
553 app: &AppContext,
554 ) -> bool {
555 let handled = self.child.dispatch_event(event, ctx, app);
556 let z_index = *self.child_max_z_index.as_ref().unwrap();
557 
558 match event.raw_event() {
559 Event::LeftMouseDragged { position, .. } => {
560 let is_dragging = self.state().started.is_some();
561 if !is_dragging {
562 return handled;
563 }
564 self.mouse_dragged(*position, ctx, app);
565 true
566 }
567 Event::LeftMouseDown { position, .. } => {
568 if ctx.is_covered(Point::from_vec2f(*position, z_index)) {
569 return handled;
570 }
571 
572 let Some(thumb_bounds) = self.scrollbar_thumb_bounds else {
573 log::warn!(
574 "Expected scrollbar thumb bounds to exist in dispatch_event, but got None"
575 );
576 return handled;
577 };
578 
579 if thumb_bounds.contains_point(*position) {
580 self.start_scrolling(*position);
581 
582 // Dispatch an action in tests so we can perform assertions
583 // on clicks.
584 #[cfg(test)]
585 ctx.dispatch_action("scrollable_click::on_thumb", ());
586 
587 true
588 } else if self
589 .scrollbar_track_bounds
590 .is_some_and(|bounds| bounds.contains_point(*position))
591 {
592 // If mouse down happens in the x range of scrollbar but not on the thumb,
593 // we should scroll to the mouse down position.
594 let previous_position = thumb_bounds.center().along(self.axis);
595 self.jump_to_position(
596 previous_position.into_pixels(),
597 position.along(self.axis).into_pixels(),
598 ctx,
599 app,
600 );
601 
602 // Dispatch an action in tests so we can perform assertions
603 // on clicks.
604 #[cfg(test)]
605 ctx.dispatch_action("scrollable_click::on_gutter", ());
606 
607 true
608 } else {
609 handled
610 }
611 }
612 Event::LeftMouseUp { .. } => {
613 let previous_dragging_position = self.state().started;
614 if previous_dragging_position.is_some() {
615 self.end_scrolling();
616 true
617 } else {
618 handled
619 }
620 }
621 Event::MouseMoved { position, .. } => {
622 let is_dragging = self.state().started.is_some();
623 
624 if is_dragging {
625 return handled;
626 }
627 let is_covered = ctx.is_covered(Point::from_vec2f(*position, z_index));
628 
629 let mouse_in = self
630 .scrollbar_thumb_bounds
631 .unwrap()
632 .contains_point(*position)
633 && !is_covered;
634 let was_hovered = mem::replace(&mut self.state().hovered, mouse_in);
635 
636 let mouse_in_child = self
637 .child
638 .bounds()
639 .unwrap_or_default()
640 .contains_point(*position)
641 && !is_covered;
642 let child_was_hovered =
643 mem::replace(&mut self.state().child_hovered, mouse_in_child);
644 
645 if was_hovered != mouse_in || child_was_hovered != mouse_in_child {
646 ctx.notify();
647 }
648 
649 if mouse_in {
650 true
651 } else {
652 handled
653 }
654 }
655 Event::ScrollWheel {
656 position,
657 delta,
658 precise,
659 modifiers: ModifiersState { ctrl: false, .. },
660 } => {
661 if !self.child.should_handle_scroll_wheel() {
662 return handled;
663 }
664 
665 if self.bounds().unwrap().contains_point(*position)
666 && !ctx.is_covered(Point::from_vec2f(*position, z_index))
667 {
668 self.mousewheel(*delta, *precise, ctx);
669 return true;
670 }
671 handled
672 }
673 _ => handled,
674 }
675 }
676 
677 fn size(&self) -> Option<Vector2F> {
678 self.scrollable_size
679 }
680 
681 fn origin(&self) -> Option<Point> {
682 self.origin
683 }
684}
685 
686#[cfg(test)]
687#[path = "scrollable_test.rs"]
688mod tests;
689