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| {
if let Some(screen) = &state.screen {
(screen.list)(rsc).fling(FLING_VELOCITY_PX_S);
animate_scroll(screen.list, rsc);
}
});
wait_for_fling_settle(ctx).await;
@@ -700,11 +699,6 @@ async fn read_anchor_position(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
.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>) {
let cap = Duration::from_millis(FLING_SETTLE_CAP_MS);
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
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**
(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
+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
implements `Scrollable`, whose one required pair of methods hands the
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:
@@ -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
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
**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
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
event, a drag frame and a fling tick all land between draws, and none of
them can know whether there is content to move into. Applying them at the
layout that follows is also what keeps layout a pure function of the state
(Iris, 2026-09-08). The visible consequence, and the thing that catches a
**Why the delta is banked rather than applied where it arrives.** A wheel or
drag lands between draws, and advancing a fling happens at the top of a draw;
none of them can know whether there is content to move into until layout runs.
Applying them in that layout is also what keeps layout a pure function of the
state (Iris, 2026-09-08). The visible consequence, and the thing that catches a
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
`frame_time_nanos`, converted through the view's one `DeviceClock`
(`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 masks: TrackedArena<Mask, u32>,
pub move_offsets: TrackedArena<MoveOffset, u32>,
animating: Vec<WidgetId>,
}
#[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 {
fn ui(&self) -> &Ui;
fn ui_mut(&mut self) -> &mut Ui;
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
Self: Sized,
{
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)]
+16 -1
View File
@@ -9,7 +9,7 @@ use crate::{
ui::render_state::Retained,
util::Vec2,
};
use std::{cell::RefCell, rc::Rc};
use std::{cell::RefCell, rc::Rc, time::Instant};
pub struct Painter<'a> {
pub(super) render_state: &'a mut UiRenderState,
@@ -52,6 +52,21 @@ impl DrawResult<'_, '_> {
}
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) {
assert!(
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
/// ever drawn and was never emptied.
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
/// last `take_counters`. LAYOUT.md section 8's pass conditions are
@@ -111,6 +116,8 @@ impl UiRenderState {
old_root: None,
resized: false,
draw_started: Default::default(),
next_frame: Default::default(),
frame_time: Instant::now(),
draw_count: 0,
region_mut_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) {
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
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
@@ -241,6 +259,7 @@ impl UiRenderState {
weak widgets: {all:#?}"
);
}
self.frame_time = frame_time;
let root = root.into();
debug_assert!(
self.draw_started.is_empty(),
@@ -270,6 +289,13 @@ impl UiRenderState {
self.apply_free();
#[cfg(debug_assertions)]
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 {
-5
View File
@@ -47,11 +47,6 @@ pub trait Widget: Any {
fn child_order(&self) -> ChildOrder {
ChildOrder::Draw
}
#[allow(unused_variables)]
fn tick(&mut self, now: std::time::Instant) -> bool {
false
}
}
impl Widget for () {
+4 -3
View File
@@ -356,13 +356,14 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
);
}
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 {
ctx.view.post_frame_callback(&mut ctx.env);
}
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 {
return;
};
+3 -3
View File
@@ -255,14 +255,14 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
// Advance animations before drawing and keep requesting frames while active.
let frame_start = std::time::Instant::now();
let animating = rsc.ui_mut().tick_animations(frame_start);
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 {
ui_state.window.request_redraw();
}
rsc.draw(&ui_state.root);
ui_state.renderer.update(&mut rsc.ui);
let mut parts = ui_state.renderer.draw();
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
/// caller driving `ScrollController::tick` or `DragGesture` by hand needs
/// to date those calls on the same clock the touch samples use.
/// caller driving a `DragGesture` by hand needs to date those calls on the
/// same clock the touch samples and frame painter use.
pub fn at(&self, t_ms: u64) -> Instant {
self.base + Duration::from_millis(t_ms)
}
@@ -218,8 +218,7 @@ impl Harness {
}
let now = self.at(t_ms);
let at = Instant::now();
let animating = self.rsc.ui.tick_animations(now);
self.rsc.draw(&self.state.root);
let animating = self.rsc.draw_at(&self.state.root, now);
// No GPU here, so there is nothing to acquire and nothing to
// submit: the frame is all `build`, which is honest rather than
// zero-filled (`FrameParts::whole`). `layout`/`redraw`/
+43
View File
@@ -69,6 +69,23 @@ struct CountedLeaf {
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 {
fn draw(&mut self, painter: &mut Painter) {
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]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
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
/// `velocity` -- what a per-frame ticker (`ScrollController::tick`) calls to
/// find how far to have scrolled by now. Clamped to the full
/// `velocity` -- what a scrolling widget uses to find how far it should
/// have moved by this frame. Clamped to the full
/// `distance()` once `elapsed` reaches `duration()`, so a caller need
/// not special-case "past the end."
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
/// `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.
/// 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.
pub struct Flinger {
fling: Option<InFlight>,
@@ -1382,7 +1382,7 @@ pub struct Flinger {
struct InFlight {
calc: FlingCalculator,
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
/// driver hands it: a caller running frames on an explicit clock
/// (`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
/// 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
/// the spline (see [`FlingCalculator`]), and a hardcoded 1.0 against a
/// 2.55-density screen made a one-second coast run for 45.
///
/// Answers whether a fling actually started, which is a caller's cue
/// to register for frames (`UiData::animate`): registering a widget
/// that is not animating only asks the next frame to find that out.
/// Cancels any fling already in progress.
/// Cancels any fling already in progress. A widget which owns one advances
/// it during `draw` and asks its painter for the following frame.
///
/// Compose's two thresholds at a release, and **only** those two. The
/// maximum is `ViewConfiguration.getScaledMaximumFlingVelocity()`
@@ -1476,7 +1474,7 @@ impl Flinger {
/// itself on the spline's own schedule, after which
/// [`Self::is_flinging`] is false and the caller stops asking for
/// frames.
pub fn tick(&mut self, now: Instant) -> f32 {
pub fn advance(&mut self, now: Instant) -> f32 {
let Some(f) = &mut self.fling else {
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());
c.buttons.left = button;
c.time = base + std::time::Duration::from_millis(at_ms);
let frame_time = c.time;
render.run_sensors(rsc, state, c, win);
render.update(&root, rsc);
render.update_at(&root, rsc, frame_time);
};
sample(
&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 \
only dangerous while something is still moving",
);
let flung_to = rsc.ui.widgets.get(&top_w).unwrap().amt();
t += 8;
sample(
&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 \
past the slop, got {moved}"
);
assert_eq!(
rsc.ui.widgets.get(&top_w).unwrap().amt(),
flung_to,
assert!(
rsc.ui.widgets.get(&top_w).unwrap().is_scrolling(),
"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"
);
+13 -23
View File
@@ -1,7 +1,6 @@
use crate::prelude::*;
use iris_core::util::HashMap;
use std::collections::VecDeque;
use std::time::Instant;
pub type RowKey = u64;
@@ -745,15 +744,13 @@ impl Widget for LazySpan {
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) {
// 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 output_len = painter.output_size().axis(axis);
self.viewport_len = painter.region().axis(axis).len().to_abs(output_len);
@@ -1529,14 +1526,11 @@ mod tests {
fn fling_frame(
rsc: &mut TestRsc,
scroll: &WeakWidget<LazySpan>,
root: &StrongWidget,
render: &mut UiRenderState,
now: Instant,
) -> bool {
let still = rsc.ui.widgets.get_mut(scroll).unwrap().tick(now);
render.update(root, rsc);
still
render.update_at(root, rsc, now)
}
#[test]
@@ -1552,7 +1546,7 @@ mod tests {
let mut still = true;
for step in 0..600 {
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 {
break;
}
@@ -1566,24 +1560,20 @@ mod tests {
}
#[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 (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap());
if rsc.ui.widgets.get_mut(&scroll).unwrap().fling(8000.0) {
let id = scroll.id();
rsc.ui.animate(id);
}
assert!(rsc.ui.widgets.get_mut(&scroll).unwrap().fling(8000.0));
let start = Instant::now();
let mut steps = 0;
let mut animating = true;
while animating && steps < 600 {
let now = start + std::time::Duration::from_millis(steps * 16);
animating = rsc.ui.tick_animations(now);
render.update(&root, &mut rsc);
animating = render.update_at(&root, &mut rsc, now);
steps += 1;
}
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);
assert!(!rsc.ui.tick_animations(now));
assert!(!render.update_at(&root, &mut rsc, now));
}
#[test]
@@ -1650,7 +1640,7 @@ mod tests {
let start = Instant::now();
for step in 0..2000 {
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;
}
}
+6 -14
View File
@@ -1,5 +1,4 @@
use crate::prelude::*;
use std::time::Instant;
pub struct ScrollArea {
inner: StrongWidget,
@@ -19,11 +18,10 @@ impl Scrollable for ScrollArea {
}
impl Widget for ScrollArea {
fn tick(&mut self, now: Instant) -> bool {
self.tick_fling(now)
}
fn draw(&mut self, painter: &mut Painter) {
if self.ctl.advance(painter.frame_time()) {
painter.request_next_frame();
}
let axis = self.ctl.axis();
let container_len = painter.px_size().axis(axis);
self.container_len = container_len;
@@ -87,7 +85,7 @@ mod tests {
use super::*;
use crate::layout_tests::TestRsc;
use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests};
use std::time::Duration;
use std::time::{Duration, Instant};
fn area() -> (Fixture, WidgetId) {
area_on(Axis::Y)
@@ -146,9 +144,7 @@ mod tests {
}
fn fling_frame(&mut self, now: Instant) -> bool {
let still = self.get().tick(now);
self.draw();
still
self.render.update_at(&self.root, &mut self.rsc, now)
}
}
@@ -158,11 +154,7 @@ mod tests {
fn drag(f: &mut Fixture, id: WidgetId, sense: CursorSense, pos: Vec2, t: Instant) {
let pointer = PointerRequests::default();
let flung = f.get().drag(&pointer, id, sense, pos, t);
if flung {
let id = f.area.id();
f.rsc.ui.animate(id);
}
f.get().drag(&pointer, id, sense, pos, t);
f.draw();
}
+8 -17
View File
@@ -159,9 +159,9 @@ impl ScrollController {
}
/// Start a fling at `velocity`, in [`Self::scroll`]'s direction
/// convention. Answers whether one actually started, which is the
/// caller's cue to register the widget for frames (`UiData::animate`).
/// Cancels any fling already in progress.
/// convention, cancelling any fling already in progress. Answers whether
/// one actually started. The owning widget advances it from `draw` and
/// requests another frame while it remains active.
pub fn fling(&mut self, velocity: f32) -> bool {
self.fling.start(velocity, self.density)
}
@@ -189,8 +189,8 @@ impl ScrollController {
self.fling.velocity()
}
pub fn tick(&mut self, now: Instant) -> bool {
let delta = self.fling.tick(now);
pub(crate) fn advance(&mut self, now: Instant) -> bool {
let delta = self.fling.advance(now);
self.scroll(delta);
self.fling.is_flinging()
}
@@ -210,9 +210,8 @@ impl ScrollController {
/// this takes the gesture over. Android's own `EditText` behaves the
/// same way -- a vertical drag scrolls, and only a long press selects.
///
/// Answers whether this frame *started a fling*, which is the caller's
/// cue to register the widget for frames (`UiData::animate`) -- see
/// [`Self::fling`].
/// Answers whether this frame started a fling, for callers that need to
/// distinguish a release which began coasting from one which did not.
pub fn drag(
&mut self,
pointer: &PointerRequests,
@@ -306,10 +305,6 @@ pub trait Scrollable {
fn set_pinned_to_end(&mut self, pinned: bool) {
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>
@@ -325,11 +320,7 @@ where
.on(CursorSense::drag(axis), |ctx, rsc: &mut Rsc| {
let id = ctx.widget.id();
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
let flung = ctx
.widget(rsc)
ctx.widget(rsc)
.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
}
GestureOutcome::Released(Some(velocity)) => {
if let Some(scroll) = self.scroll
&& scroll(rsc).fling(velocity)
{
rsc.ui_mut().animate(scroll.id());
if let Some(scroll) = self.scroll {
scroll(rsc).fling(velocity);
}
SelectionInput::Handled
}
@@ -138,8 +138,6 @@ fn what_a_fling_uploads() {
for pass in 0..PASSES {
let velocity = if pass % 2 == 0 { VELOCITY } else { -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;
while t <= end {
t += PHONE_FRAME_MS;
@@ -31,8 +31,6 @@ fn what_a_fling_frame_costs() {
let velocity = if pass % 2 == 0 { VELOCITY } else { -VELOCITY };
let list = (opened.screen.list)(&mut h.rsc);
list.fling(velocity);
let id = opened.screen.list.id();
h.rsc.ui.animate(id);
let mut frames = Vec::new();
let mut layouts = Vec::new();