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/new_scrollable/mod.rs
1#![allow(dead_code)]
2 
3mod dual_axis_config;
4mod single_axis_config;
5pub(crate) mod util;
6 
7pub use dual_axis_config::*;
8pub use single_axis_config::*;
9 
10use pathfinder_color::ColorU;
11use pathfinder_geometry::{
12 rect::RectF,
13 vector::{vec2f, Vector2F},
14};
15 
16use crate::{
17 elements::Vector2FExt,
18 event::{DispatchedEvent, ModifiersState},
19 text::{word_boundaries::WordBoundariesPolicy, IsRect, SelectionDirection, SelectionType},
20 units::{IntoPixels, Pixels},
21 AfterLayoutContext, AppContext, ClipBounds, Element, Event, EventContext, LayoutContext,
22 PaintContext, SizeConstraint,
23};
24 
25use self::util::adjust_scroll_delta_with_sensitivity_config;
26 
27use super::{
28 scrollbar_size, Axis, ClippedScrollStateHandle, CornerRadius, F32Ext, Fill, Point, Radius,
29 ScrollData, ScrollbarWidth, SelectableElement, SelectionFragment, ZIndex,
30};
31 
32const LEFT_PADDING: f32 = 2.;
33const RIGHT_PADDING: f32 = 2.;
34const MINIMUM_HEIGHT: f32 = 20.;
35 
36// TODO: we might want this to be configurable.
37const DUAL_AXES_SCROLL_SENSITIVITY: f32 = 1.0;
38 
39/// The number of pixels-per-line when dealing with a cocoa scroll event
40/// that lacks precision (i.e. [`hasPreciseScrollingDeltas`](https://developer.apple.com/documentation/appkit/nsevent/1525758-hasprecisescrollingdeltas?language=objc))
41/// is false. While some mouse devices provide finer scroll deltas
42/// (in pixels), other generic devices don't and we thus have to convert the
43/// provided non-precise scroll deltas (which are in terms of lines) into pixels.
44///
45/// While we could use the application line-height to calculate the number of pixels,
46/// this requires us to couple the scrolling APIs with `Lines`, which doesn't apply
47/// for horizontal scrolling.
48///
49/// We also decided to not use [`CGEventSourceGetPixelsPerLine`](https://developer.apple.com/documentation/coregraphics/1408775-cgeventsourcegetpixelsperline)
50/// because it defaults to ~10 pixels per line, which makes scrolling feel slow compared to other applications.
51///
52/// The value we chose is inspired by the value that Chromium and Flutter use:
53/// - https://chromium.googlesource.com/chromium/src/+/9306606fbbd1ebf51cfe23ea6bcfa19a1ff43363/ui/events/cocoa/events_mac.mm#158
54/// - https://github.com/flutter/engine/blob/cc925b0021330759e18960e1ccbd7e55dec3c375/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm#L768-L775.
55///
56/// TODO: currently, this constant reflects the value that makes sense for MacOS (cocoa) scroll events.
57/// Ideally, we should hide this implementation detail at the platform level and have consumers
58/// solely operate with pixel-based scroll events.
59const NUM_PIXELS_PER_LINE: f32 = 40.;
60 
61/// Trait a scrollable child element needs to implement to enable manual scrolling.
62/// The element could support scrolling on: horizontal axis, vertical axis, or both axes.
63pub trait NewScrollableElement: Element {
64 /// What axis the child is scrollable on. It's the implementer's responsibility to
65 /// make sure this accurately reflects the element's scrolling behavior. Scrollable uses this
66 /// information to validate if the caller's configuration is valid.
67 fn axis(&self) -> ScrollableAxis;
68 
69 /// Returns scrolling data that the child computes and that the [`Scrollable`]
70 /// uses to update its internal state. If the child is scrollable
71 /// (i.e. the child has been laid out), this must be [`Some`].
72 fn scroll_data(&self, axis: Axis, app: &AppContext) -> Option<ScrollData>;
73 
74 /// Scrolls the element by the given `delta` (in pixels).
75 fn scroll(&mut self, delta: Pixels, axis: Axis, ctx: &mut EventContext);
76 
77 /// By default, scrollable elements are responsible for their own wheel handling.
78 /// Override to return true if you want the parent scrollable to handle the wheel.
79 fn axis_should_handle_scroll_wheel(&self, _axis: Axis) -> bool {
80 false
81 }
82 
83 fn finish_scrollable(self) -> Box<dyn NewScrollableElement>
84 where
85 Self: 'static + Sized,
86 {
87 Box::new(self)
88 }
89}
90 
91/// Which axis the child element is scrollable on.
92#[derive(Debug)]
93pub enum ScrollableAxis {
94 Horizontal,
95 Vertical,
96 Both,
97}
98 
99/// The appearance configuration of each scrollbar. Scrollable supports different appearance settings
100/// on each axis when the element is scrollable on both axes.
101#[derive(Default, Clone, Copy, Debug)]
102pub struct ScrollableAppearance {
103 /// The size of the scrollbar in pixels.
104 scrollbar_size: ScrollbarWidth,
105 // The scrollbar is the runway for the draggable scrollbar. By default the scollbox renders to
106 // the side of the child element. This setting makes the scrollbar render over the child instead.
107 overlaid_scrollbar: bool,
108}
109 
110impl ScrollableAppearance {
111 pub fn new(scrollbar_size: ScrollbarWidth, overlaid_scrollbar: bool) -> Self {
112 Self {
113 scrollbar_size,
114 overlaid_scrollbar,
115 }
116 }
117 
118 fn scrollbar_size(&self, include_overlaid_scrollbar: bool) -> f32 {
119 if !include_overlaid_scrollbar && self.overlaid_scrollbar {
120 0.
121 } else {
122 self.scrollbar_size.as_f32()
123 }
124 }
125}
126 
127/// Internal state of the scrollable configuration.
128enum ScrollableState {
129 SingleAxis {
130 axis: Axis,
131 config: SingleAxisConfig,
132 appearance: ScrollableAppearance,
133 render_state: ScrollbarRenderState,
134 },
135 BothAxes {
136 config: DualAxisConfig,
137 horizontal_appearance: ScrollableAppearance,
138 vertical_appearance: ScrollableAppearance,
139 horizontal_state: ScrollbarRenderState,
140 vertical_state: ScrollbarRenderState,
141 },
142}
143 
144impl ScrollableState {
145 /// Returns the size the scrollable's scrollbar(s) would take on each axis.
146 fn scrollbar_size(&self, include_overlaid_scrollbar: bool) -> Vector2F {
147 match self {
148 Self::SingleAxis {
149 axis, appearance, ..
150 } => appearance
151 .scrollbar_size(include_overlaid_scrollbar)
152 .along(axis.invert()),
153 Self::BothAxes {
154 horizontal_appearance,
155 vertical_appearance,
156 ..
157 } => vec2f(
158 vertical_appearance.scrollbar_size(include_overlaid_scrollbar),
159 horizontal_appearance.scrollbar_size(include_overlaid_scrollbar),
160 ),
161 }
162 }
163 
164 /// Returns the total padding the scrollable's scrollbar(s) would take on each axis.
165 fn scrollbar_padding(&self, include_overlaid_scrollbar: bool) -> Vector2F {
166 match self {
167 Self::SingleAxis {
168 axis,
169 render_state,
170 appearance,
171 ..
172 } => render_state
173 .scrollbar_padding(*appearance, include_overlaid_scrollbar)
174 .along(axis.invert()),
175 Self::BothAxes {
176 horizontal_state,
177 vertical_state,
178 vertical_appearance,
179 horizontal_appearance,
180 ..
181 } => vec2f(
182 vertical_state.scrollbar_padding(*vertical_appearance, include_overlaid_scrollbar),
183 horizontal_state
184 .scrollbar_padding(*horizontal_appearance, include_overlaid_scrollbar),
185 ),
186 }
187 }
188 
189 /// Layout the child element with the incoming size constraint.
190 fn layout_child(
191 &mut self,
192 constraint: SizeConstraint,
193 ctx: &mut LayoutContext,
194 app: &AppContext,
195 ) -> Vector2F {
196 let scrollbar_size_with_padding =
197 self.scrollbar_size(false) + self.scrollbar_padding(false);
198 match self {
199 Self::SingleAxis { axis, config, .. } => {
200 config.layout_child(*axis, constraint, scrollbar_size_with_padding, ctx, app)
201 }
202 Self::BothAxes { config, .. } => {
203 config.layout_child(constraint, scrollbar_size_with_padding, ctx, app)
204 }
205 }
206 }
207 
208 fn after_layout(
209 &mut self,
210 scrollable_size: Vector2F,
211 ctx: &mut AfterLayoutContext,
212 app: &AppContext,
213 ) {
214 let viewport_size = self.viewport_size(scrollable_size);
215 match self {
216 Self::BothAxes {
217 config,
218 horizontal_state,
219 vertical_state,
220 ..
221 } => {
222 // First invoke after_layout on the child.
223 let (horizontal_data, vertical_data) = config.after_layout(viewport_size, ctx, app);
224 
225 // Update the render state with the latest ScrollData.
226 horizontal_state.update_with_scroll_data(
227 horizontal_data,
228 viewport_size.along(Axis::Horizontal),
229 );
230 vertical_state
231 .update_with_scroll_data(vertical_data, viewport_size.along(Axis::Vertical));
232 }
233 Self::SingleAxis {
234 axis,
235 config,
236 render_state,
237 ..
238 } => {
239 // First invoke after_layout on the child.
240 let scroll_data = config.after_layout(*axis, viewport_size, ctx, app);
241 // Update the render state with the latest ScrollData.
242 render_state.update_with_scroll_data(scroll_data, viewport_size.along(*axis));
243 }
244 }
245 }
246 
247 /// Paint the child element.
248 fn paint_child(
249 &mut self,
250 origin: Vector2F,
251 size: Vector2F,
252 ctx: &mut PaintContext,
253 app: &AppContext,
254 ) {
255 match self {
256 Self::BothAxes { config, .. } => config.paint_child(origin, size, ctx, app),
257 Self::SingleAxis { axis, config, .. } => {
258 config.paint_child(*axis, origin, size, ctx, app)
259 }
260 }
261 }
262 
263 fn dispatch_event_to_child(
264 &mut self,
265 event: &DispatchedEvent,
266 ctx: &mut EventContext,
267 app: &AppContext,
268 ) -> bool {
269 match self {
270 Self::BothAxes { config, .. } => config.dispatch_event_to_child(event, ctx, app),
271 Self::SingleAxis { config, .. } => config.dispatch_event_to_child(event, ctx, app),
272 }
273 }
274 
275 fn child_as_selectable_element(&self) -> Option<&dyn SelectableElement> {
276 match self {
277 Self::BothAxes { config, .. } => config.child_as_selectable_element(),
278 Self::SingleAxis { config, .. } => config.child_as_selectable_element(),
279 }
280 }
281 
282 fn scroll_offset(&self) -> Vector2F {
283 match self {
284 Self::BothAxes { config, .. } => config.scroll_offset(),
285 Self::SingleAxis { axis, config, .. } => config.scroll_offset(*axis),
286 }
287 }
288 
289 /// Paint the scrollbars on both axes.
290 fn draw_scrollbars(
291 &mut self,
292 origin: Vector2F,
293 scrollable_size: Vector2F,
294 nonactive_scrollbar_thumb_background: Fill,
295 active_scrollbar_thumb_background: Fill,
296 scrollbar_track_background: Fill,
297 ctx: &mut PaintContext,
298 ) {
299 // Consider the overlaid scrollbar when calculating sizing. This will be used for drawing the scrollbar.
300 let scrollbar_size_with_padding = self.scrollbar_size(true) + self.scrollbar_padding(true);
301 match self {
302 Self::BothAxes {
303 config,
304 horizontal_appearance,
305 vertical_appearance,
306 vertical_state,
307 horizontal_state,
308 } => {
309 vertical_state.draw_scrollbar(
310 Axis::Vertical,
311 scrollable_size,
312 scrollbar_size_with_padding,
313 origin,
314 if config.hovered(Axis::Vertical) {
315 active_scrollbar_thumb_background
316 } else if !config.child_hovered() {
317 Fill::None
318 } else {
319 nonactive_scrollbar_thumb_background
320 },
321 scrollbar_track_background,
322 *vertical_appearance,
323 ctx,
324 );
325 
326 horizontal_state.draw_scrollbar(
327 Axis::Horizontal,
328 scrollable_size,
329 scrollbar_size_with_padding,
330 origin,
331 if config.hovered(Axis::Horizontal) {
332 active_scrollbar_thumb_background
333 } else if !config.child_hovered() {
334 Fill::None
335 } else {
336 nonactive_scrollbar_thumb_background
337 },
338 scrollbar_track_background,
339 *horizontal_appearance,
340 ctx,
341 );
342 
343 // If both scrollbars are not overlaid. There will be a bottom right area (marked with asterisk)
344 // ============================================
345 // | | |
346 // | | |
347 // | | |
348 // | | |
349 // | | |
350 // | | |
351 // | | |
352 // | | |
353 // | | |
354 // | | |
355 // | | |
356 // ============================================
357 // | |**|
358 // ============================================
359 //
360 // Paint it with the scrollbar track background.
361 let viewport_size =
362 (scrollable_size - scrollbar_size_with_padding).max(Vector2F::zero());
363 if !vertical_appearance.overlaid_scrollbar
364 && !horizontal_appearance.overlaid_scrollbar
365 {
366 let bottom_right_rect =
367 RectF::new(origin + viewport_size, scrollbar_size_with_padding);
368 ctx.scene
369 .draw_rect_with_hit_recording(bottom_right_rect)
370 .with_background(scrollbar_track_background);
371 }
372 }
373 Self::SingleAxis {
374 axis,
375 config,
376 appearance,
377 render_state,
378 } => render_state.draw_scrollbar(
379 *axis,
380 scrollable_size,
381 scrollbar_size_with_padding,
382 origin,
383 if config.hovered() {
384 active_scrollbar_thumb_background
385 } else if !config.child_hovered() {
386 Fill::None
387 } else {
388 nonactive_scrollbar_thumb_background
389 },
390 scrollbar_track_background,
391 *appearance,
392 ctx,
393 ),
394 }
395 }
396 
397 /// Size of the current viewport. This excludes the scrollbar tracks.
398 fn viewport_size(&self, scrollable_size: Vector2F) -> Vector2F {
399 (scrollable_size - self.scrollbar_padding(false) - self.scrollbar_size(false))
400 .max(Vector2F::zero())
401 }
402 
403 /// Handle a mouse down event. If the click position is inbound of the scrollbar thumb,
404 /// start a scroll drag event. If the click position is inbound of the scrollbar track but not the thumb,
405 /// jump to the mouse down position.
406 fn mouse_down(
407 &mut self,
408 position: Vector2F,
409 scrollable_size: Vector2F,
410 ctx: &mut EventContext,
411 app: &AppContext,
412 ) -> bool {
413 match self {
414 Self::BothAxes {
415 horizontal_state,
416 vertical_state,
417 config,
418 ..
419 } => {
420 let drag_start_direction = if horizontal_state
421 .scrollbar_thumb_bounds
422 .expect("Thumb bound should exist")
423 .contains_point(position)
424 {
425 Some(Axis::Horizontal)
426 } else if vertical_state
427 .scrollbar_thumb_bounds
428 .expect("Thumb bound should exist")
429 .contains_point(position)
430 {
431 Some(Axis::Vertical)
432 } else {
433 None
434 };
435 
436 if let Some(axis) = drag_start_direction {
437 config.set_drag_start(position, axis);
438 
439 // Dispatch an action in tests so we can perform assertions
440 // on clicks.
441 #[cfg(test)]
442 ctx.dispatch_action("scrollable_click::on_thumb", ());
443 
444 return true;
445 }
446 
447 let scroll_track_hit = if horizontal_state
448 .scrollbar_track_bounds
449 .expect("Track bound should exist")
450 .contains_point(position)
451 {
452 Some((
453 Axis::Horizontal,
454 horizontal_state
455 .scrollbar_thumb_bounds
456 .expect("Thumb bound should exist"),
457 ))
458 } else if vertical_state
459 .scrollbar_track_bounds
460 .expect("Track bound should exist")
461 .contains_point(position)
462 {
463 Some((
464 Axis::Vertical,
465 vertical_state
466 .scrollbar_thumb_bounds
467 .expect("Thumb bound should exist"),
468 ))
469 } else {
470 None
471 };
472 
473 if let Some((axis, scrollbar_thumb_bounds)) = scroll_track_hit {
474 // If the scrollbar thumb has no area, then the `Scrollable` is not large enough
475 // in this axis for scrolling to be relevant (i.e. no scrollbar thumb will be painted).
476 // In such cases, we should return `false` so that child elements can still
477 // receive the `LeftMouseDown` event (i.e. for editor text selection).
478 if scrollbar_thumb_bounds.is_empty() {
479 return false;
480 }
481 
482 // If mouse down happens in the x range of scrollbar but not on the thumb,
483 // we should scroll to the mouse down position.
484 let previous_position = scrollbar_thumb_bounds.center().along(axis);
485 
486 self.jump_to_position(
487 previous_position.into_pixels(),
488 position.along(axis).into_pixels(),
489 scrollable_size,
490 axis,
491 ctx,
492 app,
493 );
494 
495 // Dispatch an action in tests so we can perform assertions
496 // on clicks.
497 #[cfg(test)]
498 ctx.dispatch_action("scrollable_click::on_gutter", ());
499 
500 return true;
501 }
502 
503 false
504 }
505 Self::SingleAxis {
506 config,
507 axis,
508 render_state,
509 ..
510 } => {
511 let current_axis = *axis;
512 if render_state
513 .scrollbar_thumb_bounds
514 .expect("Thumb bound should exist")
515 .contains_point(position)
516 {
517 config.set_drag_start(position, current_axis);
518 
519 // Dispatch an action in tests so we can perform assertions
520 // on clicks.
521 #[cfg(test)]
522 ctx.dispatch_action("scrollable_click::on_thumb", ());
523 
524 true
525 } else if render_state
526 .scrollbar_track_bounds
527 .expect("Track bound should exist")
528 .contains_point(position)
529 {
530 // If the scrollbar thumb has no area, then the `Scrollable` is not large enough
531 // in this axis for scrolling to be relevant (i.e. no scrollbar thumb will be painted).
532 // In such cases, we should return `false` so that child elements can still
533 // receive the `LeftMouseDown` event (i.e. for editor text selection).
534 if render_state
535 .scrollbar_thumb_bounds
536 .expect("Thumb bound should exist")
537 .is_empty()
538 {
539 return false;
540 }
541 
542 let previous_position = render_state
543 .scrollbar_thumb_bounds
544 .expect("Thumb bound should exist")
545 .center()
546 .along(current_axis);
547 
548 self.jump_to_position(
549 previous_position.into_pixels(),
550 position.along(current_axis).into_pixels(),
551 scrollable_size,
552 current_axis,
553 ctx,
554 app,
555 );
556 
557 // Dispatch an action in tests so we can perform assertions
558 // on clicks.
559 #[cfg(test)]
560 ctx.dispatch_action("scrollable_click::on_gutter", ());
561 
562 true
563 } else {
564 false
565 }
566 }
567 }
568 }
569 
570 fn mouse_dragged(
571 &mut self,
572 position: Vector2F,
573 scrollable_size: Vector2F,
574 ctx: &mut EventContext,
575 app: &AppContext,
576 ) -> bool {
577 let (previous_position_along_axis, axis) = match self {
578 Self::BothAxes { config, .. } => {
579 // If we have not started a drag session, early return.
580 let Some((previous_position, axis)) = config.drag_start() else {
581 return false;
582 };
583 
584 // Update the drag start state of the scroll handle.
585 config.set_drag_start(position, axis);
586 (previous_position.into_pixels(), axis)
587 }
588 Self::SingleAxis { axis, config, .. } => {
589 // If we have not started a drag session, early return.
590 let Some(previous_position) = config.drag_start() else {
591 return false;
592 };
593 
594 // Update the drag start state of the scroll handle.
595 config.set_drag_start(position, *axis);
596 (previous_position.into_pixels(), *axis)
597 }
598 };
599 
600 // Scroll to the new position along the axis of the active drag session.
601 self.jump_to_position(
602 previous_position_along_axis,
603 position.along(axis).into_pixels(),
604 scrollable_size,
605 axis,
606 ctx,
607 app,
608 );
609 
610 true
611 }
612 
613 fn mouse_up(&self) -> bool {
614 match self {
615 Self::BothAxes { config, .. } => {
616 // If we have not started a drag session, early return.
617 let Some((_, axis)) = config.drag_start() else {
618 return false;
619 };
620 
621 config.end_drag(axis);
622 }
623 Self::SingleAxis { config, .. } => {
624 // If we have not started a drag session, early return.
625 if config.drag_start().is_none() {
626 return false;
627 }
628 
629 config.end_drag();
630 }
631 }
632 true
633 }
634 
635 fn mouse_moved(&self, position: Vector2F, is_covered: bool, ctx: &mut EventContext) -> bool {
636 match self {
637 Self::BothAxes {
638 config,
639 horizontal_state,
640 vertical_state,
641 ..
642 } => {
643 // If we are in a drag session, we don't need to update the scrollbar thumb hover state. Early return.
644 if config.drag_start().is_some() {
645 return false;
646 }
647 
648 let was_hovered_horizontal = config.hovered(Axis::Horizontal);
649 let was_hovered_vertical = config.hovered(Axis::Vertical);
650 let was_child_hovered = config.child_hovered();
651 
652 let mouse_in_horizontal_thumb = !is_covered
653 && horizontal_state
654 .scrollbar_thumb_bounds
655 .expect("Bounds should exist")
656 .contains_point(position);
657 let mouse_in_vertical_thumb = !is_covered
658 && vertical_state
659 .scrollbar_thumb_bounds
660 .expect("Bounds should exist")
661 .contains_point(position);
662 
663 let mouse_in_child = !is_covered
664 && config
665 .child_bounds()
666 .expect("Bounds should exist")
667 .contains_point(position);
668 
669 let mut hover_state_changed = false;
670 if mouse_in_horizontal_thumb != was_hovered_horizontal {
671 config.set_hovered(Axis::Horizontal, mouse_in_horizontal_thumb);
672 hover_state_changed = true;
673 }
674 
675 if mouse_in_vertical_thumb != was_hovered_vertical {
676 config.set_hovered(Axis::Vertical, mouse_in_vertical_thumb);
677 hover_state_changed = true;
678 }
679 
680 if mouse_in_child != was_child_hovered {
681 config.set_child_hovered(mouse_in_child);
682 hover_state_changed = true;
683 }
684 
685 // Re-render if either the horizontal / vertical scrollbar state changed.
686 if hover_state_changed {
687 ctx.notify();
688 }
689 
690 mouse_in_horizontal_thumb || mouse_in_vertical_thumb
691 }
692 Self::SingleAxis {
693 config,
694 render_state,
695 ..
696 } => {
697 if config.drag_start().is_some() {
698 return false;
699 }
700 
701 let was_hovered = config.hovered();
702 let was_child_hovered = config.child_hovered();
703 let mouse_in = !is_covered
704 && render_state
705 .scrollbar_thumb_bounds
706 .expect("Bounds should exist")
707 .contains_point(position);
708 
709 let mouse_in_child = !is_covered
710 && config
711 .child_bounds()
712 .expect("Bounds should exist")
713 .contains_point(position);
714 
715 let mut hover_state_changed = false;
716 if was_hovered != mouse_in {
717 config.set_hovered(mouse_in);
718 hover_state_changed = true;
719 }
720 
721 if mouse_in_child != was_child_hovered {
722 config.set_child_hovered(mouse_in_child);
723 hover_state_changed = true;
724 }
725 
726 if hover_state_changed {
727 ctx.notify();
728 }
729 
730 mouse_in
731 }
732 }
733 }
734 
735 fn mousewheel(
736 &mut self,
737 mut delta: Vector2F,
738 precise: bool,
739 scrollable_size: Vector2F,
740 propagate_if_not_handled: bool,
741 ctx: &mut EventContext,
742 app: &AppContext,
743 ) -> bool {
744 let viewport_size = self.viewport_size(scrollable_size);
745 match self {
746 Self::BothAxes {
747 config,
748 horizontal_state,
749 vertical_state,
750 ..
751 } => {
752 delta = adjust_scroll_delta_with_sensitivity_config(
753 delta,
754 DUAL_AXES_SCROLL_SENSITIVITY,
755 );
756 
757 // Set horizontal delta to 0 if it is not scrollable on that axis.
758 if !config.should_handle_scroll_wheel(Axis::Horizontal)
759 || horizontal_state
760 .scrollbar_size_percentage
761 .expect("should be set at event dispatching time")
762 >= 1.
763 {
764 delta = delta.project_onto(Axis::Vertical);
765 }
766 
767 // Set vertical delta to 0 if it is not scrollable on that axis.
768 if !config.should_handle_scroll_wheel(Axis::Vertical)
769 || vertical_state
770 .scrollbar_size_percentage
771 .expect("should be set at event dispatching time")
772 >= 1.
773 {
774 delta = delta.project_onto(Axis::Horizontal);
775 }
776 
777 if delta.is_zero() {
778 return false;
779 }
780 
781 if !precise {
782 delta *= NUM_PIXELS_PER_LINE;
783 }
784 
785 // If there would be no change from the scroll, don't handle it
786 // so that other parent elements in the tree can handle it.
787 if propagate_if_not_handled && !config.can_scroll_delta(viewport_size, delta, app) {
788 return false;
789 }
790 
791 // Dispatch scroll event on each axis.
792 config.scroll_to(
793 viewport_size,
794 delta.along(Axis::Horizontal).into_pixels(),
795 Axis::Horizontal,
796 ctx,
797 );
798 config.scroll_to(
799 viewport_size,
800 delta.along(Axis::Vertical).into_pixels(),
801 Axis::Vertical,
802 ctx,
803 );
804 }
805 Self::SingleAxis {
806 axis,
807 config,
808 render_state,
809 ..
810 } => {
811 if !config.should_handle_scroll_wheel(*axis) {
812 return false;
813 }
814 
815 if render_state
816 .scrollbar_size_percentage
817 .expect("should be set at event dispatching time")
818 >= 1.
819 {
820 return !propagate_if_not_handled;
821 }
822 
823 if !precise {
824 delta *= NUM_PIXELS_PER_LINE;
825 }
826 
827 // If there would be no change from the scroll, don't handle it
828 // so that other parent elements in the tree can handle it.
829 if propagate_if_not_handled
830 && !config.can_scroll_delta(*axis, viewport_size, delta, app)
831 {
832 return false;
833 }
834 
835 config.scroll_to(viewport_size, delta.along(*axis).into_pixels(), *axis, ctx);
836 }
837 }
838 true
839 }
840 
841 /// Scroll the child element to match the delta scrolled from the previous to current scrollbar thumb position.
842 fn jump_to_position(
843 &mut self,
844 previous_position_along_axis: Pixels,
845 new_position_along_axis: Pixels,
846 scrollable_size: Vector2F,
847 axis: Axis,
848 ctx: &mut EventContext,
849 app: &AppContext,
850 ) {
851 let viewport_size = self.viewport_size(scrollable_size);
852 let data = match self {
853 Self::BothAxes { config, .. } => config.scroll_data(viewport_size, axis, app),
854 Self::SingleAxis {
855 config,
856 axis: scroll_axis,
857 ..
858 } if *scroll_axis == axis => config.scroll_data(axis, viewport_size, app),
859 Self::SingleAxis { .. } => {
860 log::warn!("Trying to jump to position on a non-scrollable axis");
861 return;
862 }
863 };
864 
865 let total_size = data.total_size;
866 let scroll_start = data.scroll_start;
867 let scroll_remaining = data.total_size - data.scroll_start - data.visible_px;
868 
869 // We need to use the original scrollbar size before resizing to calculate the scroll speed.
870 let scrollbar_size_percentage_before_resize = data.visible_px / total_size;
871 
872 // We don't want to update the scroll position if you're scrolled to the top and the cursor is above
873 // the element or if you're scrolled to the bottom and the cursor is below the element.
874 if (scroll_remaining <= Pixels::zero()
875 && new_position_along_axis > previous_position_along_axis)
876 || (scroll_start <= Pixels::zero()
877 && previous_position_along_axis > new_position_along_axis)
878 {
879 return;
880 }
881 
882 let delta = previous_position_along_axis - new_position_along_axis;
883 let adjusted_delta = delta / scrollbar_size_percentage_before_resize;
884 
885 match self {
886 Self::BothAxes { config, .. } => {
887 config.scroll_to(viewport_size, adjusted_delta, axis, ctx)
888 }
889 Self::SingleAxis { config, .. } => {
890 config.scroll_to(viewport_size, adjusted_delta, axis, ctx)
891 }
892 }
893 }
894}
895 
896/// Keep track of the render state of a single scrollbar.
897/// A scrollable could have multiple scrollbars when it is scrollable on both axes.
898struct ScrollbarRenderState {
899 /// The bounds of the whole scrollbar gutter.
900 scrollbar_track_bounds: Option<RectF>,
901 /// The relative position of the thumb within the scrollbar.
902 scrollbar_position_percentage: Option<f32>,
903 /// The relative height of the thumb compared to the whole scrollbar.
904 scrollbar_size_percentage: Option<f32>,
905 /// The bounds for the scrollbar thumb.
906 scrollbar_thumb_bounds: Option<RectF>,
907 /// The origin for the scrollbar thumb.
908 scrollbar_thumb_origin: Option<Vector2F>,
909 /// Padding between child element and the scrollbar.
910 padding_between_child_and_scrollbar: f32,
911 /// Padding after the scrollbar.
912 padding_after_scrollbar: f32,
913}
914 
915impl ScrollbarRenderState {
916 fn new() -> Self {
917 Self {
918 scrollbar_track_bounds: None,
919 scrollbar_position_percentage: None,
920 scrollbar_size_percentage: None,
921 scrollbar_thumb_bounds: None,
922 scrollbar_thumb_origin: None,
923 padding_between_child_and_scrollbar: LEFT_PADDING,
924 padding_after_scrollbar: RIGHT_PADDING,
925 }
926 }
927 
928 /// The additional spacing the scrollbar will take on the cross axis of the scrollable.
929 fn cross_axis_spacing(&self) -> f32 {
930 self.padding_between_child_and_scrollbar + self.padding_after_scrollbar
931 }
932 
933 /// Update the render state with the latest scroll data.
934 fn update_with_scroll_data(&mut self, scroll_data: ScrollData, scrollable_pixels: f32) {
935 // If total_size is zero (e.g., empty content), there's nothing to scroll.
936 // Set size_percentage to 1.0 so no scrollbar thumb renders.
937 if scroll_data.total_size <= Pixels::zero() {
938 self.scrollbar_size_percentage = Some(1.0);
939 self.scrollbar_position_percentage = Some(0.0);
940 return;
941 }
942 
943 let total_size = scroll_data.total_size;
944 
945 // Calculate the size percentage to render the scrollbar thumb.
946 let minimum_size_percentage = (MINIMUM_HEIGHT / scrollable_pixels).min(1.);
947 let size_percentage =
948 (scroll_data.visible_px / total_size).max(minimum_size_percentage.into_pixels());
949 
950 self.scrollbar_size_percentage = Some(size_percentage.as_f32());
951 
952 // The scrollbar position is calculated with the ratio between scroll top and scroll bottom.
953 let scroll_start = scroll_data.scroll_start;
954 let scroll_remaining = total_size - scroll_data.scroll_start - scroll_data.visible_px;
955 
956 let scrollbar_position_percentage = scroll_start / (scroll_start + scroll_remaining);
957 self.scrollbar_position_percentage = Some(scrollbar_position_percentage.as_f32());
958 }
959 
960 /// Draw the scrollbar based on the current render state.
961 #[allow(clippy::too_many_arguments)]
962 fn draw_scrollbar(
963 &mut self,
964 axis: Axis,
965 scrollable_size: Vector2F,
966 scrollable_size_with_padding: Vector2F,
967 origin: Vector2F,
968 scrollbar_thumb_background: Fill,
969 scrollbar_track_background: Fill,
970 appearance: ScrollableAppearance,
971 ctx: &mut PaintContext,
972 ) {
973 // The size of scrollbar track length is just the offset of the scrollbars projected to the cross axis.
974 let scrollbar_track_length = scrollable_size_with_padding.along(axis.invert());
975 let viewport_size = (scrollable_size - scrollable_size_with_padding).max(Vector2F::zero());
976 let scrollbar_track_origin = origin + scrollable_size.project_onto(axis.invert())
977 - scrollbar_track_length.along(axis.invert());
978 let scrollbar_track_size = scrollbar_size(axis, viewport_size, scrollbar_track_length);
979 
980 let scrollbar_track_bounds = RectF::new(scrollbar_track_origin, scrollbar_track_size);
981 self.scrollbar_track_bounds = Some(scrollbar_track_bounds);
982 
983 let scrollbar_size_percentage = self.scrollbar_size_percentage.unwrap();
984 let scrollbar_position_percentage = self.scrollbar_position_percentage.unwrap();
985 
986 // If the scrollbar is overlaid over the child, it should be rendered at a higher z-index.
987 // However, this doesn't apply if the scrollbar is non-functional (i.e. there's no thumb),
988 // as we wouldn't want a dummy scrollbar to block events dispatched to underlying children
989 // at a lower z-index (i.e. for editor text selection).
990 let render_scrollbar_thumb = scrollbar_size_percentage < 1.;
991 let render_at_higher_z_index = appearance.overlaid_scrollbar && render_scrollbar_thumb;
992 
993 if render_at_higher_z_index {
994 ctx.scene
995 .start_layer(ClipBounds::BoundedByActiveLayerAnd(scrollbar_track_bounds));
996 }
997 
998 let scrollbar = ctx
999 .scene
1000 .draw_rect_with_hit_recording(scrollbar_track_bounds);
1001 
1002 // If the scrollbar is overlaid, make it transparent.
1003 if appearance.overlaid_scrollbar {
1004 scrollbar.with_background(Fill::Solid(ColorU::transparent_black()));
1005 } else {
1006 scrollbar.with_background(scrollbar_track_background);
1007 }
1008 
1009 if render_scrollbar_thumb {
1010 let scrollbar_thumb_size = scrollbar_size(
1011 axis,
1012 viewport_size * scrollbar_size_percentage,
1013 appearance.scrollbar_size.as_f32(),
1014 );
1015 let scrollbar_thumb_origin = scrollbar_track_origin
1016 + scrollbar_size(
1017 axis,
1018 (viewport_size - scrollbar_thumb_size).max(Vector2F::zero())
1019 * scrollbar_position_percentage,
1020 self.padding_between_child_and_scrollbar,
1021 );
1022 
1023 self.scrollbar_thumb_bounds =
1024 Some(RectF::new(scrollbar_thumb_origin, scrollbar_thumb_size));
1025 self.scrollbar_thumb_origin = Some(scrollbar_thumb_origin);
1026 
1027 ctx.scene
1028 .draw_rect_with_hit_recording(RectF::new(
1029 scrollbar_thumb_origin,
1030 scrollbar_thumb_size,
1031 ))
1032 .with_background(scrollbar_thumb_background)
1033 .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)));
1034 } else {
1035 self.scrollbar_thumb_origin = Some(origin);
1036 self.scrollbar_thumb_bounds = Some(RectF::new(vec2f(0., 0.), vec2f(0., 0.)));
1037 }
1038 
1039 // See comment above about the layering of the scrollbar.
1040 if render_at_higher_z_index {
1041 ctx.scene.stop_layer();
1042 }
1043 }
1044 
1045 fn scrollbar_padding(
1046 &self,
1047 appearance: ScrollableAppearance,
1048 include_overlaid_scrollbar: bool,
1049 ) -> f32 {
1050 if !include_overlaid_scrollbar && appearance.overlaid_scrollbar {
1051 0.
1052 } else {
1053 self.cross_axis_spacing()
1054 }
1055 }
1056}
1057 
1058/// A wrapper element that makes the underlying child scrollable within a visible
1059/// viewport.
1060pub struct NewScrollable {
1061 state: ScrollableState,
1062 scrollable_size: Option<Vector2F>,
1063 origin: Option<Point>,
1064 
1065 /// The color of the scrollbar thumb when not hovered/active.
1066 nonactive_scrollbar_thumb_background: Fill,
1067 /// The color of the scrollbar thumb when hovered/active.
1068 active_scrollbar_thumb_background: Fill,
1069 /// The color of the scrollbar track.
1070 scrollbar_track_background: Fill,
1071 
1072 // This is a short-term solution for properly handling events on stacks. A stack will always
1073 // put its children on higher z-indexes than its origin, so a hit test using the standard
1074 // `z_index` method would always result in the event being covered (by the children of the
1075 // stack). Instead, we track the upper-bound of z-indexes _contained by_ the child element.
1076 // Then we use that upper bound to do the hit testing, which means a parent will always get
1077 // events from its children, regardless of whether they are stacks or not.
1078 child_max_z_index: Option<ZIndex>,
1079 
1080 // If true, propagate mousewheel events to the parent if the scrollable is scrolled to the edge
1081 // and a scrollwheel event would scroll further in the direction of the edge.
1082 // This is useful for nested scrollables where the inner scrollable is at the edge and the outer
1083 // scrollable should scroll instead.
1084 propagate_mousewheel_if_not_handled: bool,
1085 
1086 // If true, always handle scroll wheel events even if the scrollable is not scrolled to the edge.
1087 always_handle_events_first: bool,
1088}
1089 
1090impl NewScrollable {
1091 /// Internal method for creating a scrollable with an initial scrollable state.
1092 fn new_internal(
1093 state: ScrollableState,
1094 nonactive_scrollbar_thumb_background: Fill,
1095 active_scrollbar_thumb_background: Fill,
1096 scrollbar_track_background: Fill,
1097 always_handle_events_first: bool,
1098 ) -> Self {
1099 Self {
1100 state,
1101 scrollable_size: None,
1102 origin: None,
1103 nonactive_scrollbar_thumb_background,
1104 active_scrollbar_thumb_background,
1105 scrollbar_track_background,
1106 child_max_z_index: None,
1107 propagate_mousewheel_if_not_handled: false,
1108 always_handle_events_first,
1109 }
1110 }
1111 
1112 /// Thin wrapper that forwards to the underlying clipped handle(s) on each scrollable axis.
1113 ///
1114 /// Like the handle method, this mutates the scroll anchor(s) as a side effect; see
1115 /// [`ClippedScrollStateHandle::anchor_and_adjust_selection_for_scroll`] for the exact
1116 /// semantics.
1117 fn anchor_and_adjust_selection_for_scroll(
1118 &self,
1119 current_selection: Option<super::Selection>,
1120 ) -> Option<super::Selection> {
1121 match &self.state {
1122 ScrollableState::SingleAxis { axis, config, .. } => match config {
1123 SingleAxisConfig::Clipped { handle, .. } => {
1124 handle.anchor_and_adjust_selection_for_scroll(current_selection, *axis)
1125 }
1126 SingleAxisConfig::Manual { .. } => current_selection,
1127 },
1128 ScrollableState::BothAxes { config, .. } => match config {
1129 DualAxisConfig::Clipped {
1130 horizontal,
1131 vertical,
1132 ..
1133 } => {
1134 let selection = horizontal.handle.anchor_and_adjust_selection_for_scroll(
1135 current_selection,
1136 Axis::Horizontal,
1137 );
1138 vertical
1139 .handle
1140 .anchor_and_adjust_selection_for_scroll(selection, Axis::Vertical)
1141 }
1142 DualAxisConfig::Manual { .. } => current_selection,
1143 },
1144 }
1145 }
1146 
1147 fn clear_selection_scroll_anchor(&self) {
1148 match &self.state {
1149 ScrollableState::SingleAxis { config, .. } => {
1150 if let SingleAxisConfig::Clipped { handle, .. } = config {
1151 handle.clear_selection_scroll_anchor();
1152 }
1153 }
1154 ScrollableState::BothAxes { config, .. } => {
1155 if let DualAxisConfig::Clipped {
1156 horizontal,
1157 vertical,
1158 ..
1159 } = config
1160 {
1161 horizontal.handle.clear_selection_scroll_anchor();
1162 vertical.handle.clear_selection_scroll_anchor();
1163 }
1164 }
1165 }
1166 }
1167 
1168 /// Create a scroll element that is only scrollable on the vertical axis.
1169 pub fn vertical(
1170 config: SingleAxisConfig,
1171 nonactive_scrollbar_thumb_background: Fill,
1172 active_scrollbar_thumb_background: Fill,
1173 scrollbar_track_background: Fill,
1174 ) -> Self {
1175 config.validate(Axis::Vertical);
1176 let state = ScrollableState::SingleAxis {
1177 axis: Axis::Vertical,
1178 appearance: Default::default(),
1179 config,
1180 render_state: ScrollbarRenderState::new(),
1181 };
1182 Self::new_internal(
1183 state,
1184 nonactive_scrollbar_thumb_background,
1185 active_scrollbar_thumb_background,
1186 scrollbar_track_background,
1187 true,
1188 )
1189 }
1190 
1191 /// Create a scroll element that is only scrollable on the horizontal axis.
1192 pub fn horizontal(
1193 config: SingleAxisConfig,
1194 nonactive_scrollbar_thumb_background: Fill,
1195 active_scrollbar_thumb_background: Fill,
1196 scrollbar_track_background: Fill,
1197 ) -> Self {
1198 config.validate(Axis::Horizontal);
1199 let state = ScrollableState::SingleAxis {
1200 axis: Axis::Horizontal,
1201 appearance: Default::default(),
1202 config,
1203 render_state: ScrollbarRenderState::new(),
1204 };
1205 Self::new_internal(
1206 state,
1207 nonactive_scrollbar_thumb_background,
1208 active_scrollbar_thumb_background,
1209 scrollbar_track_background,
1210 true,
1211 )
1212 }
1213 
1214 /// Create a scroll element that is scrollable on both axes.
1215 pub fn horizontal_and_vertical(
1216 config: DualAxisConfig,
1217 nonactive_scrollbar_thumb_background: Fill,
1218 active_scrollbar_thumb_background: Fill,
1219 scrollbar_track_background: Fill,
1220 ) -> Self {
1221 config.validate();
1222 let state = ScrollableState::BothAxes {
1223 config,
1224 horizontal_appearance: Default::default(),
1225 vertical_appearance: Default::default(),
1226 horizontal_state: ScrollbarRenderState::new(),
1227 vertical_state: ScrollbarRenderState::new(),
1228 };
1229 Self::new_internal(
1230 state,
1231 nonactive_scrollbar_thumb_background,
1232 active_scrollbar_thumb_background,
1233 scrollbar_track_background,
1234 true,
1235 )
1236 }
1237 
1238 /// Override the default appearance for the vertical scrollbar. This will be a no-op (panic on local build)
1239 /// if the scrollable has no vertical scrollbar.
1240 pub fn with_vertical_scrollbar(mut self, new_appearance: ScrollableAppearance) -> Self {
1241 match &mut self.state {
1242 ScrollableState::BothAxes {
1243 vertical_appearance,
1244 ..
1245 } => *vertical_appearance = new_appearance,
1246 ScrollableState::SingleAxis {
1247 axis, appearance, ..
1248 } => {
1249 if matches!(axis, Axis::Horizontal) {
1250 if cfg!(debug_assertions) {
1251 panic!(
1252 "Trying to apply vertical scrollbar appearance on a horizontal scrollable"
1253 );
1254 } else {
1255 return self;
1256 }
1257 }
1258 *appearance = new_appearance;
1259 }
1260 }
1261 
1262 self
1263 }
1264 
1265 /// Override the default appearance for the horizontal scrollbar. This will be a no-op (panic on local build)
1266 /// if the scrollable has no horizontal scrollbar.
1267 pub fn with_horizontal_scrollbar(mut self, new_appearance: ScrollableAppearance) -> Self {
1268 match &mut self.state {
1269 ScrollableState::BothAxes {
1270 horizontal_appearance,
1271 ..
1272 } => *horizontal_appearance = new_appearance,
1273 ScrollableState::SingleAxis {
1274 axis, appearance, ..
1275 } => {
1276 if matches!(axis, Axis::Vertical) {
1277 if cfg!(debug_assertions) {
1278 panic!(
1279 "Trying to apply horizontal scrollbar appearance on a vertical scrollable"
1280 );
1281 } else {
1282 return self;
1283 }
1284 }
1285 *appearance = new_appearance;
1286 }
1287 }
1288 
1289 self
1290 }
1291 
1292 pub fn with_propagate_mousewheel_if_not_handled(mut self, propagate: bool) -> Self {
1293 self.propagate_mousewheel_if_not_handled = propagate;
1294 self
1295 }
1296 
1297 pub fn with_always_handle_events_first(mut self, always_handle_events_first: bool) -> Self {
1298 self.always_handle_events_first = always_handle_events_first;
1299 self
1300 }
1301 
1302 fn handle_event(
1303 &mut self,
1304 z_index: ZIndex,
1305 event: &DispatchedEvent,
1306 ctx: &mut EventContext,
1307 app: &AppContext,
1308 ) -> bool {
1309 match event.raw_event() {
1310 Event::LeftMouseDown { position, .. } => {
1311 if ctx.is_covered(Point::from_vec2f(*position, z_index)) {
1312 false
1313 } else {
1314 self.state.mouse_down(
1315 *position,
1316 self.scrollable_size.expect("Size should exist"),
1317 ctx,
1318 app,
1319 )
1320 }
1321 }
1322 Event::LeftMouseUp { .. } => self.state.mouse_up(),
1323 Event::LeftMouseDragged { position, .. } => self.state.mouse_dragged(
1324 *position,
1325 self.scrollable_size.expect("Size should exist"),
1326 ctx,
1327 app,
1328 ),
1329 Event::MouseMoved { position, .. } => {
1330 let is_covered = ctx.is_covered(Point::from_vec2f(*position, z_index));
1331 self.state.mouse_moved(*position, is_covered, ctx)
1332 }
1333 Event::ScrollWheel {
1334 delta,
1335 precise,
1336 position,
1337 modifiers: ModifiersState { ctrl: false, .. },
1338 } => {
1339 let is_covered = ctx.is_covered(Point::from_vec2f(*position, z_index));
1340 let in_bound = self
1341 .origin
1342 .zip(self.scrollable_size)
1343 .and_then(|(origin, size)| ctx.visible_rect(origin, size))
1344 .map(|visible| visible.contains_point(*position))
1345 .unwrap_or(false);
1346 
1347 if !in_bound || is_covered {
1348 false
1349 } else {
1350 self.state.mousewheel(
1351 *delta,
1352 *precise,
1353 self.scrollable_size.expect("Size should exist"),
1354 self.propagate_mousewheel_if_not_handled,
1355 ctx,
1356 app,
1357 )
1358 }
1359 }
1360 _ => false,
1361 }
1362 }
1363}
1364 
1365impl Element for NewScrollable {
1366 fn layout(
1367 &mut self,
1368 constraint: SizeConstraint,
1369 ctx: &mut LayoutContext,
1370 app: &AppContext,
1371 ) -> Vector2F {
1372 let size = self.state.layout_child(constraint, ctx, app);
1373 self.scrollable_size = Some(size);
1374 size
1375 }
1376 
1377 fn as_selectable_element(&self) -> Option<&dyn SelectableElement> {
1378 Some(self as &dyn SelectableElement)
1379 }
1380 
1381 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
1382 let scrollable_size = match self.scrollable_size {
1383 Some(size) => size,
1384 None => {
1385 log::warn!("Calling after_layout on NewScrollable without laying out the element");
1386 return;
1387 }
1388 };
1389 self.state.after_layout(scrollable_size, ctx, app);
1390 }
1391 
1392 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
1393 let size = match self.scrollable_size {
1394 Some(size) => size,
1395 None => {
1396 log::warn!("Calling paint on NewScrollable without laying out the element");
1397 return;
1398 }
1399 };
1400 // Technically, we only need to start a new layer if one of the axes is clipped.
1401 // For simplicity, always start the layer with scrollable bound. This will be just
1402 // no-op for the case when both axes are managed manually.
1403 ctx.scene
1404 .start_layer(ClipBounds::BoundedByActiveLayerAnd(RectF::new(
1405 origin, size,
1406 )));
1407 
1408 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
1409 
1410 let original_selection = ctx.current_selection;
1411 ctx.current_selection = self.anchor_and_adjust_selection_for_scroll(original_selection);
1412 self.state.paint_child(origin, size, ctx, app);
1413 ctx.current_selection = original_selection;
1414 
1415 self.state.draw_scrollbars(
1416 origin,
1417 size,
1418 self.nonactive_scrollbar_thumb_background,
1419 self.active_scrollbar_thumb_background,
1420 self.scrollbar_track_background,
1421 ctx,
1422 );
1423 self.child_max_z_index = Some(ctx.scene.max_active_z_index());
1424 ctx.scene.stop_layer();
1425 }
1426 
1427 fn size(&self) -> Option<Vector2F> {
1428 self.scrollable_size
1429 }
1430 
1431 fn origin(&self) -> Option<Point> {
1432 self.origin
1433 }
1434 
1435 fn dispatch_event(
1436 &mut self,
1437 event: &DispatchedEvent,
1438 ctx: &mut EventContext,
1439 app: &AppContext,
1440 ) -> bool {
1441 let Some(z_index) = self.child_max_z_index else {
1442 log::warn!("Tried to handle event in scrollable before the element is painted");
1443 return false;
1444 };
1445 
1446 if self.always_handle_events_first {
1447 // Different from other elements, scrollable always tries to handle the event first. It only
1448 // dispatches event to its child if the event is not handled by scrollable. This ensures we
1449 // never have additional events firing together with scrolling. Because of this requirement,
1450 // scrollable should strictly only handle events if either:
1451 // 1. There is an active scrolling session.
1452 // 2. The mouse event happens exactly on the scrollbar track and is not covered.
1453 let handled_by_scrollbar = self.handle_event(z_index, event, ctx, app);
1454 
1455 if !handled_by_scrollbar {
1456 if matches!(event.raw_event(), Event::LeftMouseDown { .. }) {
1457 self.clear_selection_scroll_anchor();
1458 }
1459 self.state.dispatch_event_to_child(event, ctx, app)
1460 } else {
1461 true
1462 }
1463 } else {
1464 let handled_by_child = self.state.dispatch_event_to_child(event, ctx, app);
1465 if !handled_by_child {
1466 self.handle_event(z_index, event, ctx, app)
1467 } else {
1468 true
1469 }
1470 }
1471 }
1472}
1473 
1474impl SelectableElement for NewScrollable {
1475 fn get_selection(
1476 &self,
1477 selection_start: Vector2F,
1478 selection_end: Vector2F,
1479 is_rect: IsRect,
1480 ) -> Option<Vec<SelectionFragment>> {
1481 let selection = self.anchor_and_adjust_selection_for_scroll(Some(super::Selection {
1482 start: selection_start,
1483 end: selection_end,
1484 is_rect,
1485 }))?;
1486 self.state
1487 .child_as_selectable_element()
1488 .and_then(|selectable_child| {
1489 selectable_child.get_selection(selection.start, selection.end, selection.is_rect)
1490 })
1491 }
1492 
1493 fn expand_selection(
1494 &self,
1495 point: Vector2F,
1496 direction: SelectionDirection,
1497 unit: SelectionType,
1498 word_boundaries_policy: &WordBoundariesPolicy,
1499 ) -> Option<Vector2F> {
1500 self.state
1501 .child_as_selectable_element()
1502 .and_then(|selectable_child| {
1503 selectable_child.expand_selection(point, direction, unit, word_boundaries_policy)
1504 })
1505 }
1506 
1507 fn is_point_semantically_before(
1508 &self,
1509 absolute_point: Vector2F,
1510 absolute_point_other: Vector2F,
1511 ) -> Option<bool> {
1512 self.state
1513 .child_as_selectable_element()
1514 .and_then(|selectable_child| {
1515 selectable_child.is_point_semantically_before(absolute_point, absolute_point_other)
1516 })
1517 }
1518 
1519 fn smart_select(
1520 &self,
1521 absolute_point: Vector2F,
1522 smart_select_fn: crate::elements::SmartSelectFn,
1523 ) -> Option<(Vector2F, Vector2F)> {
1524 self.state
1525 .child_as_selectable_element()
1526 .and_then(|selectable_child| {
1527 selectable_child.smart_select(absolute_point, smart_select_fn)
1528 })
1529 }
1530 
1531 fn calculate_clickable_bounds(
1532 &self,
1533 current_selection: Option<super::Selection>,
1534 ) -> Vec<RectF> {
1535 self.state
1536 .child_as_selectable_element()
1537 .map(|selectable_child| {
1538 selectable_child.calculate_clickable_bounds(
1539 self.anchor_and_adjust_selection_for_scroll(current_selection),
1540 )
1541 })
1542 .unwrap_or_default()
1543 }
1544}
1545 
1546impl ClippedScrollStateHandle {
1547 fn scroll_data(&self, viewport_size: Vector2F, child_size: Vector2F, axis: Axis) -> ScrollData {
1548 ScrollData {
1549 scroll_start: self.scroll_start(),
1550 visible_px: (viewport_size.along(axis)).into_pixels(),
1551 total_size: child_size.along(axis).into_pixels(),
1552 }
1553 }
1554}
1555 
1556#[cfg(test)]
1557#[path = "scrollable_test.rs"]
1558mod tests;
1559