1use std::time::Duration;
26
27use frust_core::{EventCtx, FrameTime, PaintCtx, PaintScene, PointerEvent, PointerPhase};
28use frust_theme::Theme;
29use kurbo::{Point, Rect, Size};
30use peniko::Color;
31
32use crate::authoring::presses;
33use crate::scroll::ScrollInfo;
34
35const THICKNESS: f64 = 6.0;
39const HOVER_THICKNESS: f64 = 10.0;
41const MIN_THUMB: f64 = 24.0;
43const INSET: f64 = 2.0;
45const FADE_DELAY_MS: f64 = 800.0;
47const FADE_DURATION_MS: f64 = 250.0;
49const FALLBACK_INK: Color = Color::from_rgb8(0x5C, 0x5C, 0x5C);
51const THUMB_ALPHA: f32 = 0.5;
53const THUMB_ACTIVE_ALPHA: f32 = 0.75;
54const TRACK_ALPHA: f32 = 0.12;
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum ScrollbarShow {
60 Never,
62 #[default]
65 WhileScrolling,
66 Hover,
68 Always,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct ScrollbarStyle {
86 pub thickness: f64,
88 pub hover_thickness: f64,
90 pub min_thumb: f64,
92 pub inset: f64,
94 pub fade_delay_ms: f64,
96 pub fade_duration_ms: f64,
98 pub show: ScrollbarShow,
100}
101
102impl Default for ScrollbarStyle {
103 fn default() -> Self {
104 Self {
105 thickness: THICKNESS,
106 hover_thickness: HOVER_THICKNESS,
107 min_thumb: MIN_THUMB,
108 inset: INSET,
109 fade_delay_ms: FADE_DELAY_MS,
110 fade_duration_ms: FADE_DURATION_MS,
111 show: ScrollbarShow::default(),
112 }
113 }
114}
115
116impl ScrollbarStyle {
117 pub fn new(show: ScrollbarShow) -> Self {
119 Self::default().show(show)
120 }
121
122 pub fn show(mut self, show: ScrollbarShow) -> Self {
124 self.show = show;
125 self
126 }
127
128 pub fn thickness(mut self, px: f64) -> Self {
130 self.thickness = px;
131 self
132 }
133
134 pub fn hover_thickness(mut self, px: f64) -> Self {
136 self.hover_thickness = px;
137 self
138 }
139
140 pub fn min_thumb(mut self, px: f64) -> Self {
142 self.min_thumb = px;
143 self
144 }
145
146 pub fn inset(mut self, px: f64) -> Self {
148 self.inset = px;
149 self
150 }
151
152 pub fn fade_delay_ms(mut self, ms: f64) -> Self {
154 self.fade_delay_ms = ms;
155 self
156 }
157
158 pub fn fade_duration_ms(mut self, ms: f64) -> Self {
160 self.fade_duration_ms = ms;
161 self
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
167pub(crate) struct Scrollbar {
168 pub(crate) lane: Rect,
170 pub(crate) track: Rect,
172 pub(crate) thumb: Rect,
174 travel: f64,
176 max_offset: f64,
177}
178
179impl Scrollbar {
180 pub(crate) fn new(
183 style: &ScrollbarStyle,
184 size: Size,
185 info: ScrollInfo,
186 widened: bool,
187 ) -> Option<Self> {
188 let max_offset = info.max_offset;
189 let inset = style.inset.max(0.0);
190 let thickness = if widened {
191 style.hover_thickness
192 } else {
193 style.thickness
194 };
195 let track_len = size.height - 2.0 * inset;
196 if !(max_offset > 0.0 && track_len > 0.0 && thickness > 0.0 && size.width > 0.0) {
197 return None;
198 }
199 let right = size.width - inset;
200 let track = Rect::new(right - thickness, inset, right, inset + track_len);
201 let lane_width = style.hover_thickness.max(style.thickness) + 2.0 * inset;
202 let lane = Rect::new(
203 (size.width - lane_width).max(0.0),
204 0.0,
205 size.width,
206 size.height,
207 );
208 let share = size.height / (size.height + max_offset);
209 let thumb_len = (track_len * share)
210 .max(style.min_thumb.min(track_len))
211 .min(track_len);
212 let travel = track_len - thumb_len;
213 let progress = (info.offset / max_offset).clamp(0.0, 1.0);
214 let top = inset + travel * progress;
215 let thumb = Rect::new(track.x0, top, track.x1, top + thumb_len);
216 Some(Self {
217 lane,
218 track,
219 thumb,
220 travel,
221 max_offset,
222 })
223 }
224
225 pub(crate) fn offset_for_drag(&self, start_offset: f64, dy: f64) -> f64 {
228 if self.travel <= 0.0 {
229 return start_offset;
230 }
231 (start_offset + dy * self.max_offset / self.travel).clamp(0.0, self.max_offset)
232 }
233
234 fn hits_lane(&self, p: Point) -> bool {
235 self.lane.contains(p)
236 }
237
238 fn hits_thumb(&self, p: Point) -> bool {
240 self.hits_lane(p) && p.y >= self.thumb.y0 && p.y < self.thumb.y1
241 }
242}
243
244#[derive(Clone, Copy, Debug, PartialEq)]
246pub(crate) enum BarEvent {
247 Pass,
249 Handled,
251 JumpTo(f64),
253}
254
255#[derive(Clone, Copy, Debug, PartialEq)]
257enum Press {
258 Thumb { start_y: f64, start_offset: f64 },
260 Track,
262}
263
264const MAX_HOLD_SECS: f64 = 3600.0;
267
268const FADE_FRAME_SECS: f64 = 1.0 / 60.0;
270
271const LEVEL: f64 = 1.0 / 255.0;
273
274#[derive(Clone, Copy, Debug, PartialEq)]
280enum Wake {
281 Idle,
282 Hold(Duration),
283 Fade,
284}
285
286pub(crate) struct ScrollbarState {
288 style: ScrollbarStyle,
289 lane_hovered: bool,
290 surface_hovered: bool,
291 press: Option<Press>,
292 last_active: Option<FrameTime>,
294 last_position: Option<(f64, f64)>,
296 stamp_pending: bool,
298 opacity: f64,
300 #[cfg(test)]
302 last_wake: Option<Wake>,
303}
304
305impl ScrollbarState {
306 pub(crate) fn new(style: ScrollbarStyle) -> Self {
307 Self {
308 style,
309 lane_hovered: false,
310 surface_hovered: false,
311 press: None,
312 last_active: None,
313 last_position: None,
314 stamp_pending: false,
315 opacity: 0.0,
316 #[cfg(test)]
317 last_wake: None,
318 }
319 }
320
321 pub(crate) fn set_style(&mut self, style: ScrollbarStyle) -> bool {
323 let changed = self.style != style;
324 self.style = style;
325 changed
326 }
327
328 pub(crate) fn is_widened(&self) -> bool {
329 self.lane_hovered || self.press.is_some()
330 }
331
332 fn engaged(&self) -> bool {
334 self.is_widened() || (self.style.show == ScrollbarShow::Hover && self.surface_hovered)
335 }
336
337 #[cfg(test)]
339 pub(crate) fn opacity(&self) -> f64 {
340 self.opacity
341 }
342
343 pub(crate) fn pointer(
346 &mut self,
347 ctx: &mut EventCtx,
348 p: &PointerEvent,
349 info: ScrollInfo,
350 can_drive: bool,
351 ) -> BarEvent {
352 let size = ctx.size();
353 match p.phase {
354 PointerPhase::Down => {
355 self.press = None;
356 if !presses(p) || !can_drive || self.opacity <= 0.0 {
357 return BarEvent::Pass;
358 }
359 let Some(bar) = Scrollbar::new(&self.style, size, info, true) else {
360 return BarEvent::Pass;
361 };
362 if !bar.hits_lane(p.position) {
363 return BarEvent::Pass;
364 }
365 ctx.capture_pointer();
366 ctx.request_redraw();
367 self.stamp_pending = true;
368 if bar.hits_thumb(p.position) {
369 self.press = Some(Press::Thumb {
370 start_y: p.position.y,
371 start_offset: info.offset,
372 });
373 return BarEvent::JumpTo(info.offset);
374 }
375 self.press = Some(Press::Track);
376 let page = if p.position.y < bar.thumb.y0 {
377 -size.height
378 } else {
379 size.height
380 };
381 BarEvent::JumpTo((info.offset + page).clamp(0.0, info.max_offset))
382 }
383 PointerPhase::Move => match self.press {
384 Some(Press::Thumb {
385 start_y,
386 start_offset,
387 }) => {
388 ctx.request_redraw();
389 match Scrollbar::new(&self.style, size, info, true) {
390 Some(bar) => BarEvent::JumpTo(
391 bar.offset_for_drag(start_offset, p.position.y - start_y),
392 ),
393 None => BarEvent::Handled,
394 }
395 }
396 Some(Press::Track) => BarEvent::Handled,
397 None => BarEvent::Pass,
398 },
399 PointerPhase::Up | PointerPhase::Cancel => {
400 if self.press.take().is_none() {
401 return BarEvent::Pass;
402 }
403 self.stamp_pending = true;
404 ctx.request_redraw();
405 BarEvent::Handled
406 }
407 }
408 }
409
410 pub(crate) fn hover(&mut self, ctx: &mut EventCtx, position: Point, info: ScrollInfo) {
414 let size = ctx.size();
415 let in_surface = Rect::from_origin_size(Point::ZERO, size).contains(position);
416 let in_lane = Scrollbar::new(&self.style, size, info, true)
417 .is_some_and(|bar| bar.hits_lane(position));
418 let hover_mode = self.style.show == ScrollbarShow::Hover;
419 if in_lane || (hover_mode && in_surface) {
420 ctx.claim_hover();
421 }
422 let was_engaged = self.engaged();
423 let lane_changed = self.lane_hovered != in_lane;
424 let surface_changed = self.surface_hovered != in_surface;
425 self.lane_hovered = in_lane;
426 self.surface_hovered = in_surface;
427 if was_engaged != self.engaged() {
428 self.stamp_pending = true;
429 }
430 if lane_changed || (hover_mode && surface_changed) {
431 ctx.request_redraw();
432 }
433 }
434
435 fn visibility(&self, now: FrameTime, reduce_motion: bool) -> (f64, Wake) {
441 match self.style.show {
442 ScrollbarShow::Always => return (1.0, Wake::Idle),
443 ScrollbarShow::Never => return (0.0, Wake::Idle),
444 ScrollbarShow::WhileScrolling | ScrollbarShow::Hover => {}
445 }
446 if self.engaged() {
447 return (1.0, Wake::Idle);
448 }
449 let Some(active) = self.last_active else {
450 return (0.0, Wake::Idle);
451 };
452 let elapsed = now.saturating_sub(active).as_secs_f64() * 1000.0;
453 let delay = self.style.fade_delay_ms.max(0.0);
454 if !delay.is_finite() {
455 return (1.0, Wake::Idle); }
457 if elapsed < delay {
458 let remaining = ((delay - elapsed) / 1000.0).min(MAX_HOLD_SECS);
459 return (1.0, Wake::Hold(Duration::from_secs_f64(remaining)));
460 }
461 let duration = self.style.fade_duration_ms;
462 if reduce_motion || duration.is_nan() || duration <= 0.0 {
463 return (0.0, Wake::Idle);
464 }
465 if !duration.is_finite() {
466 return (1.0, Wake::Idle); }
468 let t = (elapsed - delay) / duration;
469 if t >= 1.0 {
470 (0.0, Wake::Idle)
471 } else {
472 let opacity = 1.0 - t;
473 if FADE_FRAME_SECS * 1000.0 / duration >= LEVEL {
474 return (opacity, Wake::Fade);
475 }
476 let levels = opacity * 255.0;
477 let next = levels.ceil() - 1.0;
478 let ms = ((levels - next) / 255.0 * duration).max(1.0);
479 let secs = (ms / 1000.0).min(MAX_HOLD_SECS);
480 (opacity, Wake::Hold(Duration::from_secs_f64(secs)))
481 }
482 }
483
484 pub(crate) fn paint(
486 &mut self,
487 ctx: &mut PaintCtx,
488 scene: &mut dyn PaintScene,
489 info: ScrollInfo,
490 reduce_motion: bool,
491 ) {
492 let was_engaged = self.engaged();
493 if !ctx.is_hovered() {
494 self.lane_hovered = false;
495 self.surface_hovered = false;
496 }
497 let now = ctx.frame_time();
498 let position = (info.offset, info.overscroll);
499 let moved = self.last_position.is_some_and(|last| last != position);
500 self.last_position = Some(position);
501 let pending = std::mem::take(&mut self.stamp_pending);
502 if moved || pending || was_engaged || self.engaged() {
503 self.last_active = Some(now);
504 }
505 let (opacity, wake) = self.visibility(now, reduce_motion);
506 self.opacity = opacity;
507 #[cfg(test)]
508 {
509 self.last_wake = Some(wake);
510 }
511 match wake {
512 Wake::Idle => {}
513 Wake::Hold(remaining) => ctx.request_frame_paced_at(remaining),
514 Wake::Fade => ctx.request_frame(),
515 }
516 if opacity <= 0.0 {
517 return;
518 }
519 let widened = self.is_widened();
520 let Some(bar) = Scrollbar::new(&self.style, ctx.size(), info, widened) else {
521 return;
522 };
523 let ink =
524 Theme::from_paint_ctx(ctx).map_or(FALLBACK_INK, |t| t.scheme().on_surface_variant);
525 let shift = ctx.origin().to_vec2();
526 let radius = bar.track.width() / 2.0;
527 let opacity = opacity as f32;
528 if widened {
529 let track = with_alpha(ink, TRACK_ALPHA * opacity);
530 scene.fill_rounded_rect(bar.track.origin() + shift, bar.track.size(), radius, track);
531 }
532 let alpha = if widened {
533 THUMB_ACTIVE_ALPHA
534 } else {
535 THUMB_ALPHA
536 };
537 let thumb = with_alpha(ink, alpha * opacity);
538 scene.fill_rounded_rect(bar.thumb.origin() + shift, bar.thumb.size(), radius, thumb);
539 }
540}
541
542fn with_alpha(color: Color, alpha: f32) -> Color {
544 let c = color.components;
545 Color::new([c[0], c[1], c[2], alpha])
546}
547
548#[cfg(test)]
549pub(crate) mod tests {
550 use std::any::Any;
551
552 use frust_core::PointerButton;
553
554 use super::*;
555
556 #[derive(Default)]
558 pub(crate) struct BarScene {
559 pub(crate) rounded: Vec<(Point, Size, Color)>,
560 }
561
562 impl PaintScene for BarScene {
563 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
564 fn draw_text(&mut self, _origin: Point, _text: &str) {}
565 fn fill_rounded_rect(&mut self, origin: Point, size: Size, _radius: f64, color: Color) {
566 self.rounded.push((origin, size, color));
567 }
568 }
569
570 const VIEW: Size = Size::new(200.0, 100.0);
571
572 fn info(offset: f64, max_offset: f64) -> ScrollInfo {
573 ScrollInfo {
574 offset,
575 max_offset,
576 overscroll: 0.0,
577 }
578 }
579
580 fn at(ms: u64) -> FrameTime {
581 FrameTime::from_nanos(ms * 1_000_000)
582 }
583
584 fn pointer(phase: PointerPhase, x: f64, y: f64) -> PointerEvent {
585 PointerEvent {
586 phase,
587 position: Point::new(x, y),
588 button: PointerButton::Primary,
589 }
590 }
591
592 fn send(state: &mut ScrollbarState, p: PointerEvent, info: ScrollInfo) -> (BarEvent, bool) {
593 let mut unit = ();
594 let any: &mut dyn Any = &mut unit;
595 let mut ctx = EventCtx::new(any, Point::ZERO, VIEW);
596 let result = state.pointer(&mut ctx, &p, info, true);
597 (result, ctx.is_pointer_captured())
598 }
599
600 fn hover(state: &mut ScrollbarState, x: f64, y: f64, info: ScrollInfo) -> bool {
601 let mut unit = ();
602 let any: &mut dyn Any = &mut unit;
603 let mut ctx = EventCtx::new(any, Point::ZERO, VIEW);
604 state.hover(&mut ctx, Point::new(x, y), info);
605 ctx.needs_redraw()
606 }
607
608 fn paint(state: &mut ScrollbarState, ms: u64, info: ScrollInfo, reduce: bool) -> (usize, bool) {
610 let mut ctx = PaintCtx::for_test(Point::ZERO, VIEW, at(ms));
611 let mut scene = BarScene::default();
612 state.paint(&mut ctx, &mut scene, info, reduce);
613 (scene.rounded.len(), ctx.needs_frame())
614 }
615
616 #[test]
617 fn the_thumb_stays_in_the_track_and_tracks_progress() {
618 let style = ScrollbarStyle::default();
619 for (offset, max) in [
620 (0.0, 900.0),
621 (450.0, 900.0),
622 (900.0, 900.0),
623 (5000.0, 900.0),
624 ] {
625 let bar = Scrollbar::new(&style, VIEW, info(offset, max), false).unwrap();
626 assert!(bar.thumb.y0 >= bar.track.y0 && bar.thumb.y1 <= bar.track.y1);
627 assert_eq!(bar.track.height(), VIEW.height - 2.0 * INSET);
628 assert_eq!(bar.track.x1, VIEW.width - INSET);
629 }
630 let top = Scrollbar::new(&style, VIEW, info(-50.0, 900.0), false).unwrap();
631 assert_eq!(top.thumb.y0, top.track.y0, "progress clamps at the top");
632 let end = Scrollbar::new(&style, VIEW, info(5000.0, 900.0), false).unwrap();
633 assert_eq!(end.thumb.y1, end.track.y1, "and at the bottom");
634 }
635
636 #[test]
637 fn the_thumb_is_proportional_with_a_minimum() {
638 let style = ScrollbarStyle::default();
639 let half = Scrollbar::new(&style, VIEW, info(0.0, 100.0), false).unwrap();
640 assert_eq!(half.thumb.height(), half.track.height() / 2.0);
641 let long = Scrollbar::new(&style, VIEW, info(0.0, 100_000.0), false).unwrap();
642 assert_eq!(long.thumb.height(), MIN_THUMB);
643 let tiny = Scrollbar::new(&style.min_thumb(500.0), VIEW, info(0.0, 10.0), false).unwrap();
644 assert_eq!(
645 tiny.thumb.height(),
646 tiny.track.height(),
647 "min clamps to the track"
648 );
649 assert!(Scrollbar::new(&style, VIEW, info(0.0, 0.0), false).is_none());
650 }
651
652 #[test]
653 fn widening_thickens_the_bar_without_moving_the_thumb() {
654 let style = ScrollbarStyle::default();
655 let rest = Scrollbar::new(&style, VIEW, info(300.0, 900.0), false).unwrap();
656 let wide = Scrollbar::new(&style, VIEW, info(300.0, 900.0), true).unwrap();
657 assert_eq!(rest.track.width(), THICKNESS);
658 assert_eq!(wide.track.width(), HOVER_THICKNESS);
659 assert_eq!(
660 (rest.thumb.y0, rest.thumb.y1),
661 (wide.thumb.y0, wide.thumb.y1)
662 );
663 }
664
665 #[test]
666 fn a_thumb_drag_maps_travel_to_offset() {
667 let mut state = ScrollbarState::new(ScrollbarStyle::new(ScrollbarShow::Always));
668 paint(&mut state, 0, info(0.0, 900.0), false);
669 let bar = Scrollbar::new(&state.style, VIEW, info(0.0, 900.0), true).unwrap();
670 let (down, captured) = send(
671 &mut state,
672 pointer(PointerPhase::Down, 195.0, 5.0),
673 info(0.0, 900.0),
674 );
675 assert_eq!(
676 down,
677 BarEvent::JumpTo(0.0),
678 "the press halts where it landed"
679 );
680 assert!(captured);
681 let half = bar.travel / 2.0;
682 let (moved, _) = send(
683 &mut state,
684 pointer(PointerPhase::Move, 150.0, 5.0 + half),
685 info(0.0, 900.0),
686 );
687 assert_eq!(moved, BarEvent::JumpTo(450.0));
688 let (past, _) = send(
689 &mut state,
690 pointer(PointerPhase::Move, 150.0, 900.0),
691 info(0.0, 900.0),
692 );
693 assert_eq!(past, BarEvent::JumpTo(900.0), "clamped to the range");
694 let (up, _) = send(
695 &mut state,
696 pointer(PointerPhase::Up, 150.0, 900.0),
697 info(0.0, 900.0),
698 );
699 assert_eq!(up, BarEvent::Handled);
700 let (after, _) = send(
701 &mut state,
702 pointer(PointerPhase::Move, 150.0, 50.0),
703 info(0.0, 900.0),
704 );
705 assert_eq!(after, BarEvent::Pass, "the release ends the drag");
706 }
707
708 #[test]
709 fn a_track_press_pages_by_one_viewport() {
710 let mut state = ScrollbarState::new(ScrollbarStyle::new(ScrollbarShow::Always));
711 let at_mid = info(400.0, 900.0);
712 paint(&mut state, 0, at_mid, false);
713 let (below, _) = send(&mut state, pointer(PointerPhase::Down, 195.0, 98.0), at_mid);
714 assert_eq!(below, BarEvent::JumpTo(400.0 + VIEW.height));
715 let (moved, _) = send(&mut state, pointer(PointerPhase::Move, 195.0, 10.0), at_mid);
716 assert_eq!(moved, BarEvent::Handled, "a track press swallows its moves");
717 send(&mut state, pointer(PointerPhase::Up, 195.0, 10.0), at_mid);
718 let (above, _) = send(&mut state, pointer(PointerPhase::Down, 195.0, 3.0), at_mid);
719 assert_eq!(above, BarEvent::JumpTo(400.0 - VIEW.height));
720 }
721
722 #[test]
723 fn presses_off_the_lane_or_non_primary_pass_through() {
724 let mut state = ScrollbarState::new(ScrollbarStyle::new(ScrollbarShow::Always));
725 paint(&mut state, 0, info(0.0, 900.0), false);
726 let (content, captured) = send(
727 &mut state,
728 pointer(PointerPhase::Down, 50.0, 5.0),
729 info(0.0, 900.0),
730 );
731 assert_eq!(content, BarEvent::Pass);
732 assert!(!captured);
733 let mut secondary = pointer(PointerPhase::Down, 195.0, 5.0);
734 secondary.button = PointerButton::Secondary;
735 assert_eq!(
736 send(&mut state, secondary, info(0.0, 900.0)).0,
737 BarEvent::Pass
738 );
739 }
740
741 #[test]
742 fn hovering_the_lane_widens_and_redraws_only_on_change() {
743 let mut state = ScrollbarState::new(ScrollbarStyle::default());
744 assert!(
745 !hover(&mut state, 50.0, 50.0, info(0.0, 900.0)),
746 "content hover: no redraw"
747 );
748 assert!(hover(&mut state, 195.0, 50.0, info(0.0, 900.0)));
749 assert!(state.is_widened());
750 assert!(
751 !hover(&mut state, 196.0, 52.0, info(0.0, 900.0)),
752 "still in the lane"
753 );
754 assert!(hover(&mut state, 50.0, 50.0, info(0.0, 900.0)));
755 assert!(!state.is_widened());
756 }
757
758 #[test]
759 fn while_scrolling_fades_after_the_delay_under_a_stepped_clock() {
760 let mut state = ScrollbarState::new(ScrollbarStyle::default());
761 assert_eq!(
762 paint(&mut state, 0, info(0.0, 900.0), false),
763 (0, false),
764 "idle: hidden"
765 );
766 assert_eq!(
767 paint(&mut state, 16, info(40.0, 900.0), false),
768 (1, true),
769 "a scroll shows it"
770 );
771 assert_eq!(state.opacity(), 1.0);
772 paint(&mut state, 16 + 700, info(40.0, 900.0), false);
773 assert_eq!(state.opacity(), 1.0, "still holding");
774 paint(&mut state, 16 + 800 + 125, info(40.0, 900.0), false);
775 assert!(
776 (state.opacity() - 0.5).abs() < 1e-9,
777 "halfway through the fade"
778 );
779 assert_eq!(
780 paint(&mut state, 16 + 800 + 250, info(40.0, 900.0), false),
781 (0, false)
782 );
783 assert_eq!(state.opacity(), 0.0);
784 }
785
786 #[test]
787 fn non_finite_fade_values_neither_panic_nor_spin() {
788 let hold = ScrollbarStyle::default().fade_delay_ms(f64::INFINITY);
789 let mut state = ScrollbarState::new(hold);
790 paint(&mut state, 0, info(0.0, 900.0), false);
791 assert_eq!(
792 paint(&mut state, 16, info(40.0, 900.0), false),
793 (1, false),
794 "an infinite delay holds the bar with no wake"
795 );
796 paint(&mut state, 16 + 100_000, info(40.0, 900.0), false);
797 assert_eq!(state.opacity(), 1.0);
798
799 let huge = ScrollbarStyle::default().fade_delay_ms(1e30);
800 let mut state = ScrollbarState::new(huge);
801 paint(&mut state, 0, info(0.0, 900.0), false);
802 let (drawn, _) = paint(&mut state, 16, info(40.0, 900.0), false);
803 assert_eq!(drawn, 1, "a huge delay holds without overflowing the wake");
804 assert_eq!(state.last_wake, Some(Wake::Hold(Duration::from_secs(3600))));
805
806 let forever = ScrollbarStyle::default().fade_duration_ms(f64::INFINITY);
807 let mut state = ScrollbarState::new(forever);
808 paint(&mut state, 0, info(0.0, 900.0), false);
809 paint(&mut state, 16, info(40.0, 900.0), false);
810 assert_eq!(
811 paint(&mut state, 16 + 800 + 500, info(40.0, 900.0), false),
812 (1, false),
813 "an endless fade stays opaque and asks for no frame"
814 );
815 assert_eq!(state.opacity(), 1.0);
816
817 let slow = ScrollbarStyle::default().fade_duration_ms(1e9);
818 let mut state = ScrollbarState::new(slow);
819 paint(&mut state, 0, info(0.0, 900.0), false);
820 paint(&mut state, 16, info(40.0, 900.0), false);
821 paint(&mut state, 16 + 801, info(40.0, 900.0), false);
822 assert!(state.opacity() >= 0.99);
823 assert!(
824 matches!(state.last_wake, Some(Wake::Hold(d)) if d > Duration::ZERO),
825 "a sub-level fade step holds instead of spinning: {:?}",
826 state.last_wake
827 );
828
829 let normal = ScrollbarStyle::default().fade_duration_ms(250.0);
830 let mut state = ScrollbarState::new(normal);
831 paint(&mut state, 0, info(0.0, 900.0), false);
832 paint(&mut state, 16, info(40.0, 900.0), false);
833 paint(&mut state, 16 + 801 + 50, info(40.0, 900.0), false);
834 assert_eq!(state.last_wake, Some(Wake::Fade));
835 }
836
837 #[test]
838 fn reduce_motion_snaps_the_bar_out_after_the_delay() {
839 let mut state = ScrollbarState::new(ScrollbarStyle::default());
840 paint(&mut state, 0, info(0.0, 900.0), true);
841 paint(&mut state, 16, info(40.0, 900.0), true);
842 assert_eq!(state.opacity(), 1.0);
843 assert_eq!(
844 paint(&mut state, 16 + 801, info(40.0, 900.0), true),
845 (0, false)
846 );
847 }
848
849 #[test]
850 fn always_paints_and_a_hidden_bar_takes_no_press() {
851 let mut always = ScrollbarState::new(ScrollbarStyle::new(ScrollbarShow::Always));
852 assert_eq!(paint(&mut always, 0, info(0.0, 900.0), false), (1, false));
853 let mut idle = ScrollbarState::new(ScrollbarStyle::default());
854 paint(&mut idle, 0, info(0.0, 900.0), false);
855 let (result, captured) = send(
856 &mut idle,
857 pointer(PointerPhase::Down, 195.0, 5.0),
858 info(0.0, 900.0),
859 );
860 assert_eq!(result, BarEvent::Pass);
861 assert!(!captured);
862 }
863}