iris: drive animation from painter frame time

This commit is contained in:
iris committed 2026-09-12 21:37:47 -04:00
1 parent 9fe6aca1f1
commit b1416d86bf
19 files changed
+171 -133

No files matched your search

-6
View File
@@ -669,7 +669,6 @@ async fn run_fling_phase(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
ctx.update(|state: &mut BenchClient, rsc| { ctx.update(|state: &mut BenchClient, rsc| {
if let Some(screen) = &state.screen { if let Some(screen) = &state.screen {
(screen.list)(rsc).fling(FLING_VELOCITY_PX_S); (screen.list)(rsc).fling(FLING_VELOCITY_PX_S);
animate_scroll(screen.list, rsc);
} }
}); });
wait_for_fling_settle(ctx).await; wait_for_fling_settle(ctx).await;
@@ -700,11 +699,6 @@ async fn read_anchor_position(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
.await .await
} }
fn animate_scroll(scroll: iris::prelude::WeakWidget<iris::prelude::LazySpan>, rsc: &mut Rsc) {
let id = scroll.id();
rsc.ui_mut().animate(id);
}
async fn wait_for_fling_settle(ctx: &mut iris::task::TaskCtx<Rsc>) { async fn wait_for_fling_settle(ctx: &mut iris::task::TaskCtx<Rsc>) {
let cap = Duration::from_millis(FLING_SETTLE_CAP_MS); let cap = Duration::from_millis(FLING_SETTLE_CAP_MS);
let started = Instant::now(); let started = Instant::now();
+8
View File
@@ -813,6 +813,14 @@ widget changed. Winit's user-event proxy and Android's posted callback are
private implementations of that wake; applications do not define platform private implementations of that wake; applications do not define platform
event types or manually request redraws. event types or manually request redraws.
Frame-driven visuals use that same draw path rather than a parallel animation
registry. The host supplies the presentation timestamp to `Painter`; a widget
reads it during `draw` and calls `request_next_frame` while it remains active.
Iris stages the requesting widget's invalidation until the current retained
redraw has finished, then the host schedules the next platform frame before
renderer update or swapchain acquisition. `Widget::draw` still returns unit,
and `Widget` has no tick callback.
**Iris ships no fonts, and font families are application-named strings** **Iris ships no fonts, and font families are application-named strings**
(2026-09-12). Applications register their own font bytes on `Ui` and select (2026-09-12). Applications register their own font bytes on `Ui` and select
them by the same string used by text widgets. The public boundaries accept them by the same string used by text widgets. The public boundaries accept
+15 -7
View File
@@ -15,7 +15,7 @@ that struct (`scrollable.rs`); there is exactly one `Flinger` and one
`DragGesture` implementation in the crate's widgets. A widget with one `DragGesture` implementation in the crate's widgets. A widget with one
implements `Scrollable`, whose one required pair of methods hands the implements `Scrollable`, whose one required pair of methods hands the
controller back, and gets `scroll`, `fling`, `drag`, `amt`, controller back, and gets `scroll`, `fling`, `drag`, `amt`,
`is_scrolling`, `tick_fling` and the pin as default methods. `is_scrolling` and the pin as default methods.
Two widgets have one, and they differ only in how they spend a delta: Two widgets have one, and they differ only in how they spend a delta:
@@ -30,6 +30,14 @@ Two widgets have one, and they differ only in how they spend a delta:
Do not give a widget its own fling, scroll amount, or platform wake Do not give a widget its own fling, scroll amount, or platform wake
handle, and do not add scrolling methods to the general `Widget` trait. handle, and do not add scrolling methods to the general `Widget` trait.
There is no animation registry or animation callback on `Widget`. The
platform passes the frame's presentation time through `Painter`; a scrolling
widget advances its controller at the beginning of `draw` and calls
`Painter::request_next_frame` while the fling remains active. That request is
staged until the current retained redraw finishes, then dirties the requesting
widget for the following frame. Staging matters because the ordinary dirty set
is drained to completion within one frame.
## One convention for a delta ## One convention for a delta
**Positive scrolls the reader up or left; negative down or right.** The **Positive scrolls the reader up or left; negative down or right.** The
@@ -60,14 +68,14 @@ means "the end is not in sight". That last is not a placeholder — a lazy
layout genuinely cannot say how far its content runs without walking layout genuinely cannot say how far its content runs without walking
there, and `clamp` takes the answer with no branch of its own. there, and `clamp` takes the answer with no branch of its own.
**Why the delta is banked rather than applied where it arrives.** A wheel **Why the delta is banked rather than applied where it arrives.** A wheel or
event, a drag frame and a fling tick all land between draws, and none of drag lands between draws, and advancing a fling happens at the top of a draw;
them can know whether there is content to move into. Applying them at the none of them can know whether there is content to move into until layout runs.
layout that follows is also what keeps layout a pure function of the state Applying them in that layout is also what keeps layout a pure function of the
(Iris, 2026-09-08). The visible consequence, and the thing that catches a state (Iris, 2026-09-08). The visible consequence, and the thing that catches a
test out: **`amt` does not move until the next draw.** test out: **`amt` does not move until the next draw.**
**Which clock a fling is ticked on.** The vsync the frame callback **Which clock advances a fling.** The vsync the frame callback
carries, not `Instant::now()` -- on Android `do_frame`'s carries, not `Instant::now()` -- on Android `do_frame`'s
`frame_time_nanos`, converted through the view's one `DeviceClock` `frame_time_nanos`, converted through the view's one `DeviceClock`
(`sense.rs`), which also dates every touch sample, so a fling is advanced (`sense.rs`), which also dates every touch sample, so a fling is advanced
+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
} }
@@ -138,8 +138,6 @@ fn what_a_fling_uploads() {
for pass in 0..PASSES { for pass in 0..PASSES {
let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY }; let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY };
(opened.screen.list)(&mut h.rsc).fling(velocity); (opened.screen.list)(&mut h.rsc).fling(velocity);
let id = opened.screen.list.id();
h.rsc.ui.animate(id);
let end = t + PASS_CAP_MS; let end = t + PASS_CAP_MS;
while t <= end { while t <= end {
t += PHONE_FRAME_MS; t += PHONE_FRAME_MS;
@@ -31,8 +31,6 @@ fn what_a_fling_frame_costs() {
let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY }; let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY };
let list = (opened.screen.list)(&mut h.rsc); let list = (opened.screen.list)(&mut h.rsc);
list.fling(velocity); list.fling(velocity);
let id = opened.screen.list.id();
h.rsc.ui.animate(id);
let mut frames = Vec::new(); let mut frames = Vec::new();
let mut layouts = Vec::new(); let mut layouts = Vec::new();