1#![deny(missing_docs)]
2use floem_reactive::Effect;
5use peniko::kurbo::{Affine, Axis, Point, Rect, RoundedRect, RoundedRectRadii, Stroke, Vec2};
6use peniko::{Brush, Color};
7use std::time::Duration;
8use std::{cell::RefCell, rc::Rc};
9use taffy::Overflow;
10use ui_events::pointer::{PointerButton, PointerEvent, PointerId};
11
12use crate::easing::Linear;
13use crate::event::{
14 DragEvent, DragSourceEvent, PointerCaptureEvent, PointerScrollEventExt, RouteKind, ScrollTo,
15};
16use crate::prelude::EventListenerTrait;
17use crate::prelude::el::UpdatePhaseLayout;
18use crate::style::ScrollbarWidth;
19use crate::{
20 BoxTree, ElementId, Renderer,
21 context::{EventCx, PaintCx, StyleCx},
22 event::{Event, EventPropagation, Phase},
23 prop, prop_extractor,
24 style::{
25 Background, BorderBottomColor, BorderBottomLeftRadius, BorderBottomRightRadius,
26 BorderLeftColor, BorderRightColor, BorderTopColor, BorderTopLeftRadius,
27 BorderTopRightRadius, CustomStylable, CustomStyle, OverflowX, OverflowY, Style, StyleClass,
28 },
29 style_class,
30 unit::{Px, PxPct},
31 view::{IntoView, View},
32};
33use crate::{ViewId, custom_event};
34use understory_box_tree::NodeFlags;
35
36use super::Decorators;
37
38#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct ScrollChanged {
45 pub offset: Vec2,
47}
48custom_event!(ScrollChanged);
49
50#[derive(Debug, Clone, Copy)]
51enum ScrollState {
52 EnsureVisible(Rect),
53 ScrollDelta(Vec2),
54 ScrollTo(Point),
55 ScrollToPercent(f32),
56 ScrollToElement(ElementId),
57}
58
59struct ScrollEventResult {
60 propagation: EventPropagation,
61 new_offset: Option<Vec2>,
62}
63
64trait Vec2Ext {
65 fn max_by_component(self, other: Self) -> Self;
67
68 fn min_by_component(self, other: Self) -> Self;
70}
71
72impl Vec2Ext for Vec2 {
73 fn max_by_component(self, other: Self) -> Self {
74 Vec2::new(self.x.max(other.x), self.y.max(other.y))
75 }
76
77 fn min_by_component(self, other: Self) -> Self {
78 Vec2::new(self.x.min(other.x), self.y.min(other.y))
79 }
80}
81
82#[derive(Debug, Clone)]
83struct ScrollHandle {
84 element_id: ElementId,
85 box_tree: Rc<RefCell<BoxTree>>,
86 axis: Axis,
87 style: ScrollTrackStyle,
89 initial_offset: Vec2,
90}
91
92impl ScrollHandle {
93 fn new(parent_id: ViewId, axis: Axis) -> Self {
94 let box_tree = parent_id.box_tree();
95 let element_id = parent_id.create_child_element_id(2);
96
97 Self {
98 element_id,
99 box_tree,
100 axis,
101 style: Default::default(),
102 initial_offset: Vec2::ZERO,
103 }
104 }
105
106 fn style(&mut self, cx: &mut StyleCx) {
107 let resolved =
108 cx.resolve_nested_maps(Style::new(), &[Handle::class_ref()], self.element_id);
109 if self.style.read_style_for(cx, &resolved, self.element_id) {
110 self.element_id.owning_id().request_paint();
111 }
112 }
113
114 fn event(
115 &mut self,
116 cx: &mut EventCx,
117 parent_id: ViewId,
118 child_id: ViewId,
119 ) -> ScrollEventResult {
120 match &cx.event {
121 Event::Pointer(PointerEvent::Down(e)) => {
122 if let Some(pointer_id) = e.pointer.pointer_id
123 && e.state.buttons.contains(PointerButton::Primary)
124 {
125 cx.window_state
126 .set_pointer_capture(pointer_id, self.element_id);
127 }
128 cx.window_state.request_paint(parent_id);
129 }
130 Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => {
131 self.initial_offset = parent_id.get_child_translation();
132 cx.start_drag(
133 *drag,
134 crate::event::DragConfig::new(0., Duration::ZERO, Linear),
135 false,
136 );
137 }
138 Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => {
139 let pos = dme.current_state.logical_point();
140
141 let viewport_size = parent_id
143 .get_content_rect_local()
144 .size()
145 .get_coord(self.axis);
146 let content_size = child_id.get_layout_rect_local().size().get_coord(self.axis);
147 let scale = content_size / viewport_size;
148
149 let scroll_delta = (pos.get_coord(self.axis)
150 - dme.start_state.logical_point().get_coord(self.axis))
151 * scale;
152
153 let mut new_offset: Vec2 = self.initial_offset;
154 new_offset.set_coord(
155 self.axis,
156 self.initial_offset.get_coord(self.axis) + scroll_delta,
157 );
158
159 let viewport_size_vec = parent_id.get_content_rect_local().size();
161 let content_size_vec = child_id.get_layout_rect_local().size();
162 let max_scroll = (content_size_vec.to_vec2() - viewport_size_vec.to_vec2())
163 .max_by_component(Vec2::ZERO);
164
165 let new_offset = new_offset
166 .max_by_component(Vec2::ZERO)
167 .min_by_component(max_scroll);
168
169 return ScrollEventResult {
170 propagation: EventPropagation::Stop,
171 new_offset: Some(new_offset),
172 };
173 }
174
175 _ => {
176 return ScrollEventResult {
177 propagation: EventPropagation::Continue,
178 new_offset: None,
179 };
180 }
181 }
182 ScrollEventResult {
183 propagation: EventPropagation::Stop,
184 new_offset: None,
185 }
186 }
187
188 fn set_position(
189 &mut self,
190 scroll_offset: Vec2,
191 viewport: Rect,
192 full_rect: Rect,
193 content_size: peniko::kurbo::Size,
194 scrollbar_width: f64,
195 bar_inset: f64,
196 ) {
197 let viewport_size = viewport.size().get_coord(self.axis);
198 let content_size_val = content_size.get_coord(self.axis);
199 let full_rect_size = full_rect.size().get_coord(self.axis);
200
201 if viewport_size >= (content_size_val - f64::EPSILON) {
203 self.box_tree
205 .borrow_mut()
206 .set_flags(self.element_id.0, NodeFlags::empty());
207 return;
208 }
209
210 let percent_visible = viewport_size / content_size_val;
212 let max_scroll = content_size_val - viewport_size;
213 let scroll_offset_val = scroll_offset.get_coord(self.axis);
214
215 let percent_scrolled = if max_scroll > 0.0 {
216 scroll_offset_val / max_scroll
217 } else {
218 0.0
219 };
220
221 let handle_length = (percent_visible * full_rect_size).ceil().max(15.);
222
223 let track_length = full_rect_size;
224 let available_travel = track_length - handle_length;
225 let handle_offset = (available_travel * percent_scrolled).ceil();
226
227 let rect = match self.axis {
228 Axis::Vertical => {
229 let x0 = full_rect.width() - scrollbar_width - bar_inset;
230 let y0 = handle_offset;
231 let x1 = full_rect.width() - bar_inset;
232 let y1 = handle_offset + handle_length;
233 Rect::new(x0, y0, x1, y1)
234 }
235 Axis::Horizontal => {
236 let x0 = handle_offset;
237 let y0 = full_rect.height() - scrollbar_width - bar_inset;
238 let x1 = handle_offset + handle_length;
239 let y1 = full_rect.height() - bar_inset;
240 Rect::new(x0, y0, x1, y1)
241 }
242 };
243
244 self.box_tree
245 .borrow_mut()
246 .set_local_bounds(self.element_id.0, rect);
247 self.box_tree
248 .borrow_mut()
249 .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE);
250 }
251
252 fn paint(&self, cx: &mut PaintCx) {
253 let box_tree = self.box_tree.borrow();
254 let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default();
255
256 let radius = if self.style.rounded() {
257 match self.axis {
258 Axis::Vertical => RoundedRectRadii::from_single_radius((rect.x1 - rect.x0) / 2.),
259 Axis::Horizontal => RoundedRectRadii::from_single_radius((rect.y1 - rect.y0) / 2.),
260 }
261 } else {
262 let size = rect.size().min_side();
263 let border_radius = self.style.border_radius();
264 RoundedRectRadii {
265 top_left: crate::view::border_radius(
266 border_radius.top_left.unwrap_or(PxPct::Px(0.)),
267 size,
268 ),
269 top_right: crate::view::border_radius(
270 border_radius.top_right.unwrap_or(PxPct::Px(0.)),
271 size,
272 ),
273 bottom_left: crate::view::border_radius(
274 border_radius.bottom_left.unwrap_or(PxPct::Px(0.)),
275 size,
276 ),
277 bottom_right: crate::view::border_radius(
278 border_radius.bottom_right.unwrap_or(PxPct::Px(0.)),
279 size,
280 ),
281 }
282 };
283
284 let edge_width = self.style.border().0;
285 let rect_with_border = rect.inset(-edge_width / 2.0);
286 let rounded_rect = rect_with_border.to_rounded_rect(radius);
287
288 cx.fill(
289 &rounded_rect,
290 &self.style.color().unwrap_or(HANDLE_COLOR),
291 0.0,
292 );
293
294 if edge_width > 0.0
295 && let Some(color) = self.style.border_color().right
296 {
297 cx.stroke(&rounded_rect, &color, &Stroke::new(edge_width));
298 }
299 }
300}
301
302#[derive(Debug, Clone)]
303struct ScrollTrack {
304 element_id: ElementId,
305 handle_element_id: ElementId,
306 box_tree: Rc<RefCell<BoxTree>>,
307 axis: Axis,
308 style: ScrollTrackStyle,
309}
310
311impl ScrollTrack {
312 fn new(parent_id: ViewId, handle_element_id: ElementId, axis: Axis) -> Self {
313 let box_tree = parent_id.box_tree();
314 let element_id = parent_id.create_child_element_id(1);
315
316 Self {
317 element_id,
318 handle_element_id,
319 box_tree,
320 axis,
321 style: Default::default(),
322 }
323 }
324
325 fn style(&mut self, cx: &mut StyleCx) {
326 let resolved = cx.resolve_nested_maps(Style::new(), &[Track::class_ref()], self.element_id);
327 if self.style.read_style_for(cx, &resolved, self.element_id) {
328 self.element_id.owning_id().request_paint();
329 }
330 }
331
332 fn event(
333 &mut self,
334 cx: &mut EventCx,
335 parent_id: ViewId,
336 child_id: ViewId,
337 ) -> ScrollEventResult {
338 match &cx.event {
339 Event::Pointer(PointerEvent::Down(e)) => {
340 if e.state.buttons.contains(PointerButton::Primary) {
341 cx.window_state
342 .set_pointer_capture(PointerId::PRIMARY, self.handle_element_id);
343 }
344 let pos = e.state.logical_point();
345
346 let viewport = parent_id.get_content_rect_local();
348 let full_rect = parent_id.get_layout_rect_local();
349 let content_size = child_id.get_layout_rect_local().size();
350
351 let pos_val = pos.get_coord(self.axis);
352 let viewport_size = viewport.size().get_coord(self.axis);
353 let content_size_val = content_size.get_coord(self.axis);
354 let full_rect_size = full_rect.size().get_coord(self.axis);
355
356 let percent_visible = viewport_size / content_size_val;
357 let handle_length = (percent_visible * full_rect_size).ceil().max(15.);
358 let max_scroll = content_size_val - viewport_size;
359
360 let track_length = full_rect_size;
361 let available_travel = track_length - handle_length;
362
363 let target_handle_offset = (pos_val - handle_length / 2.0)
364 .max(0.0)
365 .min(available_travel);
366 let target_percent = if available_travel > 0.0 {
367 target_handle_offset / available_travel
368 } else {
369 0.0
370 };
371
372 let new_offset = (target_percent * max_scroll).clamp(0.0, max_scroll);
373
374 cx.window_state.request_paint(parent_id);
375
376 let mut offset = parent_id.get_child_translation();
377 offset.set_coord(self.axis, new_offset);
378 ScrollEventResult {
379 propagation: EventPropagation::Stop,
380 new_offset: Some(offset),
381 }
382 }
383 _ => ScrollEventResult {
384 propagation: EventPropagation::Continue,
385 new_offset: None,
386 },
387 }
388 }
389
390 fn set_position(
391 &mut self,
392 viewport: Rect,
393 full_rect: Rect,
394 content_size: peniko::kurbo::Size,
395 scrollbar_width: f64,
396 bar_inset: f64,
397 ) {
398 let viewport_size = viewport.size().get_coord(self.axis);
399 let content_size_val = content_size.get_coord(self.axis);
400
401 if viewport_size >= (content_size_val - f64::EPSILON) {
403 self.box_tree
405 .borrow_mut()
406 .set_flags(self.element_id.0, NodeFlags::empty());
407 return;
408 }
409
410 let rect = match self.axis {
411 Axis::Vertical => {
412 let x0 = full_rect.width() - scrollbar_width - bar_inset;
413 let y0 = 0.0;
414 let x1 = full_rect.width() - bar_inset;
415 let y1 = full_rect.height();
416 Rect::new(x0, y0, x1, y1)
417 }
418 Axis::Horizontal => {
419 let x0 = 0.0;
420 let y0 = full_rect.height() - scrollbar_width - bar_inset;
421 let x1 = full_rect.width();
422 let y1 = full_rect.height() - bar_inset;
423 Rect::new(x0, y0, x1, y1)
424 }
425 };
426
427 self.box_tree
428 .borrow_mut()
429 .set_local_bounds(self.element_id.0, rect);
430 self.box_tree
431 .borrow_mut()
432 .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE);
433 }
434
435 fn paint(&self, cx: &mut PaintCx) {
436 let box_tree = self.box_tree.borrow();
437 let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default();
438
439 if let Some(color) = self.style.color() {
440 cx.fill(&rect, &color, 0.0);
441 }
442 }
443}
444
445style_class!(
446 pub Handle
448);
449style_class!(
450 pub Track
452);
453
454prop!(
455 pub Rounded: bool {} = cfg!(target_os = "macos")
457);
458prop!(
459 pub Border: Px {} = Px(0.0)
461);
462
463prop_extractor! {
464 ScrollTrackStyle {
465 color: Background,
466 border_top_left_radius: BorderTopLeftRadius,
467 border_top_right_radius: BorderTopRightRadius,
468 border_bottom_left_radius: BorderBottomLeftRadius,
469 border_bottom_right_radius: BorderBottomRightRadius,
470 border_left_color: BorderLeftColor,
471 border_top_color: BorderTopColor,
472 border_right_color: BorderRightColor,
473 border_bottom_color: BorderBottomColor,
474 border: Border,
475 rounded: Rounded,
476 }
477}
478
479impl ScrollTrackStyle {
480 fn border_radius(&self) -> crate::style::BorderRadius {
481 crate::style::BorderRadius {
482 top_left: Some(self.border_top_left_radius()),
483 top_right: Some(self.border_top_right_radius()),
484 bottom_left: Some(self.border_bottom_left_radius()),
485 bottom_right: Some(self.border_bottom_right_radius()),
486 }
487 }
488
489 fn border_color(&self) -> crate::style::BorderColor {
490 crate::style::BorderColor {
491 left: self.border_left_color(),
492 top: self.border_top_color(),
493 right: self.border_right_color(),
494 bottom: self.border_bottom_color(),
495 }
496 }
497}
498
499prop!(
500 pub VerticalInset: Px {} = Px(0.0)
502);
503
504prop!(
505 pub HorizontalInset: Px {} = Px(0.0)
507);
508
509prop!(
510 pub HideBars: bool {} = false
512);
513
514prop!(
515 pub ShowBarsWhenIdle: bool {} = true
517);
518
519prop!(
520 pub PropagatePointerWheel: bool {} = true
522);
523
524prop!(
525 pub VerticalScrollAsHorizontal: bool {} = false
527);
528
529prop_extractor!(ScrollStyle {
530 vertical_bar_inset: VerticalInset,
531 horizontal_bar_inset: HorizontalInset,
532 hide_bar: HideBars,
533 show_bars_when_idle: ShowBarsWhenIdle,
534 propagate_pointer_wheel: PropagatePointerWheel,
535 vertical_scroll_as_horizontal: VerticalScrollAsHorizontal,
536 overflow_x: OverflowX,
537 overflow_y: OverflowY,
538 scrollbar_width: ScrollbarWidth,
539});
540
541const HANDLE_COLOR: Brush = Brush::Solid(Color::from_rgba8(0, 0, 0, 120));
542
543style_class!(
544 pub ScrollClass
546);
547
548pub struct Scroll {
550 id: ViewId,
551 child: ViewId,
552 scroll_offset: Vec2,
554 v_handle: ScrollHandle,
555 h_handle: ScrollHandle,
556 v_track: ScrollTrack,
557 h_track: ScrollTrack,
558 scroll_style: ScrollStyle,
559}
560
561#[deprecated(since = "0.2.0", note = "Use Scroll::new() instead")]
563pub fn scroll<V: IntoView + 'static>(child: V) -> Scroll {
564 Scroll::new(child)
565}
566
567impl Scroll {
568 pub fn new(child: impl IntoView) -> Self {
578 let id = ViewId::new();
579 id.register_listener(UpdatePhaseLayout::listener_key());
580
581 let child = child.into_any();
582 let child_id = child.id();
583 id.add_child(child);
584 id.set_box_tree_clip(Some(RoundedRect::from_rect(Rect::ZERO, 0.)));
586
587 let v_handle = ScrollHandle::new(id, Axis::Vertical);
588 let h_handle = ScrollHandle::new(id, Axis::Horizontal);
589
590 Scroll {
591 id,
592 child: child_id,
593 scroll_offset: Vec2::ZERO,
594 v_track: ScrollTrack::new(id, v_handle.element_id, Axis::Vertical),
595 h_track: ScrollTrack::new(id, h_handle.element_id, Axis::Horizontal),
596 v_handle,
597 h_handle,
598 scroll_style: Default::default(),
599 }
600 .class(ScrollClass)
601 }
602}
603
604impl Scroll {
605 pub fn ensure_visible(self, to: impl Fn() -> Rect + 'static) -> Self {
613 let id = self.id();
614 Effect::new(move |_| {
615 let rect = to();
616 id.update_state_deferred(ScrollState::EnsureVisible(rect));
617 });
618
619 self
620 }
621
622 pub fn scroll_delta(self, delta: impl Fn() -> Vec2 + 'static) -> Self {
628 let id = self.id();
629 Effect::new(move |_| {
630 let delta = delta();
631 id.update_state(ScrollState::ScrollDelta(delta));
632 });
633
634 self
635 }
636
637 pub fn scroll_to(self, origin: impl Fn() -> Option<Point> + 'static) -> Self {
643 let id = self.id();
644 Effect::new(move |_| {
645 if let Some(origin) = origin() {
646 id.update_state_deferred(ScrollState::ScrollTo(origin));
647 }
648 });
649
650 self
651 }
652
653 pub fn scroll_to_percent(self, percent: impl Fn() -> f32 + 'static) -> Self {
659 let id = self.id();
660 Effect::new(move |_| {
661 let percent = percent() / 100.;
662 id.update_state_deferred(ScrollState::ScrollToPercent(percent));
663 });
664 self
665 }
666
667 pub fn scroll_to_view(self, view: impl Fn() -> Option<ViewId> + 'static) -> Self {
673 let id = self.id();
674 Effect::new(move |_| {
675 if let Some(view) = view() {
676 id.update_state_deferred(ScrollState::ScrollToElement(view.get_element_id()));
677 }
678 });
679
680 self
681 }
682}
683
684impl Scroll {
686 fn apply_scroll_delta(&mut self, delta: Vec2) -> Option<Vec2> {
690 let viewport_size = self.id.get_content_rect_local().size();
691 let content_size = self.child.get_layout_rect_local().size();
692
693 let mut max_scroll =
695 (content_size.to_vec2() - viewport_size.to_vec2()).max_by_component(Vec2::ZERO);
696
697 let can_scroll_x = matches!(self.scroll_style.overflow_x(), taffy::Overflow::Scroll);
699 let can_scroll_y = matches!(self.scroll_style.overflow_y(), taffy::Overflow::Scroll);
700
701 let mut new_scroll_offset = self.scroll_offset + delta;
702 if !can_scroll_x {
703 new_scroll_offset.x = 0.0;
704 max_scroll.x = 0.0;
705 }
706 if !can_scroll_y {
707 new_scroll_offset.y = 0.0;
708 max_scroll.y = 0.0;
709 }
710
711 let old_scroll_offset = self.scroll_offset;
712 self.scroll_offset = new_scroll_offset
713 .max_by_component(Vec2::ZERO)
714 .min_by_component(max_scroll);
715 let change = self.id.set_child_translation(self.scroll_offset);
716 if change {
717 self.id.route_event(
718 Event::new_custom(ScrollChanged {
719 offset: self.scroll_offset,
720 }),
721 RouteKind::Directed {
722 target: self.id.get_element_id(),
723 phases: crate::context::Phases::TARGET,
724 },
725 );
726 }
727
728 if change {
729 self.set_positions();
730 Some(self.scroll_offset - old_scroll_offset)
731 } else {
732 None
733 }
734 }
735
736 fn do_scroll_to(&mut self, offset: Point) {
744 self.apply_scroll_delta(offset.to_vec2() - self.scroll_offset);
745 }
746
747 pub fn do_ensure_visible(&mut self, rect: Rect) {
755 let viewport = self.id.get_content_rect_local();
756 let viewport_size = viewport.size();
757
758 let visible_rect = Rect::from_origin_size(self.scroll_offset.to_point(), viewport_size);
760
761 if visible_rect.contains_rect(rect) {
763 return;
764 }
765
766 let mut new_offset = self.scroll_offset;
767
768 if rect.width() > viewport_size.width {
770 new_offset.x = rect.x0;
772 } else if rect.x0 < visible_rect.x0 {
773 new_offset.x = rect.x0;
775 } else if rect.x1 > visible_rect.x1 {
776 new_offset.x = rect.x1 - viewport_size.width;
778 }
779
780 if rect.height() > viewport_size.height {
782 new_offset.y = rect.y0;
784 } else if rect.y0 < visible_rect.y0 {
785 new_offset.y = rect.y0;
787 } else if rect.y1 > visible_rect.y1 {
788 new_offset.y = rect.y1 - viewport_size.height;
790 }
791
792 self.do_scroll_to(new_offset.to_point());
793 }
794
795 fn do_scroll_to_element(&mut self, scroll_to: ScrollTo) -> EventPropagation {
796 let child_element_id = self.child.get_element_id();
797 let box_tree = self.id.box_tree();
798 let mut box_tree = box_tree.borrow_mut();
799
800 let Some(target_local_rect) = scroll_to
801 .rect
802 .or_else(|| box_tree.local_bounds(scroll_to.id.0))
803 else {
804 return EventPropagation::Continue;
805 };
806
807 let target_transform = box_tree
808 .get_or_compute_world_transform(scroll_to.id.0)
809 .unwrap_or(Affine::IDENTITY);
810 let child_transform = box_tree
811 .get_or_compute_world_transform(child_element_id.0)
812 .unwrap_or(Affine::IDENTITY);
813
814 let target_world_rect = target_transform.transform_rect_bbox(target_local_rect);
815 let child_world_origin = child_transform * Point::ZERO;
816
817 let target_rect = Rect::new(
818 target_world_rect.x0 - child_world_origin.x,
819 target_world_rect.y0 - child_world_origin.y,
820 target_world_rect.x1 - child_world_origin.x,
821 target_world_rect.y1 - child_world_origin.y,
822 );
823 drop(box_tree);
824
825 self.do_ensure_visible(target_rect);
826
827 let viewport_size = self.id.get_content_rect_local().size();
828 let visible_rect = Rect::from_origin_size(self.scroll_offset.to_point(), viewport_size);
829
830 if visible_rect.contains_rect(target_rect) {
831 EventPropagation::Stop
832 } else {
833 EventPropagation::Continue
834 }
835 }
836
837 fn set_positions(&mut self) {
838 let viewport = self.id.get_content_rect_local();
839 let full_rect = self.id.get_layout_rect_local();
840 let content_size = self.child.get_layout_rect_local().size();
841 let scrollbar_width = self.scroll_style.scrollbar_width().0;
842 let v_bar_inset = self.scroll_style.vertical_bar_inset().0;
843 let h_bar_inset = self.scroll_style.horizontal_bar_inset().0;
844
845 self.v_track.set_position(
846 viewport,
847 full_rect,
848 content_size,
849 scrollbar_width,
850 v_bar_inset,
851 );
852 self.h_track.set_position(
853 viewport,
854 full_rect,
855 content_size,
856 scrollbar_width,
857 h_bar_inset,
858 );
859
860 self.v_handle.set_position(
861 self.scroll_offset,
862 viewport,
863 full_rect,
864 content_size,
865 scrollbar_width,
866 v_bar_inset,
867 );
868 self.h_handle.set_position(
869 self.scroll_offset,
870 viewport,
871 full_rect,
872 content_size,
873 scrollbar_width,
874 h_bar_inset,
875 );
876 }
877}
878
879impl View for Scroll {
880 fn id(&self) -> ViewId {
881 self.id
882 }
883
884 fn debug_name(&self) -> std::borrow::Cow<'static, str> {
885 "Scroll".into()
886 }
887
888 fn view_style(&self) -> Option<Style> {
889 Some(
890 Style::new()
891 .items_start()
892 .overflow_x(Overflow::Scroll)
893 .overflow_y(Overflow::Scroll),
894 )
895 }
896
897 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
898 if let Ok(state) = state.downcast::<ScrollState>() {
899 match *state {
900 ScrollState::EnsureVisible(rect) => {
901 self.do_ensure_visible(rect);
902 }
903 ScrollState::ScrollDelta(delta) => {
904 self.apply_scroll_delta(delta);
905 }
906 ScrollState::ScrollTo(origin) => {
907 self.do_scroll_to(origin);
908 }
909 ScrollState::ScrollToPercent(percent) => {
910 let content_size = self.child.get_layout_rect_local().size();
911 let viewport_size = self.id.get_content_rect_local().size();
912
913 let max_scroll = (content_size.to_vec2() - viewport_size.to_vec2())
915 .max_by_component(Vec2::ZERO);
916
917 let target_offset = max_scroll * (percent as f64);
919
920 self.do_scroll_to(target_offset.to_point());
921 }
922 ScrollState::ScrollToElement(id) => {
923 self.do_scroll_to_element(ScrollTo { id, rect: None });
924 }
925 }
926 self.id.request_box_tree_update_for_view();
927 }
928 }
929
930 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
931 self.scroll_style.read(cx);
932
933 if cx.reason.needs_resolve_nested_maps() {
935 self.v_handle.style(cx);
936 self.h_handle.style(cx);
937 self.v_track.style(cx);
938 self.h_track.style(cx);
939 return;
940 }
941
942 for (element_id, _reason) in cx.targeted_elements.clone() {
943 if element_id == self.v_handle.element_id {
944 self.v_handle.style(cx);
945 } else if element_id == self.h_handle.element_id {
946 self.h_handle.style(cx);
947 } else if element_id == self.v_track.element_id {
948 self.v_track.style(cx);
949 } else if element_id == self.h_track.element_id {
950 self.h_track.style(cx);
951 }
952 }
953 }
954
955 fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
956 if UpdatePhaseLayout::extract(&cx.event).is_some() {
958 self.set_positions();
959 return EventPropagation::Stop;
960 }
961
962 if let Some(scroll_to) = ScrollTo::extract(&cx.event) {
963 return self.do_scroll_to_element(*scroll_to);
964 }
965 if cx.phase == Phase::Target {
967 if cx.target == self.v_handle.element_id {
968 let result = self.v_handle.event(cx, self.id, self.child);
969 if let Some(new_offset) = result.new_offset
970 && self
971 .apply_scroll_delta(new_offset - self.scroll_offset)
972 .is_some()
973 {
974 cx.window_state.request_paint(self.id);
975 }
976 return result.propagation;
977 }
978 if cx.target == self.h_handle.element_id {
979 let result = self.h_handle.event(cx, self.id, self.child);
980 if let Some(new_offset) = result.new_offset
981 && self
982 .apply_scroll_delta(new_offset - self.scroll_offset)
983 .is_some()
984 {
985 cx.window_state.request_paint(self.id);
986 }
987 return result.propagation;
988 }
989 if cx.target == self.v_track.element_id {
990 let result = self.v_track.event(cx, self.id, self.child);
991 if let Some(new_offset) = result.new_offset
992 && self
993 .apply_scroll_delta(new_offset - self.scroll_offset)
994 .is_some()
995 {
996 cx.window_state.request_paint(self.id);
997 }
998 return result.propagation;
999 }
1000 if cx.target == self.h_track.element_id {
1001 let result = self.h_track.event(cx, self.id, self.child);
1002 if let Some(new_offset) = result.new_offset
1003 && self
1004 .apply_scroll_delta(new_offset - self.scroll_offset)
1005 .is_some()
1006 {
1007 cx.window_state.request_paint(self.id);
1008 }
1009 return result.propagation;
1010 }
1011 }
1012
1013 if let Event::Pointer(PointerEvent::Scroll(pse)) = &cx.event {
1015 let size = self.id.get_layout_rect_local().size();
1016 let delta = pse.resolve_to_points(None, Some(size));
1017 let delta = -if self.scroll_style.vertical_scroll_as_horizontal()
1018 && delta.x == 0.0
1019 && delta.y != 0.0
1020 {
1021 Vec2::new(delta.y, delta.x)
1022 } else {
1023 delta
1024 };
1025
1026 let change = self.apply_scroll_delta(delta);
1027
1028 if change.is_some() {
1029 cx.window_state.request_paint(self.id);
1030 }
1031
1032 return if self.scroll_style.propagate_pointer_wheel() && change.is_none() {
1033 EventPropagation::Continue
1034 } else {
1035 EventPropagation::Stop
1036 };
1037 }
1038
1039 EventPropagation::Continue
1040 }
1041
1042 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
1043 self.apply_scroll_delta(Vec2::ZERO);
1046
1047 if cx.target_id == self.id.get_element_id() {
1050 } else if cx.target_id == self.v_handle.element_id {
1052 if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1054 self.v_handle.paint(cx);
1055 }
1056 } else if cx.target_id == self.h_handle.element_id {
1057 if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1059 self.h_handle.paint(cx);
1060 }
1061 } else if cx.target_id == self.v_track.element_id {
1062 if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1064 self.v_track.paint(cx);
1065 }
1066 } else if cx.target_id == self.h_track.element_id {
1067 if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1069 self.h_track.paint(cx);
1070 }
1071 }
1072 }
1073}
1074#[derive(Default, Debug, Clone)]
1076pub struct ScrollCustomStyle(Style);
1077impl From<ScrollCustomStyle> for Style {
1078 fn from(value: ScrollCustomStyle) -> Self {
1079 value.0
1080 }
1081}
1082impl From<Style> for ScrollCustomStyle {
1083 fn from(value: Style) -> Self {
1084 Self(value)
1085 }
1086}
1087impl CustomStyle for ScrollCustomStyle {
1088 type StyleClass = ScrollClass;
1089}
1090
1091impl CustomStylable<ScrollCustomStyle> for Scroll {
1092 type DV = Self;
1093}
1094
1095impl ScrollCustomStyle {
1096 pub fn new() -> Self {
1098 Self(Style::new())
1099 }
1100
1101 pub fn shrink_to_fit(mut self) -> Self {
1110 self = Self(
1111 self.0
1112 .min_size(0., 0.)
1113 .size_full()
1114 .flex_grow(1.)
1115 .flex_basis(0.),
1116 );
1117 self
1118 }
1119
1120 pub fn handle_background(mut self, color: impl Into<Brush>) -> Self {
1122 self = Self(self.0.class(Handle, |s| s.background(color.into())));
1123 self
1124 }
1125
1126 pub fn handle_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1128 self = Self(self.0.class(Handle, |s| s.border_radius(border_radius)));
1129 self
1130 }
1131
1132 pub fn handle_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1134 self = Self(self.0.class(Handle, |s| s.border_color(border_color)));
1135 self
1136 }
1137
1138 pub fn handle_border(mut self, border: impl Into<Px>) -> Self {
1140 self = Self(self.0.class(Handle, |s| s.set(Border, border)));
1141 self
1142 }
1143
1144 pub fn handle_rounded(mut self, rounded: impl Into<bool>) -> Self {
1146 self = Self(self.0.class(Handle, |s| s.set(Rounded, rounded)));
1147 self
1148 }
1149
1150 pub fn track_background(mut self, color: impl Into<Brush>) -> Self {
1152 self = Self(self.0.class(Track, |s| s.background(color.into())));
1153 self
1154 }
1155
1156 pub fn track_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1158 self = Self(self.0.class(Track, |s| s.border_radius(border_radius)));
1159 self
1160 }
1161
1162 pub fn track_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1164 self = Self(self.0.class(Track, |s| s.border_color(border_color)));
1165 self
1166 }
1167
1168 pub fn track_border(mut self, border: impl Into<Px>) -> Self {
1170 self = Self(self.0.class(Track, |s| s.set(Border, border)));
1171 self
1172 }
1173
1174 pub fn track_rounded(mut self, rounded: impl Into<bool>) -> Self {
1176 self = Self(self.0.class(Track, |s| s.set(Rounded, rounded)));
1177 self
1178 }
1179
1180 pub fn vertical_track_inset(mut self, inset: impl Into<Px>) -> Self {
1182 self = Self(self.0.set(VerticalInset, inset));
1183 self
1184 }
1185
1186 pub fn horizontal_track_inset(mut self, inset: impl Into<Px>) -> Self {
1188 self = Self(self.0.set(HorizontalInset, inset));
1189 self
1190 }
1191
1192 pub fn hide_bars(mut self, hide: impl Into<bool>) -> Self {
1194 self = Self(self.0.set(HideBars, hide));
1195 self
1196 }
1197
1198 pub fn propagate_pointer_wheel(mut self, propagate: impl Into<bool>) -> Self {
1200 self = Self(self.0.set(PropagatePointerWheel, propagate));
1201 self
1202 }
1203
1204 pub fn vertical_scroll_as_horizontal(mut self, vert_as_horiz: impl Into<bool>) -> Self {
1206 self = Self(self.0.set(VerticalScrollAsHorizontal, vert_as_horiz));
1207 self
1208 }
1209
1210 pub fn show_bars_when_idle(mut self, show: impl Into<bool>) -> Self {
1212 self = Self(self.0.set(ShowBarsWhenIdle, show));
1213 self
1214 }
1215}
1216
1217pub trait ScrollExt {
1219 fn scroll(self) -> Scroll;
1221}
1222
1223impl<T: IntoView + 'static> ScrollExt for T {
1224 fn scroll(self) -> Scroll {
1225 Scroll::new(self)
1226 }
1227}