1#![deny(missing_docs)]
2use floem_reactive::create_effect;
5use peniko::kurbo::{Point, Rect, Size, Stroke, Vec2};
6use peniko::{Brush, Color};
7
8use crate::style::CustomStylable;
9use crate::unit::PxPct;
10use crate::{
11 app_state::AppState,
12 context::{ComputeLayoutCx, PaintCx},
13 event::{Event, EventPropagation},
14 id::ViewId,
15 prop, prop_extractor,
16 style::{Background, BorderColor, BorderRadius, Style, StyleSelector},
17 style_class,
18 unit::Px,
19 view::{IntoView, View},
20 Renderer,
21};
22
23use super::Decorators;
24
25enum ScrollState {
26 EnsureVisible(Rect),
27 ScrollDelta(Vec2),
28 ScrollTo(Point),
29 ScrollToPercent(f32),
30 ScrollToView(ViewId),
31}
32
33const SCROLLBAR_MIN_SIZE: f64 = 10.0;
36
37#[derive(Debug, Copy, Clone)]
39enum BarHeldState {
40 None,
42 Vertical(f64, Vec2),
45 Horizontal(f64, Vec2),
48}
49
50style_class!(
51 pub Handle
53);
54style_class!(
55 pub Track
57);
58
59prop!(
60 pub Rounded: bool {} = cfg!(target_os = "macos")
62);
63prop!(
64 pub Thickness: Px {} = Px(10.0)
66);
67prop!(
68 pub Border: Px {} = Px(0.0)
70);
71
72prop_extractor! {
73 ScrollTrackStyle {
74 color: Background,
75 border_radius: BorderRadius,
76 border_color: BorderColor,
77 border: Border,
78 rounded: Rounded,
79 thickness: Thickness,
80 }
81}
82
83prop!(
84 pub VerticalInset: Px {} = Px(0.0)
86);
87
88prop!(
89 pub HorizontalInset: Px {} = Px(0.0)
91);
92
93prop!(
94 pub HideBars: bool {} = false
96);
97
98prop!(
99 pub PropagatePointerWheel: bool {} = true
101);
102
103prop!(
104 pub VerticalScrollAsHorizontal: bool {} = false
106);
107
108prop!(
109 pub OverflowClip: bool {} = true
111);
112
113prop_extractor!(ScrollStyle {
114 vertical_bar_inset: VerticalInset,
115 horizontal_bar_inset: HorizontalInset,
116 hide_bar: HideBars,
117 propagate_pointer_wheel: PropagatePointerWheel,
118 vertical_scroll_as_horizontal: VerticalScrollAsHorizontal,
119 overflow_clip: OverflowClip,
120});
121
122const HANDLE_COLOR: Brush = Brush::Solid(Color::rgba8(0, 0, 0, 120));
123
124style_class!(
125 pub ScrollClass
127);
128
129pub struct Scroll {
131 id: ViewId,
132 child: ViewId,
133
134 total_rect: Rect,
135
136 content_rect: Rect,
138
139 child_size: Size,
140
141 child_viewport: Rect,
143
144 computed_child_viewport: Rect,
148
149 onscroll: Option<Box<dyn Fn(Rect)>>,
150 held: BarHeldState,
151 v_handle_hover: bool,
152 h_handle_hover: bool,
153 v_track_hover: bool,
154 h_track_hover: bool,
155 handle_style: ScrollTrackStyle,
156 handle_active_style: ScrollTrackStyle,
157 handle_hover_style: ScrollTrackStyle,
158 track_style: ScrollTrackStyle,
159 track_hover_style: ScrollTrackStyle,
160 scroll_style: ScrollStyle,
161}
162
163pub fn scroll<V: IntoView + 'static>(child: V) -> Scroll {
165 let id = ViewId::new();
166 let child = child.into_view();
167 let child_id = child.id();
168 id.set_children(vec![child]);
169
170 Scroll {
171 id,
172 child: child_id,
173 content_rect: Rect::ZERO,
174 total_rect: Rect::ZERO,
175 child_size: Size::ZERO,
176 child_viewport: Rect::ZERO,
177 computed_child_viewport: Rect::ZERO,
178 onscroll: None,
179 held: BarHeldState::None,
180 v_handle_hover: false,
181 h_handle_hover: false,
182 v_track_hover: false,
183 h_track_hover: false,
184 handle_style: Default::default(),
185 handle_active_style: Default::default(),
186 handle_hover_style: Default::default(),
187 track_style: Default::default(),
188 track_hover_style: Default::default(),
189 scroll_style: Default::default(),
190 }
191 .class(ScrollClass)
192}
193
194impl Scroll {
195 pub fn on_scroll(mut self, onscroll: impl Fn(Rect) + 'static) -> Self {
200 self.onscroll = Some(Box::new(onscroll));
201 self
202 }
203
204 pub fn ensure_visible(self, to: impl Fn() -> Rect + 'static) -> Self {
212 let id = self.id();
213 create_effect(move |_| {
214 let rect = to();
215 id.update_state_deferred(ScrollState::EnsureVisible(rect));
216 });
217
218 self
219 }
220
221 pub fn scroll_delta(self, delta: impl Fn() -> Vec2 + 'static) -> Self {
227 let id = self.id();
228 create_effect(move |_| {
229 let delta = delta();
230 id.update_state(ScrollState::ScrollDelta(delta));
231 });
232
233 self
234 }
235
236 pub fn scroll_to(self, origin: impl Fn() -> Option<Point> + 'static) -> Self {
242 let id = self.id();
243 create_effect(move |_| {
244 if let Some(origin) = origin() {
245 id.update_state_deferred(ScrollState::ScrollTo(origin));
246 }
247 });
248
249 self
250 }
251
252 pub fn scroll_to_percent(self, percent: impl Fn() -> f32 + 'static) -> Self {
258 let id = self.id();
259 create_effect(move |_| {
260 let percent = percent() / 100.;
261 id.update_state_deferred(ScrollState::ScrollToPercent(percent));
262 });
263 self
264 }
265
266 pub fn scroll_to_view(self, view: impl Fn() -> Option<ViewId> + 'static) -> Self {
272 let id = self.id();
273 create_effect(move |_| {
274 if let Some(view) = view() {
275 id.update_state_deferred(ScrollState::ScrollToView(view));
276 }
277 });
278
279 self
280 }
281
282 fn do_scroll_delta(&mut self, app_state: &mut AppState, delta: Vec2) {
283 let new_origin = self.child_viewport.origin() + delta;
284 self.clamp_child_viewport(app_state, self.child_viewport.with_origin(new_origin));
285 }
286
287 fn do_scroll_to(&mut self, app_state: &mut AppState, origin: Point) {
288 self.clamp_child_viewport(app_state, self.child_viewport.with_origin(origin));
289 }
290
291 pub fn pan_to_visible(&mut self, app_state: &mut AppState, rect: Rect) {
296 fn closest_on_axis(val: f64, min: f64, max: f64) -> f64 {
302 assert!(min <= max);
303 if val > min && val < max {
304 0.0
305 } else if val <= min {
306 val - min
307 } else {
308 val - max
309 }
310 }
311
312 let target_size = Size::new(
316 rect.width().min(self.child_viewport.width()),
317 rect.height().min(self.child_viewport.height()),
318 );
319 let rect = rect.with_size(target_size);
320
321 let x0 = closest_on_axis(
322 rect.min_x(),
323 self.child_viewport.min_x(),
324 self.child_viewport.max_x(),
325 );
326 let x1 = closest_on_axis(
327 rect.max_x(),
328 self.child_viewport.min_x(),
329 self.child_viewport.max_x(),
330 );
331 let y0 = closest_on_axis(
332 rect.min_y(),
333 self.child_viewport.min_y(),
334 self.child_viewport.max_y(),
335 );
336 let y1 = closest_on_axis(
337 rect.max_y(),
338 self.child_viewport.min_y(),
339 self.child_viewport.max_y(),
340 );
341
342 let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 };
343 let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 };
344 let new_origin = self.child_viewport.origin() + Vec2::new(delta_x, delta_y);
345 self.clamp_child_viewport(app_state, self.child_viewport.with_origin(new_origin));
346 }
347
348 fn update_size(&mut self) {
349 self.child_size = self.child_size();
350 self.content_rect = self.id.get_content_rect();
351 self.total_rect = self.id.get_size().unwrap_or_default().to_rect();
352 }
353
354 fn clamp_child_viewport(
355 &mut self,
356 app_state: &mut AppState,
357 child_viewport: Rect,
358 ) -> Option<()> {
359 let actual_rect = self.content_rect;
360 let actual_size = actual_rect.size();
361 let width = actual_rect.width();
362 let height = actual_rect.height();
363 let child_size = self.child_size;
364
365 let mut child_viewport = child_viewport;
366 if width >= child_size.width {
367 child_viewport.x0 = 0.0;
368 } else if child_viewport.x0 > child_size.width - width {
369 child_viewport.x0 = child_size.width - width;
370 } else if child_viewport.x0 < 0.0 {
371 child_viewport.x0 = 0.0;
372 }
373
374 if height >= child_size.height {
375 child_viewport.y0 = 0.0;
376 } else if child_viewport.y0 > child_size.height - height {
377 child_viewport.y0 = child_size.height - height;
378 } else if child_viewport.y0 < 0.0 {
379 child_viewport.y0 = 0.0;
380 }
381 child_viewport = child_viewport.with_size(actual_size);
382
383 if child_viewport != self.child_viewport {
384 self.child.set_viewport(child_viewport);
385 app_state.request_compute_layout_recursive(self.id());
386 app_state.request_paint(self.id());
387 self.child_viewport = child_viewport;
388 if let Some(onscroll) = &self.onscroll {
389 onscroll(child_viewport);
390 }
391 } else {
392 return None;
393 }
394 Some(())
395 }
396
397 fn child_size(&self) -> Size {
398 self.child
399 .get_layout()
400 .map(|layout| Size::new(layout.size.width as f64, layout.size.height as f64))
401 .unwrap()
402 }
403
404 fn v_handle_style(&self) -> &ScrollTrackStyle {
405 if let BarHeldState::Vertical(..) = self.held {
406 &self.handle_active_style
407 } else if self.v_handle_hover {
408 &self.handle_hover_style
409 } else {
410 &self.handle_style
411 }
412 }
413
414 fn h_handle_style(&self) -> &ScrollTrackStyle {
415 if let BarHeldState::Horizontal(..) = self.held {
416 &self.handle_active_style
417 } else if self.h_handle_hover {
418 &self.handle_hover_style
419 } else {
420 &self.handle_style
421 }
422 }
423
424 fn draw_bars(&self, cx: &mut PaintCx) {
425 let scroll_offset = self.child_viewport.origin().to_vec2();
426 let radius = |style: &ScrollTrackStyle, rect: Rect, vertical| {
427 if style.rounded() {
428 if vertical {
429 (rect.x1 - rect.x0) / 2.
430 } else {
431 (rect.y1 - rect.y0) / 2.
432 }
433 } else {
434 match style.border_radius() {
435 crate::unit::PxPct::Px(px) => px,
436 crate::unit::PxPct::Pct(pct) => rect.size().min_side() * (pct / 100.),
437 }
438 }
439 };
440
441 if let Some(bounds) = self.calc_vertical_bar_bounds(cx.app_state) {
442 let style = self.v_handle_style();
443 let track_style =
444 if self.v_track_hover || matches!(self.held, BarHeldState::Vertical(..)) {
445 &self.track_hover_style
446 } else {
447 &self.track_style
448 };
449
450 if let Some(color) = track_style.color() {
451 let mut bounds = bounds - scroll_offset;
452 bounds.y0 = self.total_rect.y0;
453 bounds.y1 = self.total_rect.y1;
454 cx.fill(&bounds, &color, 0.0);
455 }
456 let edge_width = style.border().0;
457 let rect = (bounds - scroll_offset).inset(-edge_width / 2.0);
458 let rect = rect.to_rounded_rect(radius(style, rect, true));
459 cx.fill(&rect, &style.color().unwrap_or(HANDLE_COLOR), 0.0);
460 if edge_width > 0.0 {
461 cx.stroke(&rect, &style.border_color(), &Stroke::new(edge_width));
462 }
463 }
464
465 if let Some(bounds) = self.calc_horizontal_bar_bounds(cx.app_state) {
467 let style = self.h_handle_style();
468 let track_style =
469 if self.h_track_hover || matches!(self.held, BarHeldState::Horizontal(..)) {
470 &self.track_hover_style
471 } else {
472 &self.track_style
473 };
474
475 if let Some(color) = track_style.color() {
476 let mut bounds = bounds - scroll_offset;
477 bounds.x0 = self.total_rect.x0;
478 bounds.x1 = self.total_rect.x1;
479 cx.fill(&bounds, &color, 0.0);
480 }
481 let edge_width = style.border().0;
482 let rect = (bounds - scroll_offset).inset(-edge_width / 2.0);
483 let rect = rect.to_rounded_rect(radius(style, rect, false));
484 cx.fill(&rect, &style.color().unwrap_or(HANDLE_COLOR), 0.0);
485 if edge_width > 0.0 {
486 cx.stroke(&rect, &style.border_color(), &Stroke::new(edge_width));
487 }
488 }
489 }
490
491 fn calc_vertical_bar_bounds(&self, _app_state: &mut AppState) -> Option<Rect> {
492 let viewport_size = self.child_viewport.size();
493 let content_size = self.child_size;
494 let scroll_offset = self.child_viewport.origin().to_vec2();
495
496 if viewport_size.height >= content_size.height - 1. {
498 return None;
499 }
500
501 let style = self.v_handle_style();
502
503 let bar_width = style.thickness().0;
504 let bar_pad = self.scroll_style.vertical_bar_inset().0;
505
506 let percent_visible = viewport_size.height / content_size.height;
507 let percent_scrolled = scroll_offset.y / (content_size.height - viewport_size.height);
508
509 let length = (percent_visible * self.total_rect.height()).ceil();
510 let length = length.max(style.thickness().0);
512
513 let top_y_offset = ((self.total_rect.height() - length) * percent_scrolled).ceil();
514 let bottom_y_offset = top_y_offset + length;
515
516 let x0 = scroll_offset.x + self.total_rect.width() - bar_width - bar_pad;
517 let y0 = scroll_offset.y + top_y_offset;
518
519 let x1 = scroll_offset.x + self.total_rect.width() - bar_pad;
520 let y1 = scroll_offset.y + bottom_y_offset;
521
522 Some(Rect::new(x0, y0, x1, y1))
523 }
524
525 fn calc_horizontal_bar_bounds(&self, _app_state: &mut AppState) -> Option<Rect> {
526 let viewport_size = self.child_viewport.size();
527 let content_size = self.child_size;
528 let scroll_offset = self.child_viewport.origin().to_vec2();
529
530 if viewport_size.width >= content_size.width - 1. {
531 return None;
532 }
533
534 let style = self.h_handle_style();
535
536 let bar_width = style.thickness().0;
537 let bar_pad = self.scroll_style.horizontal_bar_inset().0;
538
539 let percent_visible = viewport_size.width / content_size.width;
540 let percent_scrolled = scroll_offset.x / (content_size.width - viewport_size.width);
541
542 let length = (percent_visible * self.total_rect.width()).ceil();
543 let length = length.max(SCROLLBAR_MIN_SIZE);
544
545 let horizontal_padding = if viewport_size.height >= content_size.height {
546 0.0
547 } else {
548 bar_pad + bar_pad + bar_width
549 };
550
551 let left_x_offset =
552 ((self.total_rect.width() - length - horizontal_padding) * percent_scrolled).ceil();
553 let right_x_offset = left_x_offset + length;
554
555 let x0 = scroll_offset.x + left_x_offset;
556 let y0 = scroll_offset.y + self.total_rect.height() - bar_width - bar_pad;
557
558 let x1 = scroll_offset.x + right_x_offset;
559 let y1 = scroll_offset.y + self.total_rect.height() - bar_pad;
560
561 Some(Rect::new(x0, y0, x1, y1))
562 }
563
564 fn click_vertical_bar_area(&mut self, app_state: &mut AppState, pos: Point) {
565 let new_y = (pos.y / self.content_rect.height()) * self.child_size.height
566 - self.content_rect.height() / 2.0;
567 let mut new_origin = self.child_viewport.origin();
568 new_origin.y = new_y;
569 self.do_scroll_to(app_state, new_origin);
570 }
571
572 fn click_horizontal_bar_area(&mut self, app_state: &mut AppState, pos: Point) {
573 let new_x = (pos.x / self.content_rect.width()) * self.child_size.width
574 - self.content_rect.width() / 2.0;
575 let mut new_origin = self.child_viewport.origin();
576 new_origin.x = new_x;
577 self.do_scroll_to(app_state, new_origin);
578 }
579
580 fn point_hits_vertical_bar(&self, app_state: &mut AppState, pos: Point) -> bool {
581 if let Some(mut bounds) = self.calc_vertical_bar_bounds(app_state) {
582 let scroll_offset = self.child_viewport.origin().to_vec2();
584 bounds.x1 = self.total_rect.x1 + scroll_offset.x;
585 pos.x >= bounds.x0 && pos.x <= bounds.x1
586 } else {
587 false
588 }
589 }
590
591 fn point_hits_horizontal_bar(&self, app_state: &mut AppState, pos: Point) -> bool {
592 if let Some(mut bounds) = self.calc_horizontal_bar_bounds(app_state) {
593 let scroll_offset = self.child_viewport.origin().to_vec2();
595 bounds.y1 = self.total_rect.y1 + scroll_offset.y;
596 pos.y >= bounds.y0 && pos.y <= bounds.y1
597 } else {
598 false
599 }
600 }
601
602 fn point_hits_vertical_handle(&self, app_state: &mut AppState, pos: Point) -> bool {
603 if let Some(mut bounds) = self.calc_vertical_bar_bounds(app_state) {
604 let scroll_offset = self.child_viewport.origin().to_vec2();
606 bounds.x1 = self.total_rect.x1 + scroll_offset.x;
607 bounds.contains(pos)
608 } else {
609 false
610 }
611 }
612
613 fn point_hits_horizontal_handle(&self, app_state: &mut AppState, pos: Point) -> bool {
614 if let Some(mut bounds) = self.calc_horizontal_bar_bounds(app_state) {
615 let scroll_offset = self.child_viewport.origin().to_vec2();
617 bounds.y1 = self.total_rect.y1 + scroll_offset.y;
618 bounds.contains(pos)
619 } else {
620 false
621 }
622 }
623
624 fn are_bars_held(&self) -> bool {
626 !matches!(self.held, BarHeldState::None)
627 }
628
629 fn update_hover_states(&mut self, app_state: &mut AppState, pos: Point) {
630 let scroll_offset = self.child_viewport.origin().to_vec2();
631 let pos = pos + scroll_offset;
632 let hover = self.point_hits_vertical_handle(app_state, pos);
633 if self.v_handle_hover != hover {
634 self.v_handle_hover = hover;
635 app_state.request_paint(self.id());
636 }
637 let hover = self.point_hits_horizontal_handle(app_state, pos);
638 if self.h_handle_hover != hover {
639 self.h_handle_hover = hover;
640 app_state.request_paint(self.id());
641 }
642 let hover = self.point_hits_vertical_bar(app_state, pos);
643 if self.v_track_hover != hover {
644 self.v_track_hover = hover;
645 app_state.request_paint(self.id());
646 }
647 let hover = self.point_hits_horizontal_bar(app_state, pos);
648 if self.h_track_hover != hover {
649 self.h_track_hover = hover;
650 app_state.request_paint(self.id());
651 }
652 }
653
654 fn do_scroll_to_view(
655 &mut self,
656 app_state: &mut AppState,
657 target: ViewId,
658 target_rect: Option<Rect>,
659 ) {
660 if target.get_layout().is_some() && !target.is_hidden_recursive() {
661 let mut rect = target.layout_rect();
662
663 if let Some(target_rect) = target_rect {
664 rect = rect + target_rect.origin().to_vec2();
665
666 let new_size = target_rect
667 .size()
668 .to_rect()
669 .intersect(rect.size().to_rect())
670 .size();
671 rect = rect.with_size(new_size);
672 }
673
674 let rect = rect.with_origin(
679 rect.origin()
680 - self.id.layout_rect().origin().to_vec2()
681 - self.content_rect.origin().to_vec2()
682 + self.computed_child_viewport.origin().to_vec2(),
683 );
684
685 self.pan_to_visible(app_state, rect);
686 }
687 }
688
689 pub fn scroll_style(
691 self,
692 style: impl Fn(ScrollCustomStyle) -> ScrollCustomStyle + 'static,
693 ) -> Self {
694 self.custom_style(style)
695 }
696}
697
698impl View for Scroll {
699 fn id(&self) -> ViewId {
700 self.id
701 }
702
703 fn debug_name(&self) -> std::borrow::Cow<'static, str> {
704 "Scroll".into()
705 }
706
707 fn view_style(&self) -> Option<Style> {
708 Some(Style::new().items_start())
709 }
710
711 fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
712 if let Ok(state) = state.downcast::<ScrollState>() {
713 match *state {
714 ScrollState::EnsureVisible(rect) => {
715 self.pan_to_visible(cx.app_state, rect);
716 }
717 ScrollState::ScrollDelta(delta) => {
718 self.do_scroll_delta(cx.app_state, delta);
719 }
720 ScrollState::ScrollTo(origin) => {
721 self.do_scroll_to(cx.app_state, origin);
722 }
723 ScrollState::ScrollToPercent(percent) => {
724 let mut child_size = self.child_size;
725 child_size *= percent as f64;
726 let point = child_size.to_vec2().to_point();
727 self.do_scroll_to(cx.app_state, point);
728 }
729 ScrollState::ScrollToView(id) => {
730 self.do_scroll_to_view(cx.app_state, id, None);
731 }
732 }
733 self.id.request_layout();
734 }
735 }
736
737 fn scroll_to(&mut self, cx: &mut AppState, target: ViewId, rect: Option<Rect>) -> bool {
738 let found = self.child.view().borrow_mut().scroll_to(cx, target, rect);
739 if found {
740 self.do_scroll_to_view(cx, target, rect);
741 }
742 found
743 }
744
745 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
746 let style = cx.style();
747
748 self.scroll_style.read(cx);
749
750 let handle_style = style.clone().apply_class(Handle);
751 self.handle_style.read_style(cx, &handle_style);
752 self.handle_hover_style.read_style(
753 cx,
754 &handle_style
755 .clone()
756 .apply_selectors(&[StyleSelector::Hover]),
757 );
758 self.handle_active_style
759 .read_style(cx, &handle_style.apply_selectors(&[StyleSelector::Active]));
760
761 let track_style = style.apply_class(Track);
762 self.track_style.read_style(cx, &track_style);
763 self.track_hover_style
764 .read_style(cx, &track_style.apply_selectors(&[StyleSelector::Hover]));
765
766 cx.style_view(self.child);
767 }
768
769 fn compute_layout(&mut self, cx: &mut ComputeLayoutCx) -> Option<Rect> {
770 self.update_size();
771 self.clamp_child_viewport(cx.app_state_mut(), self.child_viewport);
772 self.computed_child_viewport = self.child_viewport;
773 cx.compute_view_layout(self.child);
774 None
775 }
776
777 fn event_before_children(
778 &mut self,
779 cx: &mut crate::context::EventCx,
780 event: &Event,
781 ) -> EventPropagation {
782 let viewport_size = self.child_viewport.size();
783 let scroll_offset = self.child_viewport.origin().to_vec2();
784 let content_size = self.child_size;
785
786 match &event {
787 Event::PointerDown(event) => {
788 if !self.scroll_style.hide_bar() && event.button.is_primary() {
789 self.held = BarHeldState::None;
790
791 let pos = event.pos + scroll_offset;
792
793 if self.point_hits_vertical_bar(cx.app_state, pos) {
794 if self.point_hits_vertical_handle(cx.app_state, pos) {
795 self.held = BarHeldState::Vertical(
796 event.pos.y,
798 scroll_offset,
799 );
800 cx.update_active(self.id());
801 self.id.request_paint();
803 return EventPropagation::Stop;
804 }
805 self.click_vertical_bar_area(cx.app_state, event.pos);
806 let scroll_offset = self.child_viewport.origin().to_vec2();
807 self.held = BarHeldState::Vertical(
808 event.pos.y,
810 scroll_offset,
811 );
812 cx.update_active(self.id());
813 return EventPropagation::Stop;
814 } else if self.point_hits_horizontal_bar(cx.app_state, pos) {
815 if self.point_hits_horizontal_handle(cx.app_state, pos) {
816 self.held = BarHeldState::Horizontal(
817 event.pos.x,
819 scroll_offset,
820 );
821 cx.update_active(self.id());
822 cx.app_state.request_paint(self.id());
824 return EventPropagation::Stop;
825 }
826 self.click_horizontal_bar_area(cx.app_state, event.pos);
827 let scroll_offset = self.child_viewport.origin().to_vec2();
828 self.held = BarHeldState::Horizontal(
829 event.pos.x,
831 scroll_offset,
832 );
833 cx.update_active(self.id());
834 return EventPropagation::Stop;
835 }
836 }
837 }
838 Event::PointerUp(_event) => {
839 if self.are_bars_held() {
840 self.held = BarHeldState::None;
841 cx.app_state.request_paint(self.id());
843 }
844 }
845 Event::PointerMove(event) => {
846 if !self.scroll_style.hide_bar() {
847 let pos = event.pos + scroll_offset;
848 self.update_hover_states(cx.app_state, event.pos);
849
850 if self.are_bars_held() {
851 match self.held {
852 BarHeldState::Vertical(offset, initial_scroll_offset) => {
853 let scale_y = viewport_size.height / content_size.height;
854 let y = initial_scroll_offset.y + (event.pos.y - offset) / scale_y;
855 self.clamp_child_viewport(
856 cx.app_state,
857 self.child_viewport
858 .with_origin(Point::new(initial_scroll_offset.x, y)),
859 );
860 }
861 BarHeldState::Horizontal(offset, initial_scroll_offset) => {
862 let scale_x = viewport_size.width / content_size.width;
863 let x = initial_scroll_offset.x + (event.pos.x - offset) / scale_x;
864 self.clamp_child_viewport(
865 cx.app_state,
866 self.child_viewport
867 .with_origin(Point::new(x, initial_scroll_offset.y)),
868 );
869 }
870 BarHeldState::None => {}
871 }
872 } else if self.point_hits_vertical_bar(cx.app_state, pos)
873 || self.point_hits_horizontal_bar(cx.app_state, pos)
874 {
875 return EventPropagation::Continue;
876 }
877 }
878 }
879 Event::PointerLeave => {
880 self.v_handle_hover = false;
881 self.h_handle_hover = false;
882 self.v_track_hover = false;
883 self.h_track_hover = false;
884 cx.app_state.request_paint(self.id());
885 }
886 _ => {}
887 }
888 EventPropagation::Continue
889 }
890
891 fn event_after_children(
892 &mut self,
893 cx: &mut crate::context::EventCx,
894 event: &Event,
895 ) -> EventPropagation {
896 if let Event::PointerWheel(pointer_event) = &event {
897 if let Some(listener) = event.listener() {
898 if self
899 .id
900 .apply_event(&listener, event)
901 .is_some_and(|prop| prop.is_processed())
902 {
903 return EventPropagation::Stop;
904 }
905 }
906 let delta = pointer_event.delta;
907 let delta = if self.scroll_style.vertical_scroll_as_horizontal()
908 && delta.x == 0.0
909 && delta.y != 0.0
910 {
911 Vec2::new(delta.y, delta.x)
912 } else {
913 delta
914 };
915 let any_change = self.clamp_child_viewport(cx.app_state, self.child_viewport + delta);
916
917 self.update_hover_states(cx.app_state, pointer_event.pos);
919
920 return if self.scroll_style.propagate_pointer_wheel() && any_change.is_none() {
921 EventPropagation::Continue
922 } else {
923 EventPropagation::Stop
924 };
925 }
926
927 EventPropagation::Continue
928 }
929
930 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
931 cx.save();
932 let radius = match self.id.state().borrow().combined_style.get(BorderRadius) {
933 crate::unit::PxPct::Px(px) => px,
934 crate::unit::PxPct::Pct(pct) => self.total_rect.size().min_side() * (pct / 100.),
935 };
936 if self.scroll_style.overflow_clip() {
937 if radius > 0.0 {
938 let rect = self.total_rect.to_rounded_rect(radius);
939 cx.clip(&rect);
940 } else {
941 cx.clip(&self.total_rect);
942 }
943 }
944 cx.offset((-self.child_viewport.x0, -self.child_viewport.y0));
945 cx.paint_view(self.child);
946 cx.restore();
947
948 if !self.scroll_style.hide_bar() {
949 self.draw_bars(cx);
950 }
951 }
952}
953#[derive(Default, Debug, Clone)]
955pub struct ScrollCustomStyle(Style);
956impl From<ScrollCustomStyle> for Style {
957 fn from(value: ScrollCustomStyle) -> Self {
958 value.0
959 }
960}
961
962impl CustomStylable<ScrollCustomStyle> for Scroll {
963 type DV = Self;
964}
965
966impl ScrollCustomStyle {
967 pub fn new() -> Self {
969 Self(Style::new())
970 }
971
972 pub fn shrink_to_fit(mut self) -> Self {
981 self = Self(self.0.min_size(0., 0.).size_full());
982 self
983 }
984
985 pub fn overflow_clip(mut self, clip: bool) -> Self {
987 self = Self(self.0.set(OverflowClip, clip));
988 self
989 }
990
991 pub fn handle_background(mut self, color: impl Into<Brush>) -> Self {
993 self = Self(self.0.class(Handle, |s| s.background(color.into())));
994 self
995 }
996
997 pub fn handle_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
999 self = Self(self.0.class(Handle, |s| s.border_radius(border_radius)));
1000 self
1001 }
1002
1003 pub fn handle_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1005 self = Self(self.0.class(Handle, |s| s.border_color(border_color)));
1006 self
1007 }
1008
1009 pub fn handle_border(mut self, border: impl Into<Px>) -> Self {
1011 self = Self(self.0.class(Handle, |s| s.set(Border, border)));
1012 self
1013 }
1014
1015 pub fn handle_rounded(mut self, rounded: impl Into<bool>) -> Self {
1017 self = Self(self.0.class(Handle, |s| s.set(Rounded, rounded)));
1018 self
1019 }
1020
1021 pub fn handle_thickness(mut self, thickness: impl Into<Px>) -> Self {
1023 self = Self(self.0.class(Handle, |s| s.set(Thickness, thickness)));
1024 self
1025 }
1026
1027 pub fn track_background(mut self, color: impl Into<Brush>) -> Self {
1029 self = Self(self.0.class(Track, |s| s.background(color.into())));
1030 self
1031 }
1032
1033 pub fn track_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1035 self = Self(self.0.class(Track, |s| s.border_radius(border_radius)));
1036 self
1037 }
1038
1039 pub fn track_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1041 self = Self(self.0.class(Track, |s| s.border_color(border_color)));
1042 self
1043 }
1044
1045 pub fn track_border(mut self, border: impl Into<Px>) -> Self {
1047 self = Self(self.0.class(Track, |s| s.set(Border, border)));
1048 self
1049 }
1050
1051 pub fn track_rounded(mut self, rounded: impl Into<bool>) -> Self {
1053 self = Self(self.0.class(Track, |s| s.set(Rounded, rounded)));
1054 self
1055 }
1056
1057 pub fn track_thickness(mut self, thickness: impl Into<Px>) -> Self {
1059 self = Self(self.0.class(Track, |s| s.set(Thickness, thickness)));
1060 self
1061 }
1062
1063 pub fn vertical_track_inset(mut self, inset: impl Into<Px>) -> Self {
1065 self = Self(self.0.set(VerticalInset, inset));
1066 self
1067 }
1068
1069 pub fn horizontal_track_inset(mut self, inset: impl Into<Px>) -> Self {
1071 self = Self(self.0.set(HorizontalInset, inset));
1072 self
1073 }
1074
1075 pub fn hide_bars(mut self, hide: impl Into<bool>) -> Self {
1077 self = Self(self.0.set(HideBars, hide));
1078 self
1079 }
1080
1081 pub fn propagate_pointer_wheel(mut self, propagate: impl Into<bool>) -> Self {
1083 self = Self(self.0.set(PropagatePointerWheel, propagate));
1084 self
1085 }
1086
1087 pub fn vertical_scroll_as_horizontal(mut self, vert_as_horiz: impl Into<bool>) -> Self {
1089 self = Self(self.0.set(VerticalScrollAsHorizontal, vert_as_horiz));
1090 self
1091 }
1092}
1093
1094pub trait ScrollExt {
1096 fn scroll(self) -> Scroll;
1098}
1099
1100impl<T: IntoView + 'static> ScrollExt for T {
1101 fn scroll(self) -> Scroll {
1102 scroll(self)
1103 }
1104}