diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index 5d851ef1a..d77a0a9a3 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -1,7 +1,8 @@ -use core::{mem, time::Duration}; +use core::mem; use heapless::Vec; +use crate::time::Duration; #[cfg(feature = "model_t1")] use crate::ui::model_t1::event::ButtonEvent; #[cfg(feature = "model_tt")] @@ -12,12 +13,20 @@ use crate::ui::model_tt::event::TouchEvent; /// Alternative to the yet-unstable `!`-type. pub enum Never {} +/// User interface is composed of components that can react to `Event`s through +/// the `event` method and know how to paint themselves to screen through the +/// `paint` method. Components can emit messages as a reaction to events. pub trait Component { type Msg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option; fn paint(&mut self); } +/// Components should always avoid unnecessary overpaint to prevent obvious +/// tearing and flickering. `Child` wraps an inner component `T` and keeps a +/// dirty flag for it. Any mutation of `T` has to happen through the `mutate` +/// accessor, `T` can then request a paint call to be scheduled later by calling +/// `EventCtx::request_paint` in its `event` pass. pub struct Child { component: T, marked_for_paint: bool, @@ -39,18 +48,26 @@ impl Child { self.component } + /// Access inner component mutably, track whether a paint call has been + /// requested, and propagate the flag upwards the component tree. pub fn mutate(&mut self, ctx: &mut EventCtx, component_func: F) -> U where F: FnOnce(&mut EventCtx, &mut T) -> U, { - let paint_was_previously_requested = mem::replace(&mut ctx.paint_requested, false); - let component_result = component_func(ctx, &mut self.component); + let prev_requested = mem::replace(&mut ctx.paint_requested, false); + let result = component_func(ctx, &mut self.component); if ctx.paint_requested { + // If a paint was requested anywhere in the inner component tree, we need to + // mark ourselves for paint as well, and keep the `ctx` flag so it can + // propagate upwards. self.marked_for_paint = true; } else { - ctx.paint_requested = paint_was_previously_requested; + // Paint has not been requested in the *inner* component, so there's no need to + // paint it, but we need to preserve the previous flag carried in `ctx` so it + // properly propagates upwards (i.e. from our previous siblings). + ctx.paint_requested = prev_requested; } - component_result + result } } @@ -61,7 +78,15 @@ where type Msg = T::Msg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - self.mutate(ctx, |ctx, c| c.event(ctx, event)) + self.mutate(ctx, |ctx, c| { + // Handle the internal invalidation event here, so components don't have to. We + // still pass it inside, so the event propagates correctly to all components in + // the sub-tree. + if let Event::RequestPaint = event { + ctx.request_paint(); + } + c.event(ctx, event) + }) } fn paint(&mut self) { @@ -82,13 +107,31 @@ where } } +pub trait ComponentExt: Sized { + fn into_child(self) -> Child; +} + +impl ComponentExt for T +where + T: Component, +{ + fn into_child(self) -> Child { + Child::new(self) + } +} + #[derive(Copy, Clone, PartialEq, Eq)] pub enum Event { #[cfg(feature = "model_t1")] Button(ButtonEvent), #[cfg(feature = "model_tt")] Touch(TouchEvent), + /// Previously requested timer was triggered. This invalidates the timer + /// token (another timer has to be requested). Timer(TimerToken), + /// Internally-handled event to inform all `Child` wrappers in a sub-tree to + /// get scheduled for painting. + RequestPaint, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -125,6 +168,9 @@ impl EventCtx { } } + /// Indicate that the inner state of the component has changed, any screen + /// content it has painted before is now invalid, and it should be painted + /// again by the nearest `Child` wrapper. pub fn request_paint(&mut self) { self.paint_requested = true; } @@ -133,12 +179,13 @@ impl EventCtx { 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(); if self.timers.push((token, deadline)).is_err() { // The timer queue is full. Let's just ignore this request. #[cfg(feature = "ui_debug")] - panic!("Timer queue is full"); + panic!("timer queue is full"); } token } diff --git a/core/embed/rust/src/ui/component/empty.rs b/core/embed/rust/src/ui/component/empty.rs index e87309029..ee13a184c 100644 --- a/core/embed/rust/src/ui/component/empty.rs +++ b/core/embed/rust/src/ui/component/empty.rs @@ -1,4 +1,4 @@ -use crate::ui::component::{Component, Event, EventCtx, Never}; +use super::{Component, Event, EventCtx, Never}; pub struct Empty; diff --git a/core/embed/rust/src/ui/component/map.rs b/core/embed/rust/src/ui/component/map.rs new file mode 100644 index 000000000..8def0b394 --- /dev/null +++ b/core/embed/rust/src/ui/component/map.rs @@ -0,0 +1,28 @@ +use super::{Component, Event, EventCtx}; + +pub struct Map { + inner: T, + func: F, +} + +impl Map { + pub fn new(inner: T, func: F) -> Self { + Self { inner, func } + } +} + +impl Component for Map +where + T: Component, + F: Fn(T::Msg) -> U, +{ + type Msg = U; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.inner.event(ctx, event).map(&self.func) + } + + fn paint(&mut self) { + self.inner.paint() + } +} diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index 49a0a40d8..cabc4c5ba 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -1,7 +1,9 @@ -mod base; +pub mod base; pub mod empty; pub mod label; +pub mod map; pub mod text; +pub mod tuple; pub use base::{Child, Component, Event, EventCtx, Never, TimerToken}; pub use empty::Empty; diff --git a/core/embed/rust/src/ui/component/text.rs b/core/embed/rust/src/ui/component/text.rs index f51e28ea9..05cd71387 100644 --- a/core/embed/rust/src/ui/component/text.rs +++ b/core/embed/rust/src/ui/component/text.rs @@ -38,7 +38,7 @@ impl Text { if self.args.insert(key, value).is_err() { // Map is full, ignore. #[cfg(feature = "ui_debug")] - panic!("Text args map is full"); + panic!("text args map is full"); } self } diff --git a/core/embed/rust/src/ui/component/tuple.rs b/core/embed/rust/src/ui/component/tuple.rs new file mode 100644 index 000000000..b2cb1db52 --- /dev/null +++ b/core/embed/rust/src/ui/component/tuple.rs @@ -0,0 +1,42 @@ +use super::{Component, Event, EventCtx}; + +impl Component for (A, B) +where + A: Component, + B: Component, +{ + type Msg = T; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.0 + .event(ctx, event) + .or_else(|| self.1.event(ctx, event)) + } + + fn paint(&mut self) { + self.0.paint(); + self.1.paint(); + } +} + +impl Component for (A, B, C) +where + A: Component, + B: Component, + C: Component, +{ + type Msg = T; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.0 + .event(ctx, event) + .or_else(|| self.1.event(ctx, event)) + .or_else(|| self.2.event(ctx, event)) + } + + fn paint(&mut self) { + self.0.paint(); + self.1.paint(); + self.2.paint(); + } +} diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 2623bd35a..94638df46 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -1,7 +1,6 @@ use core::{ cell::RefCell, convert::{TryFrom, TryInto}, - time::Duration, }; use crate::{ @@ -13,6 +12,7 @@ use crate::{ qstr::Qstr, typ::Type, }, + time::Duration, ui::component::{Child, Component, Event, EventCtx, Never, TimerToken}, util, }; @@ -268,7 +268,7 @@ impl TryFrom for Obj { type Error = Error; fn try_from(value: Duration) -> Result { - let millis: usize = value.as_millis().try_into()?; + let millis: usize = value.to_millis().try_into()?; millis.try_into() } } diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index bd87700f4..c7619c8d8 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -1,4 +1,4 @@ -use super::event::TouchEvent; +use super::{event::TouchEvent, theme}; use crate::ui::{ component::{Component, Event, EventCtx}, display::{self, Color, Font}, @@ -6,6 +6,8 @@ use crate::ui::{ }; pub enum ButtonMsg { + Pressed, + Released, Clicked, } @@ -17,21 +19,26 @@ pub struct Button { } impl Button { - pub fn new(area: Rect, content: ButtonContent, styles: ButtonStyleSheet) -> Self { + pub fn new(area: Rect, content: ButtonContent) -> Self { Self { area, content, - styles, + styles: theme::button_default(), state: State::Initial, } } - pub fn with_text(area: Rect, text: &'static [u8], styles: ButtonStyleSheet) -> Self { - Self::new(area, ButtonContent::Text(text), styles) + pub fn with_text(area: Rect, text: &'static [u8]) -> Self { + Self::new(area, ButtonContent::Text(text)) } - pub fn with_icon(area: Rect, image: &'static [u8], styles: ButtonStyleSheet) -> Self { - Self::new(area, ButtonContent::Icon(image), styles) + pub fn with_icon(area: Rect, image: &'static [u8]) -> Self { + Self::new(area, ButtonContent::Icon(image)) + } + + pub fn styled(mut self, styles: ButtonStyleSheet) -> Self { + self.styles = styles; + self } pub fn enable(&mut self, ctx: &mut EventCtx) { @@ -42,6 +49,14 @@ impl Button { self.set(ctx, State::Disabled) } + pub fn enabled(&mut self, ctx: &mut EventCtx, enabled: bool) { + if enabled { + self.enable(ctx); + } else { + self.disable(ctx); + } + } + pub fn is_enabled(&self) -> bool { matches!( self.state, @@ -87,6 +102,7 @@ impl Component for Button { // Touch started in our area, transform to `Pressed` state. if self.area.contains(pos) { self.set(ctx, State::Pressed); + return Some(ButtonMsg::Pressed); } } } @@ -96,10 +112,12 @@ impl Component for Button { State::Released if self.area.contains(pos) => { // Touch entered our area, transform to `Pressed` state. self.set(ctx, State::Pressed); + return Some(ButtonMsg::Pressed); } State::Pressed if !self.area.contains(pos) => { // Touch is leaving our area, transform to `Released` state. self.set(ctx, State::Released); + return Some(ButtonMsg::Released); } _ => { // Do nothing. @@ -114,7 +132,6 @@ impl Component for Button { State::Pressed if self.area.contains(pos) => { // Touch finished in our area, we got clicked. self.set(ctx, State::Initial); - return Some(ButtonMsg::Clicked); } _ => { diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index fb8c154d5..f290e45e4 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -1,80 +1,96 @@ use crate::ui::{ - component::{Child, Component, Event, EventCtx}, + component::{base::ComponentExt, Child, Component, Event, EventCtx}, geometry::{Grid, Rect}, }; -use super::button::{Button, ButtonMsg::Clicked}; - -pub enum DialogMsg { +pub enum DialogMsg { Content(T), - LeftClicked, - RightClicked, + Left(L), + Right(R), } -pub struct Dialog { +pub struct Dialog { content: Child, - left_btn: Option>, - right_btn: Option>, + left: Child, + right: Child, } -impl Dialog { +impl Dialog +where + T: Component, + L: Component, + R: Component, +{ pub fn new( area: Rect, content: impl FnOnce(Rect) -> T, - left: impl FnOnce(Rect) -> Button, - right: impl FnOnce(Rect) -> Button, + left: impl FnOnce(Rect) -> L, + right: impl FnOnce(Rect) -> R, ) -> Self { - let grid = Grid::new(area, 5, 2); - let content = Child::new(content(Rect::new( - grid.row_col(0, 0).top_left(), - grid.row_col(4, 1).bottom_right(), - ))); - let left_btn = Child::new(left(grid.row_col(4, 0))); - let right_btn = Child::new(right(grid.row_col(4, 1))); + let layout = DialogLayout::middle(area); Self { - content, - left_btn: Some(left_btn), - right_btn: Some(right_btn), + content: content(layout.content).into_child(), + left: left(layout.left).into_child(), + right: right(layout.right).into_child(), } } } -impl Component for Dialog { - type Msg = DialogMsg; +impl Component for Dialog +where + T: Component, + L: Component, + R: Component, +{ + type Msg = DialogMsg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if let Some(msg) = self.content.event(ctx, event) { - Some(DialogMsg::Content(msg)) - } else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::LeftClicked) - } else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::RightClicked) - } else { - None - } + self.content + .event(ctx, event) + .map(Self::Msg::Content) + .or_else(|| self.left.event(ctx, event).map(Self::Msg::Left)) + .or_else(|| self.right.event(ctx, event).map(Self::Msg::Right)) } fn paint(&mut self) { self.content.paint(); - if let Some(b) = self.left_btn.as_mut() { - b.paint(); - } - if let Some(b) = self.right_btn.as_mut() { - b.paint(); + self.left.paint(); + self.right.paint(); + } +} + +struct DialogLayout { + content: Rect, + left: Rect, + right: Rect, +} + +impl DialogLayout { + 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(), + ), + left: grid.row_col(4, 0), + right: grid.row_col(4, 1), } } } #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Dialog +impl crate::trace::Trace for Dialog where T: crate::trace::Trace, + L: crate::trace::Trace, + R: crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("Dialog"); t.field("content", &self.content); - t.field("left", &self.left_btn); - t.field("right", &self.right_btn); + t.field("left", &self.left); + t.field("right", &self.right); t.close(); } } diff --git a/core/embed/rust/src/ui/model_tt/component/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/passphrase.rs index 4b2d4b200..6ad7f1915 100644 --- a/core/embed/rust/src/ui/model_tt/component/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/passphrase.rs @@ -1,11 +1,12 @@ -use core::time::Duration; - use heapless::Vec; -use crate::ui::{ - component::{Child, Component, Event, EventCtx, Never, TimerToken}, - display, - geometry::{Grid, Rect}, +use crate::{ + time::Duration, + ui::{ + component::{base::ComponentExt, Child, Component, Event, EventCtx, Never, TimerToken}, + display, + geometry::{Grid, Rect}, + }, }; use super::{ @@ -35,10 +36,18 @@ struct Pending { timer: TimerToken, } -const MAX_LENGTH: usize = 50; const STARTING_PAGE: usize = 1; const PAGES: usize = 4; const KEYS: usize = 10; +#[rustfmt::skip] +const KEYBOARD: [[&str; KEYS]; PAGES] = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + [" ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"], + [" ", "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"], + ["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="], + ]; + +const MAX_LENGTH: usize = 50; const PENDING_DEADLINE: Duration = Duration::from_secs(1); impl PassphraseKeyboard { @@ -50,17 +59,13 @@ impl PassphraseKeyboard { let text = Vec::new(); let page_swipe = Swipe::horizontal(area); - let textbox = Child::new(TextBox::new(textbox_area, text)); - let confirm_btn = Child::new(Button::with_text( - confirm_btn_area, - b"Confirm", - theme::button_confirm(), - )); - let back_btn = Child::new(Button::with_text( - back_btn_area, - b"Back", - theme::button_clear(), - )); + let textbox = TextBox::new(textbox_area, text).into_child(); + let confirm_btn = Button::with_text(confirm_btn_area, b"Confirm") + .styled(theme::button_confirm()) + .into_child(); + let back_btn = Button::with_text(back_btn_area, b"Back") + .styled(theme::button_clear()) + .into_child(); let key_btns = Self::generate_keyboard(&key_grid); Self { @@ -75,38 +80,16 @@ impl PassphraseKeyboard { } fn generate_keyboard(grid: &Grid) -> [[Child