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, util,
}; };
use super::error;
use super::{ use super::{
defs::{self, FieldDef, FieldType, MsgDef}, defs::{self, FieldDef, FieldType, MsgDef},
error,
obj::{MsgDefObj, MsgObj}, obj::{MsgDefObj, MsgObj},
zigzag, zigzag,
}; };

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

@ -109,6 +109,7 @@ where
pub trait ComponentExt: Sized { pub trait ComponentExt: Sized {
fn into_child(self) -> Child<Self>; fn into_child(self) -> Child<Self>;
fn request_complete_repaint(&mut self, ctx: &mut EventCtx);
} }
impl<T> ComponentExt for T impl<T> ComponentExt for T
@ -118,6 +119,15 @@ where
fn into_child(self) -> Child<Self> { fn into_child(self) -> Child<Self> {
Child::new(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)] #[derive(Copy, Clone, PartialEq, Eq)]
@ -154,12 +164,16 @@ pub struct EventCtx {
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
next_token: u32, next_token: u32,
paint_requested: bool, paint_requested: bool,
anim_frame_scheduled: bool,
} }
impl EventCtx { impl EventCtx {
/// Timer token dedicated for animation frames. /// Timer token dedicated for animation frames.
pub const ANIM_FRAME_TIMER: TimerToken = TimerToken(1); 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`, // 0 == `TimerToken::INVALID`,
// 1 == `Self::ANIM_FRAME_TIMER`. // 1 == `Self::ANIM_FRAME_TIMER`.
const STARTING_TIMER_TOKEN: u32 = 2; const STARTING_TIMER_TOKEN: u32 = 2;
@ -172,6 +186,7 @@ impl EventCtx {
timers: Vec::new(), timers: Vec::new(),
next_token: Self::STARTING_TIMER_TOKEN, next_token: Self::STARTING_TIMER_TOKEN,
paint_requested: false, paint_requested: false,
anim_frame_scheduled: false,
} }
} }
@ -182,10 +197,6 @@ impl EventCtx {
self.paint_requested = true; 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. /// Request a timer event to be delivered after `deadline` elapses.
pub fn request_timer(&mut self, deadline: Duration) -> TimerToken { pub fn request_timer(&mut self, deadline: Duration) -> TimerToken {
let token = self.next_timer_token(); let token = self.next_timer_token();
@ -195,13 +206,21 @@ impl EventCtx {
/// Request an animation frame timer to fire as soon as possible. /// Request an animation frame timer to fire as soon as possible.
pub fn request_anim_frame(&mut self) { 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)> { pub fn pop_timer(&mut self) -> Option<(TimerToken, Duration)> {
self.timers.pop() 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) { fn register_timer(&mut self, token: TimerToken, deadline: Duration) {
if self.timers.push((token, deadline)).is_err() { if self.timers.push((token, deadline)).is_err() {
// The timer queue is full, this would be a development error in the layout // 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 text;
pub mod tuple; 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 empty::Empty;
pub use label::{Label, LabelStyle}; pub use label::{Label, LabelStyle};
pub use text::{ pub use text::{

@ -128,8 +128,8 @@ impl LayoutObj {
fn obj_event(&self, event: Event) -> Result<Obj, Error> { fn obj_event(&self, event: Event) -> Result<Obj, Error> {
let inner = &mut *self.inner.borrow_mut(); let inner = &mut *self.inner.borrow_mut();
// Clear the upwards-propagating paint request flag from the last event pass. // Clear the leftover flags from the previous event pass.
inner.event_ctx.clear_paint_requests(); inner.event_ctx.clear();
// Send the event down the component tree. Bail out in case of failure. // Send the event down the component tree. Bail out in case of failure.
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. // 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> { pub struct Dialog<T, L, R> {
content: Child<T>, pub content: Child<T>,
left: Child<L>, pub left: Child<L>,
right: Child<R>, pub right: Child<R>,
} }
impl<T, L, R> Dialog<T, L, R> impl<T, L, R> Dialog<T, L, R>
@ -59,19 +59,19 @@ where
} }
} }
struct DialogLayout { pub struct DialogLayout {
content: Rect, pub content: Rect,
left: Rect, pub left: Rect,
right: Rect, pub right: Rect,
} }
impl DialogLayout { impl DialogLayout {
fn middle(area: Rect) -> Self { pub fn middle(area: Rect) -> Self {
let grid = Grid::new(area, 5, 2); let grid = Grid::new(area, 5, 2);
Self { Self {
content: Rect::new( content: Rect::new(
grid.row_col(0, 0).top_left(), 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), left: grid.row_col(4, 0),
right: grid.row_col(4, 1), right: grid.row_col(4, 1),

@ -2,13 +2,19 @@ use crate::{
time::{Duration, Instant}, time::{Duration, Instant},
ui::{ ui::{
animation::Animation, animation::Animation,
component::{Component, Event, EventCtx, Never}, component::{Component, Event, EventCtx},
display::{self, Color}, display::{self, Color},
geometry::Offset,
}, },
}; };
use super::theme; use super::theme;
pub enum LoaderMsg {
GrownCompletely,
ShrunkCompletely,
}
enum State { enum State {
Initial, Initial,
Growing(Animation<u16>), Growing(Animation<u16>),
@ -24,6 +30,8 @@ pub struct Loader {
} }
impl Loader { impl Loader {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new(offset_y: i32) -> Self { pub fn new(offset_y: i32) -> Self {
Self { Self {
offset_y, 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( let mut anim = Animation::new(
display::LOADER_MIN, display::LOADER_MIN,
display::LOADER_MAX, display::LOADER_MAX,
@ -45,9 +53,16 @@ impl Loader {
anim.seek_to_value(shrinking.value(now)); anim.seek_to_value(shrinking.value(now));
} }
self.state = State::Growing(anim); 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( let mut anim = Animation::new(
display::LOADER_MAX, display::LOADER_MAX,
display::LOADER_MIN, display::LOADER_MIN,
@ -55,46 +70,77 @@ impl Loader {
now, now,
); );
if let State::Growing(growing) = &self.state { 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); 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) { pub fn reset(&mut self) {
self.state = State::Initial; self.state = State::Initial;
} }
pub fn progress(&self, now: Instant) -> Option<u16> { pub fn animation(&self) -> Option<&Animation<u16>> {
match &self.state { match &self.state {
State::Initial => None, 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 { pub fn progress(&self, now: Instant) -> Option<u16> {
matches!(self.state, State::Growing(_) | State::Shrinking(_)) self.animation().map(|a| a.value(now))
} }
pub fn is_finished(&self, now: Instant) -> bool { pub fn is_animating(&self) -> bool {
self.progress(now) == Some(display::LOADER_MAX) 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 { impl Component for Loader {
type Msg = Never; type Msg = LoaderMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { 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 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_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 None
} }
fn paint(&mut self) { 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 { let style = if progress < display::LOADER_MAX {
self.styles.normal self.styles.normal
} else { } else {
@ -122,20 +168,29 @@ pub struct LoaderStyle {
pub background_color: Color, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn loader_yields_expected_progress() { fn loader_yields_expected_progress() {
let mut ctx = EventCtx::new();
let mut l = Loader::new(0); let mut l = Loader::new(0);
let t = Instant::now(); let t = Instant::now();
assert_eq!(l.progress(t), None); assert_eq!(l.progress(t), None);
l.start(t); l.start_growing(&mut ctx, t);
assert_eq!(l.progress(t), Some(0)); assert_eq!(l.progress(t), Some(0));
let t = add_millis(t, 500); let t = add_millis(t, 500);
assert_eq!(l.progress(t), Some(500)); assert_eq!(l.progress(t), Some(500));
l.stop(t); l.start_shrinking(&mut ctx, t);
assert_eq!(l.progress(t), Some(500)); assert_eq!(l.progress(t), Some(500));
let t = add_millis(t, 125); let t = add_millis(t, 125);
assert_eq!(l.progress(t), Some(250)); assert_eq!(l.progress(t), Some(250));

@ -1,4 +1,5 @@
mod button; mod button;
mod confirm;
mod dialog; mod dialog;
mod loader; mod loader;
mod page; mod page;
@ -7,7 +8,9 @@ mod pin;
mod swipe; mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet}; 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}; pub use swipe::{Swipe, SwipeDirection};
use super::{event, theme}; use super::{event, theme};

@ -2,7 +2,7 @@ use core::convert::{TryFrom, TryInto};
use crate::{ use crate::{
error::Error, error::Error,
micropython::{buffer::Buffer, obj::Obj}, micropython::obj::Obj,
ui::{ ui::{
component::{Child, FormattedText}, component::{Child, FormattedText},
display, display,
@ -12,7 +12,7 @@ use crate::{
}; };
use super::{ use super::{
component::{Button, ButtonMsg, Dialog, DialogMsg}, component::{ButtonMsg, DialogMsg, HoldToConfirm, HoldToConfirmMsg},
theme, 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] #[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 block = move || {
let param: Buffer = param.try_into()?; let layout = LayoutObj::new(Child::new(HoldToConfirm::new(display::screen(), |area| {
let layout = LayoutObj::new(Child::new(Dialog::new( FormattedText::new::<theme::TTDefaultText>(
display::screen(), area,
|area| { "Testing text layout, with some text, and some more text. And {param}",
FormattedText::new::<theme::TTDefaultText>(area, param) )
.with(b"some", "a few") .with(b"param", b"parameters!")
.with(b"param", "xx") })))?;
},
|area| Button::with_text(area, b"Left"),
|area| Button::with_text(area, b"Right"),
)))?;
Ok(layout.into()) Ok(layout.into())
}; };
unsafe { util::try_or_raise(block) } unsafe { util::try_or_raise(block) }
@ -54,7 +66,10 @@ extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::trace::{Trace, Tracer}; use crate::{
trace::{Trace, Tracer},
ui::model_tt::component::{Button, Dialog},
};
use super::*; use super::*;
@ -64,15 +79,15 @@ mod tests {
} }
fn bytes(&mut self, b: &[u8]) { fn bytes(&mut self, b: &[u8]) {
self.extend(b) self.extend(b);
} }
fn string(&mut self, s: &str) { fn string(&mut self, s: &str) {
self.extend(s.as_bytes()) self.extend(s.as_bytes());
} }
fn symbol(&mut self, name: &str) { fn symbol(&mut self, name: &str) {
self.extend(name.as_bytes()) self.extend(name.as_bytes());
} }
fn open(&mut self, name: &str) { fn open(&mut self, name: &str) {
@ -89,7 +104,7 @@ mod tests {
} }
fn close(&mut self) { 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. is True, it would be calling close on self, which will result in a ValueError.
""" """
return self.task is this_task 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 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 set_timer(self, token: int, deadline: int) -> None:
def __init__(self, layout: Any): self.timer.schedule(deadline, token)
super().__init__()
self.layout = layout
self.layout.set_timer_fn(self.set_timer)
def set_timer(self, token: int, deadline: int) -> None: def create_tasks(self) -> tuple[loop.Task, ...]:
# TODO: schedule a timer tick with `token` in `deadline` ms return self.handle_input_and_rendering(), self.handle_timers()
print("timer", token, deadline)
def dispatch(self, event: int, x: int, y: int) -> None: if utils.MODEL == "T":
msg = None
if event is RENDER: 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() self.layout.paint()
elif event in (io.BUTTON_PRESSED, io.BUTTON_RELEASED): if msg is not None:
msg = self.layout.button_event(event, x) raise Result(msg)
# elif event in (io.TOUCH_START, io.TOUCH_MOVE, io.TOUCH_END):
# self.layout.touch_event(event, x, y) 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: if msg is not None:
raise Result(msg) raise Result(msg)

Loading…
Cancel
Save