mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-27 06:42:02 +00:00
feat(core): Add HoldToConfirm example, improve Loader and animation frame support
This commit is contained in:
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()`.
|
||||
|
140
core/embed/rust/src/ui/model_tt/component/confirm.rs
Normal file
140
core/embed/rust/src/ui/model_tt/component/confirm.rs
Normal file
@ -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();
|
||||
|
||||
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):
|
||||
class RustLayout(Layout):
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(self, layout: Any):
|
||||
super().__init__()
|
||||
self.layout = layout
|
||||
self.timer = loop.Timer()
|
||||
self.layout.set_timer_fn(self.set_timer)
|
||||
|
||||
def set_timer(self, token: int, deadline: int) -> None:
|
||||
# TODO: schedule a timer tick with `token` in `deadline` ms
|
||||
print("timer", token, deadline)
|
||||
self.timer.schedule(deadline, token)
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
msg = None
|
||||
if event is RENDER:
|
||||
def create_tasks(self) -> tuple[loop.Task, ...]:
|
||||
return self.handle_input_and_rendering(), self.handle_timers()
|
||||
|
||||
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()
|
||||
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()
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user