feat(core): Add HoldToConfirm example, improve Loader and animation frame support

matejcik/one-of
Jan Pochyla 3 years ago committed by matejcik
parent 117a0bd518
commit 3dd3d7f87b

@ -9,10 +9,9 @@ use crate::{
util,
};
use super::error;
use super::{
defs::{self, FieldDef, FieldType, MsgDef},
error,
obj::{MsgDefObj, MsgObj},
zigzag,
};

@ -39,9 +39,9 @@ impl Duration {
}
impl Mul<f32> for Duration {
// Multiplication by float is saturating -- in particular, casting from a float to
// an int is saturating, value larger than INT_MAX casts to INT_MAX. So this
// operation does not need to be checked.
// Multiplication by float is saturating -- in particular, casting from a float
// to an int is saturating, value larger than INT_MAX casts to INT_MAX. So
// this operation does not need to be checked.
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
@ -116,10 +116,10 @@ impl Instant {
}
pub fn checked_sub(self, duration: Duration) -> Option<Self> {
let add_millis = duration.to_millis();
if add_millis <= MAX_DIFFERENCE_IN_MILLIS {
let sub_millis = duration.to_millis();
if sub_millis <= MAX_DIFFERENCE_IN_MILLIS {
Some(Self {
millis: self.millis.wrapping_sub(add_millis),
millis: self.millis.wrapping_sub(sub_millis),
})
} else {
None

@ -109,6 +109,7 @@ where
pub trait ComponentExt: Sized {
fn into_child(self) -> Child<Self>;
fn request_complete_repaint(&mut self, ctx: &mut EventCtx);
}
impl<T> ComponentExt for T
@ -118,6 +119,15 @@ where
fn into_child(self) -> Child<Self> {
Child::new(self)
}
fn request_complete_repaint(&mut self, ctx: &mut EventCtx) {
if self.event(ctx, Event::RequestPaint).is_some() {
// Messages raised during a `RequestPaint` dispatch are not propagated, let's
// make sure we don't do that.
#[cfg(feature = "ui_debug")]
panic!("cannot raise messages during RequestPaint");
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
@ -154,12 +164,16 @@ pub struct EventCtx {
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
next_token: u32,
paint_requested: bool,
anim_frame_scheduled: bool,
}
impl EventCtx {
/// Timer token dedicated for animation frames.
pub const ANIM_FRAME_TIMER: TimerToken = TimerToken(1);
/// How long into the future we should schedule the animation frame timer.
const ANIM_FRAME_DEADLINE: Duration = Duration::from_millis(18);
// 0 == `TimerToken::INVALID`,
// 1 == `Self::ANIM_FRAME_TIMER`.
const STARTING_TIMER_TOKEN: u32 = 2;
@ -172,6 +186,7 @@ impl EventCtx {
timers: Vec::new(),
next_token: Self::STARTING_TIMER_TOKEN,
paint_requested: false,
anim_frame_scheduled: false,
}
}
@ -182,10 +197,6 @@ impl EventCtx {
self.paint_requested = true;
}
pub fn clear_paint_requests(&mut self) {
self.paint_requested = false;
}
/// Request a timer event to be delivered after `deadline` elapses.
pub fn request_timer(&mut self, deadline: Duration) -> TimerToken {
let token = self.next_timer_token();
@ -195,13 +206,21 @@ impl EventCtx {
/// Request an animation frame timer to fire as soon as possible.
pub fn request_anim_frame(&mut self) {
self.register_timer(Self::ANIM_FRAME_TIMER, Duration::ZERO);
if !self.anim_frame_scheduled {
self.anim_frame_scheduled = true;
self.register_timer(Self::ANIM_FRAME_TIMER, Self::ANIM_FRAME_DEADLINE);
}
}
pub fn pop_timer(&mut self) -> Option<(TimerToken, Duration)> {
self.timers.pop()
}
pub fn clear(&mut self) {
self.paint_requested = false;
self.anim_frame_scheduled = false;
}
fn register_timer(&mut self, token: TimerToken, deadline: Duration) {
if self.timers.push((token, deadline)).is_err() {
// The timer queue is full, this would be a development error in the layout

@ -7,7 +7,7 @@ pub mod map;
pub mod text;
pub mod tuple;
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use empty::Empty;
pub use label::{Label, LabelStyle};
pub use text::{

@ -128,8 +128,8 @@ impl LayoutObj {
fn obj_event(&self, event: Event) -> Result<Obj, Error> {
let inner = &mut *self.inner.borrow_mut();
// Clear the upwards-propagating paint request flag from the last event pass.
inner.event_ctx.clear_paint_requests();
// Clear the leftover flags from the previous event pass.
inner.event_ctx.clear();
// Send the event down the component tree. Bail out in case of failure.
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.

@ -0,0 +1,140 @@
use crate::{
time::Instant,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx},
display::{self, Color},
geometry::Rect,
},
};
use super::{theme, Button, ButtonMsg, DialogLayout, Loader, LoaderMsg};
pub enum HoldToConfirmMsg<T> {
Content(T),
Cancelled,
Confirmed,
}
pub struct HoldToConfirm<T> {
loader: Loader,
content: Child<T>,
cancel: Child<Button>,
confirm: Child<Button>,
pad: Pad,
}
impl<T> HoldToConfirm<T>
where
T: Component,
{
pub fn new(area: Rect, content: impl FnOnce(Rect) -> T) -> Self {
let layout = DialogLayout::middle(area);
Self {
loader: Loader::new(0),
content: content(layout.content).into_child(),
cancel: Button::with_text(layout.left, b"Cancel").into_child(),
confirm: Button::with_text(layout.right, b"Hold").into_child(),
pad: Pad::with_background(layout.content, theme::BG),
}
}
}
impl<T> Component for HoldToConfirm<T>
where
T: Component,
{
type Msg = HoldToConfirmMsg<T::Msg>;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let now = Instant::now();
if let Some(LoaderMsg::ShrunkCompletely) = self.loader.event(ctx, event) {
// Clear the remnants of the loader.
self.pad.clear();
// Switch it to the initial state, so we stop painting it.
self.loader.reset();
// Re-draw the whole content tree.
self.content.request_complete_repaint(ctx);
// This can be a result of an animation frame event, we should take
// care to not short-circuit here and deliver the event to the
// content as well.
}
if let Some(msg) = self.content.event(ctx, event) {
return Some(Self::Msg::Content(msg));
}
if let Some(ButtonMsg::Clicked) = self.cancel.event(ctx, event) {
return Some(Self::Msg::Cancelled);
}
match self.confirm.event(ctx, event) {
Some(ButtonMsg::Pressed) => {
self.loader.start_growing(ctx, now);
self.pad.clear(); // Clear the remnants of the content.
}
Some(ButtonMsg::Released) => {
self.loader.start_shrinking(ctx, now);
}
Some(ButtonMsg::Clicked) => {
if self.loader.is_completely_grown(now) {
self.loader.reset();
return Some(HoldToConfirmMsg::Confirmed);
} else {
self.loader.start_shrinking(ctx, now);
}
}
_ => {}
}
None
}
fn paint(&mut self) {
self.pad.paint();
if self.loader.is_animating() {
self.loader.paint();
} else {
self.content.paint();
}
self.cancel.paint();
self.confirm.paint();
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for HoldToConfirm<T>
where
T: crate::trace::Trace,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("HoldToConfirm");
self.content.trace(d);
d.close();
}
}
struct Pad {
area: Rect,
color: Color,
clear: bool,
}
impl Pad {
fn with_background(area: Rect, color: Color) -> Self {
Self {
area,
color,
clear: false,
}
}
fn clear(&mut self) {
self.clear = true;
}
fn paint(&mut self) {
if self.clear {
self.clear = false;
display::rect(self.area, self.color);
}
}
}

@ -10,9 +10,9 @@ pub enum DialogMsg<T, L, R> {
}
pub struct Dialog<T, L, R> {
content: Child<T>,
left: Child<L>,
right: Child<R>,
pub content: Child<T>,
pub left: Child<L>,
pub right: Child<R>,
}
impl<T, L, R> Dialog<T, L, R>
@ -59,19 +59,19 @@ where
}
}
struct DialogLayout {
content: Rect,
left: Rect,
right: Rect,
pub struct DialogLayout {
pub content: Rect,
pub left: Rect,
pub right: Rect,
}
impl DialogLayout {
fn middle(area: Rect) -> Self {
pub fn middle(area: Rect) -> Self {
let grid = Grid::new(area, 5, 2);
Self {
content: Rect::new(
grid.row_col(0, 0).top_left(),
grid.row_col(4, 1).bottom_right(),
grid.row_col(3, 1).bottom_right(),
),
left: grid.row_col(4, 0),
right: grid.row_col(4, 1),

@ -2,13 +2,19 @@ use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Never},
component::{Component, Event, EventCtx},
display::{self, Color},
geometry::Offset,
},
};
use super::theme;
pub enum LoaderMsg {
GrownCompletely,
ShrunkCompletely,
}
enum State {
Initial,
Growing(Animation<u16>),
@ -24,6 +30,8 @@ pub struct Loader {
}
impl Loader {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new(offset_y: i32) -> Self {
Self {
offset_y,
@ -34,7 +42,7 @@ impl Loader {
}
}
pub fn start(&mut self, now: Instant) {
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MIN,
display::LOADER_MAX,
@ -45,9 +53,16 @@ impl Loader {
anim.seek_to_value(shrinking.value(now));
}
self.state = State::Growing(anim);
// The animation is starting, request an animation frame event.
ctx.request_anim_frame();
// We don't have to wait for the animation frame event with the first paint,
// let's do that now.
ctx.request_paint();
}
pub fn stop(&mut self, now: Instant) {
pub fn start_shrinking(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MAX,
display::LOADER_MIN,
@ -55,46 +70,77 @@ impl Loader {
now,
);
if let State::Growing(growing) = &self.state {
anim.seek_to_value(growing.value(now));
anim.seek_to_value(display::LOADER_MAX - growing.value(now));
}
self.state = State::Shrinking(anim);
// The animation should be already progressing at this point, so we don't need
// to request another animation frames, but we should request to get painted
// after this event pass.
ctx.request_paint();
}
pub fn reset(&mut self) {
self.state = State::Initial;
}
pub fn progress(&self, now: Instant) -> Option<u16> {
pub fn animation(&self) -> Option<&Animation<u16>> {
match &self.state {
State::Initial => None,
State::Growing(anim) | State::Shrinking(anim) => Some(anim.value(now)),
State::Growing(a) | State::Shrinking(a) => Some(a),
}
}
pub fn is_started(&self) -> bool {
matches!(self.state, State::Growing(_) | State::Shrinking(_))
pub fn progress(&self, now: Instant) -> Option<u16> {
self.animation().map(|a| a.value(now))
}
pub fn is_finished(&self, now: Instant) -> bool {
self.progress(now) == Some(display::LOADER_MAX)
pub fn is_animating(&self) -> bool {
self.animation().is_some()
}
pub fn is_completely_grown(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MAX))
}
pub fn is_completely_shrunk(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MIN))
}
}
impl Component for Loader {
type Msg = Never;
type Msg = LoaderMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let now = Instant::now();
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_started() {
if self.is_animating() {
// We have something to paint, so request to be painted in the next pass.
ctx.request_paint();
ctx.request_anim_frame();
if self.is_completely_grown(now) {
return Some(LoaderMsg::GrownCompletely);
} else if self.is_completely_shrunk(now) {
return Some(LoaderMsg::ShrunkCompletely);
} else {
// There is further progress in the animation, request an animation frame event.
ctx.request_anim_frame();
}
}
}
None
}
fn paint(&mut self) {
if let Some(progress) = self.progress(Instant::now()) {
// TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful
// in automated tests.
// In practice, taking the current instant here is more precise in case some
// other component in the tree takes a long time to draw.
let now = Instant::now();
if let Some(progress) = self.progress(now) {
let style = if progress < display::LOADER_MAX {
self.styles.normal
} else {
@ -122,20 +168,29 @@ pub struct LoaderStyle {
pub background_color: Color,
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Loader {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Loader");
d.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loader_yields_expected_progress() {
let mut ctx = EventCtx::new();
let mut l = Loader::new(0);
let t = Instant::now();
assert_eq!(l.progress(t), None);
l.start(t);
l.start_growing(&mut ctx, t);
assert_eq!(l.progress(t), Some(0));
let t = add_millis(t, 500);
assert_eq!(l.progress(t), Some(500));
l.stop(t);
l.start_shrinking(&mut ctx, t);
assert_eq!(l.progress(t), Some(500));
let t = add_millis(t, 125);
assert_eq!(l.progress(t), Some(250));

@ -1,4 +1,5 @@
mod button;
mod confirm;
mod dialog;
mod loader;
mod page;
@ -7,7 +8,9 @@ mod pin;
mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet};
pub use dialog::{Dialog, DialogMsg};
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use swipe::{Swipe, SwipeDirection};
use super::{event, theme};

@ -2,7 +2,7 @@ use core::convert::{TryFrom, TryInto};
use crate::{
error::Error,
micropython::{buffer::Buffer, obj::Obj},
micropython::obj::Obj,
ui::{
component::{Child, FormattedText},
display,
@ -12,7 +12,7 @@ use crate::{
};
use super::{
component::{Button, ButtonMsg, Dialog, DialogMsg},
component::{ButtonMsg, DialogMsg, HoldToConfirm, HoldToConfirmMsg},
theme,
};
@ -33,20 +33,32 @@ where
}
}
impl<T> TryFrom<HoldToConfirmMsg<T>> for Obj
where
Obj: TryFrom<T>,
Error: From<<Obj as TryFrom<T>>::Error>,
{
type Error = Error;
fn try_from(val: HoldToConfirmMsg<T>) -> Result<Self, Self::Error> {
match val {
HoldToConfirmMsg::Content(c) => Ok(c.try_into()?),
HoldToConfirmMsg::Confirmed => 1.try_into(),
HoldToConfirmMsg::Cancelled => 2.try_into(),
}
}
}
#[no_mangle]
extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
extern "C" fn ui_layout_new_example(_param: Obj) -> Obj {
let block = move || {
let param: Buffer = param.try_into()?;
let layout = LayoutObj::new(Child::new(Dialog::new(
display::screen(),
|area| {
FormattedText::new::<theme::TTDefaultText>(area, param)
.with(b"some", "a few")
.with(b"param", "xx")
},
|area| Button::with_text(area, b"Left"),
|area| Button::with_text(area, b"Right"),
)))?;
let layout = LayoutObj::new(Child::new(HoldToConfirm::new(display::screen(), |area| {
FormattedText::new::<theme::TTDefaultText>(
area,
"Testing text layout, with some text, and some more text. And {param}",
)
.with(b"param", b"parameters!")
})))?;
Ok(layout.into())
};
unsafe { util::try_or_raise(block) }
@ -54,7 +66,10 @@ extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
#[cfg(test)]
mod tests {
use crate::trace::{Trace, Tracer};
use crate::{
trace::{Trace, Tracer},
ui::model_tt::component::{Button, Dialog},
};
use super::*;
@ -64,15 +79,15 @@ mod tests {
}
fn bytes(&mut self, b: &[u8]) {
self.extend(b)
self.extend(b);
}
fn string(&mut self, s: &str) {
self.extend(s.as_bytes())
self.extend(s.as_bytes());
}
fn symbol(&mut self, name: &str) {
self.extend(name.as_bytes())
self.extend(name.as_bytes());
}
fn open(&mut self, name: &str) {
@ -89,7 +104,7 @@ mod tests {
}
fn close(&mut self) {
self.extend(b">")
self.extend(b">");
}
}

@ -572,3 +572,15 @@ class spawn(Syscall):
is True, it would be calling close on self, which will result in a ValueError.
"""
return self.task is this_task
class Timer(Syscall):
def __init__(self) -> None:
self.task: Task | None = None
def handle(self, task: Task) -> None:
self.task = task
def schedule(self, deadline: int, value: Any) -> None:
if self.task is not None:
schedule(self.task, value, deadline)

@ -445,25 +445,58 @@ def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore
yield
if utils.MODEL == "1":
class RustLayout(Layout):
# pylint: disable=super-init-not-called
def __init__(self, layout: Any):
self.layout = layout
self.timer = loop.Timer()
self.layout.set_timer_fn(self.set_timer)
class RustLayout(Layout):
def __init__(self, layout: Any):
super().__init__()
self.layout = layout
self.layout.set_timer_fn(self.set_timer)
def set_timer(self, token: int, deadline: int) -> None:
self.timer.schedule(deadline, token)
def set_timer(self, token: int, deadline: int) -> None:
# TODO: schedule a timer tick with `token` in `deadline` ms
print("timer", token, deadline)
def create_tasks(self) -> tuple[loop.Task, ...]:
return self.handle_input_and_rendering(), self.handle_timers()
def dispatch(self, event: int, x: int, y: int) -> None:
msg = None
if event is RENDER:
if utils.MODEL == "T":
def handle_input_and_rendering(self) -> loop.Task: # type: ignore
touch = loop.wait(io.TOUCH)
display.clear()
self.layout.paint()
while True:
# Using `yield` instead of `await` to avoid allocations.
event, x, y = yield touch
workflow.idle_timer.touch()
msg = None
if event in (io.TOUCH_START, io.TOUCH_MOVE, io.TOUCH_END):
msg = self.layout.touch_event(event, x, y)
self.layout.paint()
if msg is not None:
raise Result(msg)
elif utils.MODEL == "1":
def handle_input_and_rendering(self) -> loop.Task: # type: ignore
button = loop.wait(io.BUTTON)
display.clear()
self.layout.paint()
while True:
# Using `yield` instead of `await` to avoid allocations.
event, button_num = yield button
workflow.idle_timer.touch()
msg = None
if event in (io.BUTTON_PRESSED, io.BUTTON_RELEASED):
msg = self.layout.button_event(event, button_num)
self.layout.paint()
elif event in (io.BUTTON_PRESSED, io.BUTTON_RELEASED):
msg = self.layout.button_event(event, x)
# elif event in (io.TOUCH_START, io.TOUCH_MOVE, io.TOUCH_END):
# self.layout.touch_event(event, x, y)
if msg is not None:
raise Result(msg)
def handle_timers(self) -> loop.Task: # type: ignore
while True:
# Using `yield` instead of `await` to avoid allocations.
token = yield self.timer
msg = self.layout.timer(token)
self.layout.paint()
if msg is not None:
raise Result(msg)

Loading…
Cancel
Save