use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_render::{Color, DrawCommand};
use rosace_scroll::ScrollPhysics;
use super::{avail_h, avail_w, intersect_rect, BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
const TRIGGER_DISTANCE: f32 = 70.0;
const INDICATOR_SIZE: f32 = 32.0;
const INDICATOR_TOP_MARGIN: f32 = 16.0;
const PHYSICS: ScrollPhysics = ScrollPhysics::Bounce { friction: 0.88, spring_stiffness: 12.0 };
pub struct PullToRefresh {
child: BoxedWidget,
on_refresh: Option<Arc<dyn Fn() + Send + Sync>>,
refreshing: bool,
color: Option<Color>,
}
impl PullToRefresh {
pub fn new(child: impl Widget + 'static) -> Self {
Self { child: Box::new(child), on_refresh: None, refreshing: false, color: None }
}
pub fn on_refresh(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.on_refresh = Some(Arc::new(f));
self
}
pub fn refreshing(mut self, v: bool) -> Self {
self.refreshing = v;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
}
impl Widget for PullToRefresh {
fn children(&self) -> Children<'_> {
Children::One(&*self.child)
}
fn layout(&self, ctx: &LayoutCtx) -> Size {
Size { width: avail_w(ctx.constraints), height: avail_h(ctx.constraints) }
}
fn paint(&self, ctx: &mut PaintCtx) {
let r = ctx.rect;
let color = self.color.unwrap_or_else(|| ctx.tc(ctx.theme.colors.primary));
let ctrl = ctx.scroll_controller();
let drag_ctrl = ctrl.clone();
ctx.register_nested_scroll(move |_dx, dy| drag_ctrl.try_apply_delta(0.0, -dy, PHYSICS));
let dt = rosace_animate::frame_dt().max(0.0001);
let is_pressed = ctx.pressed();
let was_pressed = ctrl.was_pressed();
if is_pressed {
ctrl.track_velocity(dt);
} else {
if was_pressed { ctrl.end_drag(); }
if ctrl.coast(PHYSICS, dt) {
ctx.request_animation();
}
}
let released_this_frame = was_pressed && !is_pressed;
ctrl.set_was_pressed(is_pressed);
let pull = (-ctrl.offset.get()[1]).max(0.0);
if released_this_frame && !self.refreshing && pull >= TRIGGER_DISTANCE {
if let Some(cb) = &self.on_refresh {
cb();
}
}
let child_rect = Rect {
origin: Point { x: r.origin.x, y: r.origin.y + pull },
size: r.size,
};
ctx.record(DrawCommand::PushClip { rect: r });
let effective_clip = ctx.clip_rect.and_then(|p| intersect_rect(p, r)).unwrap_or(r);
let mut child_ctx = ctx.child(child_rect);
child_ctx.clip_rect = Some(effective_clip);
self.child.paint(&mut child_ctx);
ctx.record(DrawCommand::PopClip);
if self.refreshing {
let cx = r.origin.x + r.size.width / 2.0;
let cy = r.origin.y + INDICATOR_TOP_MARGIN + INDICATOR_SIZE / 2.0;
draw_indicator(ctx, Point { x: cx, y: cy }, None, color);
ctx.request_animation();
} else if pull > 0.0 {
let progress = (pull / TRIGGER_DISTANCE).min(1.0);
let travel = pull.min(TRIGGER_DISTANCE + INDICATOR_TOP_MARGIN);
let cx = r.origin.x + r.size.width / 2.0;
let cy = r.origin.y - INDICATOR_SIZE / 2.0 + travel;
draw_indicator(ctx, Point { x: cx, y: cy }, Some(progress), color);
}
}
}
fn draw_indicator(ctx: &mut PaintCtx, center: Point, progress: Option<f32>, color: Color) {
const THICKNESS: f32 = 3.0;
let radius = (INDICATOR_SIZE - THICKNESS) / 2.0;
let track = Color::rgba(color.r, color.g, color.b, 40);
ctx.fill_arc(center, radius, THICKNESS, 0.0, 360.0, track);
match progress {
Some(p) if p > 0.0 => {
ctx.fill_arc(center, radius, THICKNESS, -90.0, 360.0 * p, color);
}
Some(_) => {}
None => {
let t = super::anim_clock();
let start = (t * 360.0) % 360.0;
ctx.fill_arc(center, radius, THICKNESS, start, 270.0, color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
struct Filler;
impl Widget for Filler {
fn layout(&self, ctx: &LayoutCtx) -> Size {
Size { width: avail_w(ctx.constraints), height: 2000.0 }
}
fn paint(&self, _ctx: &mut PaintCtx) {}
}
fn test_env() -> (rosace_render::FontCache, rosace_theme::ThemeData) {
(rosace_render::FontCache::embedded(), rosace_theme::built_in::dark_theme())
}
#[test]
fn fills_available_space() {
let w = PullToRefresh::new(Filler);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::tight(390.0, 800.0), &font, &theme);
let size = w.layout(&ctx);
assert_eq!((size.width, size.height), (390.0, 800.0));
}
#[test]
fn builders_set_state() {
let w = PullToRefresh::new(Filler).refreshing(true).color(Color::rgb(1, 2, 3));
assert!(w.refreshing);
assert_eq!(w.color, Some(Color::rgb(1, 2, 3)));
}
}