iris: drive animation from painter frame time
This commit is contained in:
1 parent
9fe6aca1f1
commit
b1416d86bf
19 files changed
+171
-133
No files matched your search
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)]
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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`/
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user