iris: drive animation from painter frame time

This commit is contained in:
iris committed 2026-09-12 21:37:47 -04:00
1 parent 5f4975285d
commit eb30a01f2d
14 files changed
+148 -116

No files matched your search

+12 -26
View File
@@ -26,7 +26,6 @@ pub struct UiData {
pub text: Rc<RefCell<TextResources>>, pub text: Rc<RefCell<TextResources>>,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
pub move_offsets: TrackedArena<MoveOffset, u32>, pub move_offsets: TrackedArena<MoveOffset, u32>,
animating: Vec<WidgetId>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -169,40 +168,27 @@ impl DerefMut for Ui {
} }
} }
impl UiData {
/// Ask for `id`'s [`crate::Widget::tick`] to run every frame until it
/// says it is done. Idempotent -- registering an already-animating
/// widget is the ordinary case (a second fling before the first
/// settled) and must not tick it twice per frame.
pub fn animate(&mut self, id: WidgetId) {
if !self.animating.contains(&id) {
self.animating.push(id);
}
}
pub fn tick_animations(&mut self, now: std::time::Instant) -> bool {
let mut registered = std::mem::take(&mut self.animating);
registered.retain(|&id| match self.widgets.get_dyn_mut(id) {
Some(widget) => widget.tick(now),
None => false,
});
for id in registered {
self.animate(id);
}
!self.animating.is_empty()
}
}
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &Ui; fn ui(&self) -> &Ui;
fn ui_mut(&mut self) -> &mut Ui; fn ui_mut(&mut self) -> &mut Ui;
fn draw<'a>(&mut self, root: impl Into<Option<&'a crate::StrongWidget>>) fn draw<'a>(&mut self, root: impl Into<Option<&'a crate::StrongWidget>>)
where
Self: Sized,
{
self.draw_at(root, std::time::Instant::now());
}
fn draw_at<'a>(
&mut self,
root: impl Into<Option<&'a crate::StrongWidget>>,
frame_time: std::time::Instant,
) -> bool
where where
Self: Sized, Self: Sized,
{ {
let render_state = self.ui().render_state.clone(); let render_state = self.ui().render_state.clone();
render_state.get_mut().update(root, self); render_state.get_mut().update_at(root, self, frame_time)
} }
#[allow(unused_variables)] #[allow(unused_variables)]
+16 -1
View File
@@ -9,7 +9,7 @@ use crate::{
ui::render_state::Retained, ui::render_state::Retained,
util::Vec2, util::Vec2,
}; };
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc, time::Instant};
pub struct Painter<'a> { pub struct Painter<'a> {
pub(super) render_state: &'a mut UiRenderState, pub(super) render_state: &'a mut UiRenderState,
@@ -52,6 +52,21 @@ impl DrawResult<'_, '_> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
/// The presentation time of this frame, supplied by the platform. On a
/// backend with a display clock this is the vsync timestamp, not the time
/// at which this widget happened to be drawn.
pub fn frame_time(&self) -> Instant {
self.render_state.frame_time
}
/// Redraw this widget on the following frame and ask the platform to
/// produce that frame. The invalidation is staged until the current
/// retained redraw has finished, so it cannot be consumed again in this
/// frame.
pub fn request_next_frame(&mut self) {
self.render_state.next_frame.insert(self.id);
}
pub fn set_size(&mut self, size: Size) { pub fn set_size(&mut self, size: Size) {
assert!( assert!(
self.size.replace(size).is_none(), self.size.replace(size).is_none(),
+26
View File
@@ -45,6 +45,11 @@ pub struct UiRenderState {
/// guard could never fire and the set grew by one entry per widget /// guard could never fire and the set grew by one entry per widget
/// ever drawn and was never emptied. /// ever drawn and was never emptied.
draw_started: HashSet<WidgetId>, draw_started: HashSet<WidgetId>,
/// Widgets which asked from inside `draw` to be drawn on the following
/// frame. Kept separate from `Widgets::needs_redraw` until `update_at`
/// finishes because `redraw_updates` drains that set to completion.
pub(super) next_frame: HashSet<WidgetId>,
pub(super) frame_time: Instant,
/// `Widget::draw` calls and `Primitives::region_mut` rewrites since the /// `Widget::draw` calls and `Primitives::region_mut` rewrites since the
/// last `take_counters`. LAYOUT.md section 8's pass conditions are /// last `take_counters`. LAYOUT.md section 8's pass conditions are
@@ -111,6 +116,8 @@ impl UiRenderState {
old_root: None, old_root: None,
resized: false, resized: false,
draw_started: Default::default(), draw_started: Default::default(),
next_frame: Default::default(),
frame_time: Instant::now(),
draw_count: 0, draw_count: 0,
region_mut_count: 0, region_mut_count: 0,
mov_count: 0, mov_count: 0,
@@ -225,6 +232,17 @@ impl UiRenderState {
} }
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) { pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
self.update_at(root, rsc, Instant::now());
}
/// Draw a frame dated on the platform's presentation clock. Returns
/// whether a widget requested another frame.
pub fn update_at<'a>(
&mut self,
root: impl Into<Option<&'a StrongWidget>>,
rsc: &mut dyn UiRsc,
frame_time: Instant,
) -> bool {
// safety mechanism for memory leaks; might wanna return a result instead so user can // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
if !rsc.widgets().waiting.is_empty() { if !rsc.widgets().waiting.is_empty() {
@@ -241,6 +259,7 @@ impl UiRenderState {
weak widgets: {all:#?}" weak widgets: {all:#?}"
); );
} }
self.frame_time = frame_time;
let root = root.into(); let root = root.into();
debug_assert!( debug_assert!(
self.draw_started.is_empty(), self.draw_started.is_empty(),
@@ -270,6 +289,13 @@ impl UiRenderState {
self.apply_free(); self.apply_free();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug_assert!(self.primitive_counts_agree(), "{}", self.orphan_report(rsc),); debug_assert!(self.primitive_counts_agree(), "{}", self.orphan_report(rsc),);
let next: Vec<_> = std::mem::take(&mut self.next_frame)
.into_iter()
.filter(|id| self.active.contains_key(id))
.collect();
let requested = !next.is_empty();
rsc.widgets_mut().needs_redraw.extend(next);
requested
} }
pub fn epoch(&self) -> Instant { pub fn epoch(&self) -> Instant {
-5
View File
@@ -47,11 +47,6 @@ pub trait Widget: Any {
fn child_order(&self) -> ChildOrder { fn child_order(&self) -> ChildOrder {
ChildOrder::Draw ChildOrder::Draw
} }
#[allow(unused_variables)]
fn tick(&mut self, now: std::time::Instant) -> bool {
false
}
} }
impl Widget for () { impl Widget for () {
+4 -3
View File
@@ -356,13 +356,14 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
); );
} }
let frame_start = Instant::now(); let frame_start = Instant::now();
let animating = self.rsc.ui_mut().tick_animations(now); let ui_state = self.state.android_state_mut();
let animating = self.rsc.draw_at(&ui_state.root, now);
// Ask before renderer update and swapchain acquisition: the latter
// may block until near the next presentation boundary.
if animating { if animating {
ctx.view.post_frame_callback(&mut ctx.env); ctx.view.post_frame_callback(&mut ctx.env);
} }
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
self.rsc.draw(&ui_state.root);
let ui_state = self.state.android_state_mut();
let Some(renderer) = &mut ui_state.renderer else { let Some(renderer) = &mut ui_state.renderer else {
return; return;
}; };
+3 -3
View File
@@ -255,14 +255,14 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
// Advance animations before drawing and keep requesting frames while active.
let frame_start = std::time::Instant::now(); let frame_start = std::time::Instant::now();
let animating = rsc.ui_mut().tick_animations(frame_start);
let ui_state = state.desktop_state_mut(); let ui_state = state.desktop_state_mut();
let animating = rsc.draw_at(&ui_state.root, frame_start);
// Ask before renderer update and swapchain acquisition: the latter
// may block until near the next presentation boundary.
if animating { if animating {
ui_state.window.request_redraw(); ui_state.window.request_redraw();
} }
rsc.draw(&ui_state.root);
ui_state.renderer.update(&mut rsc.ui); ui_state.renderer.update(&mut rsc.ui);
let mut parts = ui_state.renderer.draw(); let mut parts = ui_state.renderer.draw();
parts.total = frame_start.elapsed(); parts.total = frame_start.elapsed();
+3 -4
View File
@@ -198,8 +198,8 @@ impl Harness {
} }
/// The `Instant` this harness means by `t_ms`. Public because a /// The `Instant` this harness means by `t_ms`. Public because a
/// caller driving `ScrollController::tick` or `DragGesture` by hand needs /// caller driving a `DragGesture` by hand needs to date those calls on the
/// to date those calls on the same clock the touch samples use. /// same clock the touch samples and frame painter use.
pub fn at(&self, t_ms: u64) -> Instant { pub fn at(&self, t_ms: u64) -> Instant {
self.base + Duration::from_millis(t_ms) self.base + Duration::from_millis(t_ms)
} }
@@ -218,8 +218,7 @@ impl Harness {
} }
let now = self.at(t_ms); let now = self.at(t_ms);
let at = Instant::now(); let at = Instant::now();
let animating = self.rsc.ui.tick_animations(now); let animating = self.rsc.draw_at(&self.state.root, now);
self.rsc.draw(&self.state.root);
// No GPU here, so there is nothing to acquire and nothing to // No GPU here, so there is nothing to acquire and nothing to
// submit: the frame is all `build`, which is honest rather than // submit: the frame is all `build`, which is honest rather than
// zero-filled (`FrameParts::whole`). `layout`/`redraw`/ // zero-filled (`FrameParts::whole`). `layout`/`redraw`/
+43
View File
@@ -69,6 +69,23 @@ struct CountedLeaf {
draws: Rc<Cell<u32>>, draws: Rc<Cell<u32>>,
} }
struct FrameRequester {
draws: Rc<Cell<u32>>,
seen_time: Rc<Cell<Option<std::time::Instant>>>,
request: bool,
}
impl Widget for FrameRequester {
fn draw(&mut self, painter: &mut Painter) {
self.draws.set(self.draws.get() + 1);
self.seen_time.set(Some(painter.frame_time()));
if self.request {
painter.request_next_frame();
}
painter.set_size(Size::ZERO);
}
}
impl Widget for CountedLeaf { impl Widget for CountedLeaf {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
@@ -93,6 +110,32 @@ impl Widget for TracedParent {
} }
} }
#[test]
fn a_next_frame_request_is_staged_after_the_current_redraw() {
let mut rsc = TestRsc { ui: Ui::default() };
let draws = Rc::new(Cell::new(0));
let seen_time = Rc::new(Cell::new(None));
let widget = rsc.ui.widgets.add_strong(FrameRequester {
draws: draws.clone(),
seen_time: seen_time.clone(),
request: true,
});
let weak = widget.weak();
let root = widget.any();
let mut render = UiRenderState::new();
let frame_time = std::time::Instant::now();
assert!(render.update_at(&root, &mut rsc, frame_time));
assert_eq!(draws.get(), 1, "the request was consumed in the same frame");
assert_eq!(seen_time.get(), Some(frame_time));
assert!(rsc.ui.widgets.needs_redraw.contains(&weak.id()));
rsc.ui.widgets.get_mut(&weak).unwrap().request = false;
assert!(!render.update_at(&root, &mut rsc, frame_time));
assert_eq!(draws.get(), 2);
assert!(!rsc.ui.widgets.needs_redraw.contains(&weak.id()));
}
#[test] #[test]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() { fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
let mut rsc = TestRsc { ui: Ui::default() }; let mut rsc = TestRsc { ui: Ui::default() };
+8 -10
View File
@@ -1344,8 +1344,8 @@ impl FlingCalculator {
} }
/// The signed distance covered by `elapsed` into a fling of this /// The signed distance covered by `elapsed` into a fling of this
/// `velocity` -- what a per-frame ticker (`ScrollController::tick`) calls to /// `velocity` -- what a scrolling widget uses to find how far it should
/// find how far to have scrolled by now. Clamped to the full /// have moved by this frame. Clamped to the full
/// `distance()` once `elapsed` reaches `duration()`, so a caller need /// `distance()` once `elapsed` reaches `duration()`, so a caller need
/// not special-case "past the end." /// not special-case "past the end."
pub fn position_at(&self, velocity: f32, elapsed: Duration) -> f32 { pub fn position_at(&self, velocity: f32, elapsed: Duration) -> f32 {
@@ -1373,7 +1373,7 @@ impl FlingCalculator {
/// go, are the caller's -- a `LazySpan` scrolls its anchor one way and a /// go, are the caller's -- a `LazySpan` scrolls its anchor one way and a
/// `ScrollArea` moves its `amt` the other, and a `Flinger` that tried to know /// `ScrollArea` moves its `amt` the other, and a `Flinger` that tried to know
/// which would have to be told, which is the same thing as not knowing. /// which would have to be told, which is the same thing as not knowing.
/// So a caller applies [`Self::tick`]'s delta in its own convention and /// So a caller applies [`Self::advance`]'s delta in its own convention and
/// calls [`Self::stop`] when it runs out of content. /// calls [`Self::stop`] when it runs out of content.
pub struct Flinger { pub struct Flinger {
fling: Option<InFlight>, fling: Option<InFlight>,
@@ -1382,7 +1382,7 @@ pub struct Flinger {
struct InFlight { struct InFlight {
calc: FlingCalculator, calc: FlingCalculator,
velocity: f32, velocity: f32,
/// When the curve begins -- **the first [`Flinger::tick`], not the /// When the curve begins -- **the first [`Flinger::advance`], not the
/// release**. Set there so the only clock this reads is the one its /// release**. Set there so the only clock this reads is the one its
/// driver hands it: a caller running frames on an explicit clock /// driver hands it: a caller running frames on an explicit clock
/// (`iris::harness`, `bench_client.rs`'s scripted phases) would /// (`iris::harness`, `bench_client.rs`'s scripted phases) would
@@ -1406,15 +1406,13 @@ impl Flinger {
} }
/// Start a fling at `velocity_px_per_s`, in whatever pixel space the /// Start a fling at `velocity_px_per_s`, in whatever pixel space the
/// caller applies [`Self::tick`]'s delta in. `density` is physical /// caller applies [`Self::advance`]'s delta in. `density` is physical
/// pixels per dp, from the painter -- it does **not** cancel out of /// pixels per dp, from the painter -- it does **not** cancel out of
/// the spline (see [`FlingCalculator`]), and a hardcoded 1.0 against a /// the spline (see [`FlingCalculator`]), and a hardcoded 1.0 against a
/// 2.55-density screen made a one-second coast run for 45. /// 2.55-density screen made a one-second coast run for 45.
/// ///
/// Answers whether a fling actually started, which is a caller's cue /// Cancels any fling already in progress. A widget which owns one advances
/// to register for frames (`UiData::animate`): registering a widget /// it during `draw` and asks its painter for the following frame.
/// that is not animating only asks the next frame to find that out.
/// Cancels any fling already in progress.
/// ///
/// Compose's two thresholds at a release, and **only** those two. The /// Compose's two thresholds at a release, and **only** those two. The
/// maximum is `ViewConfiguration.getScaledMaximumFlingVelocity()` /// maximum is `ViewConfiguration.getScaledMaximumFlingVelocity()`
@@ -1476,7 +1474,7 @@ impl Flinger {
/// itself on the spline's own schedule, after which /// itself on the spline's own schedule, after which
/// [`Self::is_flinging`] is false and the caller stops asking for /// [`Self::is_flinging`] is false and the caller stops asking for
/// frames. /// frames.
pub fn tick(&mut self, now: Instant) -> f32 { pub fn advance(&mut self, now: Instant) -> f32 {
let Some(f) = &mut self.fling else { let Some(f) = &mut self.fling else {
return 0.0; return 0.0;
}; };
+4 -6
View File
@@ -616,8 +616,9 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
let mut c = cursor_at((50.0, y).into()); let mut c = cursor_at((50.0, y).into());
c.buttons.left = button; c.buttons.left = button;
c.time = base + std::time::Duration::from_millis(at_ms); c.time = base + std::time::Duration::from_millis(at_ms);
let frame_time = c.time;
render.run_sensors(rsc, state, c, win); render.run_sensors(rsc, state, c, win);
render.update(&root, rsc); render.update_at(&root, rsc, frame_time);
}; };
sample( sample(
&mut render, &mut render,
@@ -645,8 +646,6 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
"the flick must leave the top area coasting -- the press below is \ "the flick must leave the top area coasting -- the press below is \
only dangerous while something is still moving", only dangerous while something is still moving",
); );
let flung_to = rsc.ui.widgets.get(&top_w).unwrap().amt();
t += 8; t += 8;
sample( sample(
&mut render, &mut render,
@@ -672,9 +671,8 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
"the area actually under the finger should have panned by the 40px \ "the area actually under the finger should have panned by the 40px \
past the slop, got {moved}" past the slop, got {moved}"
); );
assert_eq!( assert!(
rsc.ui.widgets.get(&top_w).unwrap().amt(), rsc.ui.widgets.get(&top_w).unwrap().is_scrolling(),
flung_to,
"the area the pointer had left must not have seen the press at all -- \ "the area the pointer had left must not have seen the press at all -- \
a catch would have stopped its fling on the touch-down" a catch would have stopped its fling on the touch-down"
); );
+13 -23
View File
@@ -1,7 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::util::HashMap; use iris_core::util::HashMap;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::Instant;
pub type RowKey = u64; pub type RowKey = u64;
@@ -745,15 +744,13 @@ impl Widget for LazySpan {
ChildOrder::Axis(self.dir.axis) ChildOrder::Axis(self.dir.axis)
} }
/// A lazy span animates exactly one thing, its fling -- and it drives
/// its own rather than being handed deltas by a `ScrollArea` around
/// it, since which rows exist at all is a function of where it is
/// scrolled to and a moved lump would never update them.
fn tick(&mut self, now: Instant) -> bool {
self.tick_fling(now)
}
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
// A lazy span drives its own fling because which rows exist is a
// function of where it is scrolled to; it cannot be moved as one
// retained lump by a `ScrollArea` around it.
if self.ctl.advance(painter.frame_time()) {
painter.request_next_frame();
}
let axis = self.dir.axis; let axis = self.dir.axis;
let output_len = painter.output_size().axis(axis); let output_len = painter.output_size().axis(axis);
self.viewport_len = painter.region().axis(axis).len().to_abs(output_len); self.viewport_len = painter.region().axis(axis).len().to_abs(output_len);
@@ -1529,14 +1526,11 @@ mod tests {
fn fling_frame( fn fling_frame(
rsc: &mut TestRsc, rsc: &mut TestRsc,
scroll: &WeakWidget<LazySpan>,
root: &StrongWidget, root: &StrongWidget,
render: &mut UiRenderState, render: &mut UiRenderState,
now: Instant, now: Instant,
) -> bool { ) -> bool {
let still = rsc.ui.widgets.get_mut(scroll).unwrap().tick(now); render.update_at(root, rsc, now)
render.update(root, rsc);
still
} }
#[test] #[test]
@@ -1552,7 +1546,7 @@ mod tests {
let mut still = true; let mut still = true;
for step in 0..600 { for step in 0..600 {
let now = start + std::time::Duration::from_millis(step * 16); let now = start + std::time::Duration::from_millis(step * 16);
still = fling_frame(&mut rsc, &scroll, &root, &mut render, now); still = fling_frame(&mut rsc, &root, &mut render, now);
if !still { if !still {
break; break;
} }
@@ -1566,24 +1560,20 @@ mod tests {
} }
#[test] #[test]
fn a_registered_fling_is_driven_by_tick_animations_and_then_unregisters() { fn a_fling_requests_frames_until_it_settles() {
let mut rsc = TestRsc { ui: Ui::default() }; let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap()); let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap());
if rsc.ui.widgets.get_mut(&scroll).unwrap().fling(8000.0) { assert!(rsc.ui.widgets.get_mut(&scroll).unwrap().fling(8000.0));
let id = scroll.id();
rsc.ui.animate(id);
}
let start = Instant::now(); let start = Instant::now();
let mut steps = 0; let mut steps = 0;
let mut animating = true; let mut animating = true;
while animating && steps < 600 { while animating && steps < 600 {
let now = start + std::time::Duration::from_millis(steps * 16); let now = start + std::time::Duration::from_millis(steps * 16);
animating = rsc.ui.tick_animations(now); animating = render.update_at(&root, &mut rsc, now);
render.update(&root, &mut rsc);
steps += 1; steps += 1;
} }
assert!(!animating, "the driver never stopped within 600 frames"); assert!(!animating, "the driver never stopped within 600 frames");
@@ -1595,7 +1585,7 @@ mod tests {
); );
let now = start + std::time::Duration::from_millis(steps * 16); let now = start + std::time::Duration::from_millis(steps * 16);
assert!(!rsc.ui.tick_animations(now)); assert!(!render.update_at(&root, &mut rsc, now));
} }
#[test] #[test]
@@ -1650,7 +1640,7 @@ mod tests {
let start = Instant::now(); let start = Instant::now();
for step in 0..2000 { for step in 0..2000 {
let now = start + std::time::Duration::from_millis(step * 16); let now = start + std::time::Duration::from_millis(step * 16);
if !fling_frame(&mut rsc, &scroll, &root, &mut render, now) { if !fling_frame(&mut rsc, &root, &mut render, now) {
break; break;
} }
} }
+6 -14
View File
@@ -1,5 +1,4 @@
use crate::prelude::*; use crate::prelude::*;
use std::time::Instant;
pub struct ScrollArea { pub struct ScrollArea {
inner: StrongWidget, inner: StrongWidget,
@@ -19,11 +18,10 @@ impl Scrollable for ScrollArea {
} }
impl Widget for ScrollArea { impl Widget for ScrollArea {
fn tick(&mut self, now: Instant) -> bool {
self.tick_fling(now)
}
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
if self.ctl.advance(painter.frame_time()) {
painter.request_next_frame();
}
let axis = self.ctl.axis(); let axis = self.ctl.axis();
let container_len = painter.px_size().axis(axis); let container_len = painter.px_size().axis(axis);
self.container_len = container_len; self.container_len = container_len;
@@ -87,7 +85,7 @@ mod tests {
use super::*; use super::*;
use crate::layout_tests::TestRsc; use crate::layout_tests::TestRsc;
use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests}; use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests};
use std::time::Duration; use std::time::{Duration, Instant};
fn area() -> (Fixture, WidgetId) { fn area() -> (Fixture, WidgetId) {
area_on(Axis::Y) area_on(Axis::Y)
@@ -146,9 +144,7 @@ mod tests {
} }
fn fling_frame(&mut self, now: Instant) -> bool { fn fling_frame(&mut self, now: Instant) -> bool {
let still = self.get().tick(now); self.render.update_at(&self.root, &mut self.rsc, now)
self.draw();
still
} }
} }
@@ -158,11 +154,7 @@ mod tests {
fn drag(f: &mut Fixture, id: WidgetId, sense: CursorSense, pos: Vec2, t: Instant) { fn drag(f: &mut Fixture, id: WidgetId, sense: CursorSense, pos: Vec2, t: Instant) {
let pointer = PointerRequests::default(); let pointer = PointerRequests::default();
let flung = f.get().drag(&pointer, id, sense, pos, t); f.get().drag(&pointer, id, sense, pos, t);
if flung {
let id = f.area.id();
f.rsc.ui.animate(id);
}
f.draw(); f.draw();
} }
+8 -17
View File
@@ -159,9 +159,9 @@ impl ScrollController {
} }
/// Start a fling at `velocity`, in [`Self::scroll`]'s direction /// Start a fling at `velocity`, in [`Self::scroll`]'s direction
/// convention. Answers whether one actually started, which is the /// convention, cancelling any fling already in progress. Answers whether
/// caller's cue to register the widget for frames (`UiData::animate`). /// one actually started. The owning widget advances it from `draw` and
/// Cancels any fling already in progress. /// requests another frame while it remains active.
pub fn fling(&mut self, velocity: f32) -> bool { pub fn fling(&mut self, velocity: f32) -> bool {
self.fling.start(velocity, self.density) self.fling.start(velocity, self.density)
} }
@@ -189,8 +189,8 @@ impl ScrollController {
self.fling.velocity() self.fling.velocity()
} }
pub fn tick(&mut self, now: Instant) -> bool { pub(crate) fn advance(&mut self, now: Instant) -> bool {
let delta = self.fling.tick(now); let delta = self.fling.advance(now);
self.scroll(delta); self.scroll(delta);
self.fling.is_flinging() self.fling.is_flinging()
} }
@@ -210,9 +210,8 @@ impl ScrollController {
/// this takes the gesture over. Android's own `EditText` behaves the /// this takes the gesture over. Android's own `EditText` behaves the
/// same way -- a vertical drag scrolls, and only a long press selects. /// same way -- a vertical drag scrolls, and only a long press selects.
/// ///
/// Answers whether this frame *started a fling*, which is the caller's /// Answers whether this frame started a fling, for callers that need to
/// cue to register the widget for frames (`UiData::animate`) -- see /// distinguish a release which began coasting from one which did not.
/// [`Self::fling`].
pub fn drag( pub fn drag(
&mut self, &mut self,
pointer: &PointerRequests, pointer: &PointerRequests,
@@ -306,10 +305,6 @@ pub trait Scrollable {
fn set_pinned_to_end(&mut self, pinned: bool) { fn set_pinned_to_end(&mut self, pinned: bool) {
self.controller_mut().set_pinned_to_end(pinned); self.controller_mut().set_pinned_to_end(pinned);
} }
fn tick_fling(&mut self, now: Instant) -> bool {
self.controller_mut().tick(now)
}
} }
pub fn scroll_senses<Rsc, Tag, W, WL>(w: WL, axis: Axis) -> impl WidgetIdFn<Rsc, W> pub fn scroll_senses<Rsc, Tag, W, WL>(w: WL, axis: Axis) -> impl WidgetIdFn<Rsc, W>
@@ -325,11 +320,7 @@ where
.on(CursorSense::drag(axis), |ctx, rsc: &mut Rsc| { .on(CursorSense::drag(axis), |ctx, rsc: &mut Rsc| {
let id = ctx.widget.id(); let id = ctx.widget.id();
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos); let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
let flung = ctx ctx.widget(rsc)
.widget(rsc)
.drag(ctx.data.pointer, id, sense, pos, ctx.data.cursor.time); .drag(ctx.data.pointer, id, sense, pos, ctx.data.cursor.time);
if flung {
rsc.ui_mut().animate(id);
}
}) })
} }
+2 -4
View File
@@ -432,10 +432,8 @@ impl SelectionController {
SelectionInput::Handled SelectionInput::Handled
} }
GestureOutcome::Released(Some(velocity)) => { GestureOutcome::Released(Some(velocity)) => {
if let Some(scroll) = self.scroll if let Some(scroll) = self.scroll {
&& scroll(rsc).fling(velocity) scroll(rsc).fling(velocity);
{
rsc.ui_mut().animate(scroll.id());
} }
SelectionInput::Handled SelectionInput::Handled
} }