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,
|
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()`.
|
||||||
|
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> {
|
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()
|
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)
|
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()
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
raise Result(msg)
|
raise Result(msg)
|
||||||
|
Loading…
Reference in New Issue
Block a user