use rosace_render::SkiaCanvas;
use crate::{
controller::ScrollController,
physics::{MomentumState, ScrollDirection, ScrollPhysics},
scrollbar::render_scrollbar,
};
pub struct ScrollView {
pub direction: ScrollDirection,
pub physics: ScrollPhysics,
pub show_scrollbar: bool,
pub controller: ScrollController,
pub content_width: f32,
pub content_height: f32,
pub viewport_width: f32,
pub viewport_height: f32,
momentum: MomentumState,
scrollbar_opacity: f32,
scrollbar_fade_timer: f32,
}
impl ScrollView {
pub fn new(viewport_w: f32, viewport_h: f32) -> Self {
let controller = ScrollController::new();
controller.viewport_size.set([viewport_w, viewport_h]);
Self {
direction: ScrollDirection::Vertical,
physics: ScrollPhysics::default(),
show_scrollbar: true,
controller,
content_width: viewport_w,
content_height: viewport_h,
viewport_width: viewport_w,
viewport_height: viewport_h,
momentum: MomentumState::new(),
scrollbar_opacity: 0.0,
scrollbar_fade_timer: 0.0,
}
}
pub fn direction(mut self, d: ScrollDirection) -> Self {
self.direction = d;
self
}
pub fn physics(mut self, p: ScrollPhysics) -> Self {
self.physics = p;
self
}
pub fn show_scrollbar(mut self, s: bool) -> Self {
self.show_scrollbar = s;
self
}
pub fn content_size(mut self, w: f32, h: f32) -> Self {
self.content_width = w;
self.content_height = h;
self.controller.content_size.set([w, h]);
self
}
pub fn tick(&mut self, dt: f32) {
let (dx, dy) = self.momentum.tick(self.physics);
if dx.abs() > 0.0 || dy.abs() > 0.0 {
self.controller.scroll_by(dx, dy);
self.scrollbar_opacity = 1.0;
self.scrollbar_fade_timer = 1.5;
}
if self.scrollbar_fade_timer > 0.0 {
self.scrollbar_fade_timer -= dt;
if self.scrollbar_fade_timer <= 0.0 {
self.scrollbar_opacity = (self.scrollbar_opacity - dt * 2.0).max(0.0);
}
}
}
pub fn on_scroll(&mut self, dx: f32, dy: f32) {
self.momentum.push(dx, dy);
self.controller.scroll_by(dx, dy);
self.scrollbar_opacity = 1.0;
self.scrollbar_fade_timer = 1.5;
}
pub fn render_chrome(&self, canvas: &mut SkiaCanvas, x: f32, y: f32) {
if !self.show_scrollbar {
return;
}
let [ox, oy] = self.controller.offset();
match self.direction {
ScrollDirection::Vertical | ScrollDirection::Both => {
render_scrollbar(
canvas,
ScrollDirection::Vertical,
x,
y,
self.viewport_width,
self.viewport_height,
oy,
self.content_height,
self.scrollbar_opacity,
);
}
ScrollDirection::Horizontal => {
render_scrollbar(
canvas,
ScrollDirection::Horizontal,
x,
y,
self.viewport_width,
self.viewport_height,
ox,
self.content_width,
self.scrollbar_opacity,
);
}
}
}
pub fn scroll_offset(&self) -> [f32; 2] {
self.controller.offset()
}
pub fn controller(&self) -> &ScrollController {
&self.controller
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scroll_view_on_scroll_updates_offset() {
let mut sv = ScrollView::new(300.0, 400.0).content_size(300.0, 1200.0);
sv.on_scroll(0.0, 50.0);
let [_x, y] = sv.scroll_offset();
assert_eq!(y, 50.0);
}
#[test]
fn scroll_view_on_scroll_clamps_to_content_bounds() {
let mut sv = ScrollView::new(300.0, 400.0).content_size(300.0, 600.0);
sv.on_scroll(0.0, 9999.0);
let [_x, y] = sv.scroll_offset();
assert_eq!(y, 200.0);
}
#[test]
fn scroll_view_tick_advances_momentum() {
let mut sv = ScrollView::new(300.0, 400.0)
.content_size(300.0, 2000.0)
.physics(ScrollPhysics::Momentum { friction: 0.92 });
sv.on_scroll(0.0, 100.0);
let before = sv.scroll_offset()[1];
sv.tick(0.016);
let after = sv.scroll_offset()[1];
assert!(after >= before);
}
#[test]
fn scroll_view_render_chrome_does_not_panic() {
let mut canvas = SkiaCanvas::new(300, 400);
canvas.clear(rosace_render::Color::WHITE);
let sv = ScrollView::new(300.0, 400.0).content_size(300.0, 800.0);
sv.render_chrome(&mut canvas, 0.0, 0.0);
}
#[test]
fn scroll_view_controller_accessible() {
let sv = ScrollView::new(300.0, 400.0).content_size(300.0, 800.0);
let c = sv.controller();
assert_eq!(c.offset(), [0.0, 0.0]);
}
}