mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-01 20:32:35 +00:00
chore(core): Various little improvements in Rust UI
This commit is contained in:
parent
998210d569
commit
39263144b7
@ -1,7 +1,8 @@
|
|||||||
use core::{mem, time::Duration};
|
use core::mem;
|
||||||
|
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
|
use crate::time::Duration;
|
||||||
#[cfg(feature = "model_t1")]
|
#[cfg(feature = "model_t1")]
|
||||||
use crate::ui::model_t1::event::ButtonEvent;
|
use crate::ui::model_t1::event::ButtonEvent;
|
||||||
#[cfg(feature = "model_tt")]
|
#[cfg(feature = "model_tt")]
|
||||||
@ -12,12 +13,20 @@ use crate::ui::model_tt::event::TouchEvent;
|
|||||||
/// Alternative to the yet-unstable `!`-type.
|
/// Alternative to the yet-unstable `!`-type.
|
||||||
pub enum Never {}
|
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 {
|
pub trait Component {
|
||||||
type Msg;
|
type Msg;
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg>;
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg>;
|
||||||
fn paint(&mut self);
|
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<T> {
|
pub struct Child<T> {
|
||||||
component: T,
|
component: T,
|
||||||
marked_for_paint: bool,
|
marked_for_paint: bool,
|
||||||
@ -39,18 +48,26 @@ impl<T> Child<T> {
|
|||||||
self.component
|
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<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U
|
pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut EventCtx, &mut T) -> U,
|
F: FnOnce(&mut EventCtx, &mut T) -> U,
|
||||||
{
|
{
|
||||||
let paint_was_previously_requested = mem::replace(&mut ctx.paint_requested, false);
|
let prev_requested = mem::replace(&mut ctx.paint_requested, false);
|
||||||
let component_result = component_func(ctx, &mut self.component);
|
let result = component_func(ctx, &mut self.component);
|
||||||
if ctx.paint_requested {
|
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;
|
self.marked_for_paint = true;
|
||||||
} else {
|
} 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;
|
type Msg = T::Msg;
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
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) {
|
fn paint(&mut self) {
|
||||||
@ -82,13 +107,31 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ComponentExt: Sized {
|
||||||
|
fn into_child(self) -> Child<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ComponentExt for T
|
||||||
|
where
|
||||||
|
T: Component,
|
||||||
|
{
|
||||||
|
fn into_child(self) -> Child<Self> {
|
||||||
|
Child::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
#[cfg(feature = "model_t1")]
|
#[cfg(feature = "model_t1")]
|
||||||
Button(ButtonEvent),
|
Button(ButtonEvent),
|
||||||
#[cfg(feature = "model_tt")]
|
#[cfg(feature = "model_tt")]
|
||||||
Touch(TouchEvent),
|
Touch(TouchEvent),
|
||||||
|
/// Previously requested timer was triggered. This invalidates the timer
|
||||||
|
/// token (another timer has to be requested).
|
||||||
Timer(TimerToken),
|
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)]
|
#[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) {
|
pub fn request_paint(&mut self) {
|
||||||
self.paint_requested = true;
|
self.paint_requested = true;
|
||||||
}
|
}
|
||||||
@ -133,12 +179,13 @@ impl EventCtx {
|
|||||||
self.paint_requested = false;
|
self.paint_requested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
if self.timers.push((token, deadline)).is_err() {
|
if self.timers.push((token, deadline)).is_err() {
|
||||||
// The timer queue is full. Let's just ignore this request.
|
// The timer queue is full. Let's just ignore this request.
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
panic!("Timer queue is full");
|
panic!("timer queue is full");
|
||||||
}
|
}
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::ui::component::{Component, Event, EventCtx, Never};
|
use super::{Component, Event, EventCtx, Never};
|
||||||
|
|
||||||
pub struct Empty;
|
pub struct Empty;
|
||||||
|
|
||||||
|
28
core/embed/rust/src/ui/component/map.rs
Normal file
28
core/embed/rust/src/ui/component/map.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use super::{Component, Event, EventCtx};
|
||||||
|
|
||||||
|
pub struct Map<T, F> {
|
||||||
|
inner: T,
|
||||||
|
func: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> Map<T, F> {
|
||||||
|
pub fn new(inner: T, func: F) -> Self {
|
||||||
|
Self { inner, func }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F, U> Component for Map<T, F>
|
||||||
|
where
|
||||||
|
T: Component,
|
||||||
|
F: Fn(T::Msg) -> U,
|
||||||
|
{
|
||||||
|
type Msg = U;
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
self.inner.event(ctx, event).map(&self.func)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
self.inner.paint()
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
mod base;
|
pub mod base;
|
||||||
pub mod empty;
|
pub mod empty;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
|
pub mod map;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod tuple;
|
||||||
|
|
||||||
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
|
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
|
||||||
pub use empty::Empty;
|
pub use empty::Empty;
|
||||||
|
@ -38,7 +38,7 @@ impl<F, T> Text<F, T> {
|
|||||||
if self.args.insert(key, value).is_err() {
|
if self.args.insert(key, value).is_err() {
|
||||||
// Map is full, ignore.
|
// Map is full, ignore.
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
panic!("Text args map is full");
|
panic!("text args map is full");
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
42
core/embed/rust/src/ui/component/tuple.rs
Normal file
42
core/embed/rust/src/ui/component/tuple.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use super::{Component, Event, EventCtx};
|
||||||
|
|
||||||
|
impl<T, A, B> Component for (A, B)
|
||||||
|
where
|
||||||
|
A: Component<Msg = T>,
|
||||||
|
B: Component<Msg = T>,
|
||||||
|
{
|
||||||
|
type Msg = T;
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
self.0
|
||||||
|
.event(ctx, event)
|
||||||
|
.or_else(|| self.1.event(ctx, event))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
self.0.paint();
|
||||||
|
self.1.paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, A, B, C> Component for (A, B, C)
|
||||||
|
where
|
||||||
|
A: Component<Msg = T>,
|
||||||
|
B: Component<Msg = T>,
|
||||||
|
C: Component<Msg = T>,
|
||||||
|
{
|
||||||
|
type Msg = T;
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
use core::{
|
use core::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
convert::{TryFrom, TryInto},
|
convert::{TryFrom, TryInto},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -13,6 +12,7 @@ use crate::{
|
|||||||
qstr::Qstr,
|
qstr::Qstr,
|
||||||
typ::Type,
|
typ::Type,
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
ui::component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
ui::component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
@ -268,7 +268,7 @@ impl TryFrom<Duration> for Obj {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: Duration) -> Result<Self, Self::Error> {
|
fn try_from(value: Duration) -> Result<Self, Self::Error> {
|
||||||
let millis: usize = value.as_millis().try_into()?;
|
let millis: usize = value.to_millis().try_into()?;
|
||||||
millis.try_into()
|
millis.try_into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::event::TouchEvent;
|
use super::{event::TouchEvent, theme};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
@ -6,6 +6,8 @@ use crate::ui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub enum ButtonMsg {
|
pub enum ButtonMsg {
|
||||||
|
Pressed,
|
||||||
|
Released,
|
||||||
Clicked,
|
Clicked,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,21 +19,26 @@ pub struct Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
pub fn new(area: Rect, content: ButtonContent, styles: ButtonStyleSheet) -> Self {
|
pub fn new(area: Rect, content: ButtonContent) -> Self {
|
||||||
Self {
|
Self {
|
||||||
area,
|
area,
|
||||||
content,
|
content,
|
||||||
styles,
|
styles: theme::button_default(),
|
||||||
state: State::Initial,
|
state: State::Initial,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_text(area: Rect, text: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
pub fn with_text(area: Rect, text: &'static [u8]) -> Self {
|
||||||
Self::new(area, ButtonContent::Text(text), styles)
|
Self::new(area, ButtonContent::Text(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_icon(area: Rect, image: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
pub fn with_icon(area: Rect, image: &'static [u8]) -> Self {
|
||||||
Self::new(area, ButtonContent::Icon(image), styles)
|
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) {
|
pub fn enable(&mut self, ctx: &mut EventCtx) {
|
||||||
@ -42,6 +49,14 @@ impl Button {
|
|||||||
self.set(ctx, State::Disabled)
|
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 {
|
pub fn is_enabled(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.state,
|
self.state,
|
||||||
@ -87,6 +102,7 @@ impl Component for Button {
|
|||||||
// Touch started in our area, transform to `Pressed` state.
|
// Touch started in our area, transform to `Pressed` state.
|
||||||
if self.area.contains(pos) {
|
if self.area.contains(pos) {
|
||||||
self.set(ctx, State::Pressed);
|
self.set(ctx, State::Pressed);
|
||||||
|
return Some(ButtonMsg::Pressed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,10 +112,12 @@ impl Component for Button {
|
|||||||
State::Released if self.area.contains(pos) => {
|
State::Released if self.area.contains(pos) => {
|
||||||
// Touch entered our area, transform to `Pressed` state.
|
// Touch entered our area, transform to `Pressed` state.
|
||||||
self.set(ctx, State::Pressed);
|
self.set(ctx, State::Pressed);
|
||||||
|
return Some(ButtonMsg::Pressed);
|
||||||
}
|
}
|
||||||
State::Pressed if !self.area.contains(pos) => {
|
State::Pressed if !self.area.contains(pos) => {
|
||||||
// Touch is leaving our area, transform to `Released` state.
|
// Touch is leaving our area, transform to `Released` state.
|
||||||
self.set(ctx, State::Released);
|
self.set(ctx, State::Released);
|
||||||
|
return Some(ButtonMsg::Released);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
@ -114,7 +132,6 @@ impl Component for Button {
|
|||||||
State::Pressed if self.area.contains(pos) => {
|
State::Pressed if self.area.contains(pos) => {
|
||||||
// Touch finished in our area, we got clicked.
|
// Touch finished in our area, we got clicked.
|
||||||
self.set(ctx, State::Initial);
|
self.set(ctx, State::Initial);
|
||||||
|
|
||||||
return Some(ButtonMsg::Clicked);
|
return Some(ButtonMsg::Clicked);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -1,80 +1,96 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Child, Component, Event, EventCtx},
|
component::{base::ComponentExt, Child, Component, Event, EventCtx},
|
||||||
geometry::{Grid, Rect},
|
geometry::{Grid, Rect},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::button::{Button, ButtonMsg::Clicked};
|
pub enum DialogMsg<T, L, R> {
|
||||||
|
|
||||||
pub enum DialogMsg<T> {
|
|
||||||
Content(T),
|
Content(T),
|
||||||
LeftClicked,
|
Left(L),
|
||||||
RightClicked,
|
Right(R),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Dialog<T> {
|
pub struct Dialog<T, L, R> {
|
||||||
content: Child<T>,
|
content: Child<T>,
|
||||||
left_btn: Option<Child<Button>>,
|
left: Child<L>,
|
||||||
right_btn: Option<Child<Button>>,
|
right: Child<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Component> Dialog<T> {
|
impl<T, L, R> Dialog<T, L, R>
|
||||||
|
where
|
||||||
|
T: Component,
|
||||||
|
L: Component,
|
||||||
|
R: Component,
|
||||||
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
area: Rect,
|
area: Rect,
|
||||||
content: impl FnOnce(Rect) -> T,
|
content: impl FnOnce(Rect) -> T,
|
||||||
left: impl FnOnce(Rect) -> Button,
|
left: impl FnOnce(Rect) -> L,
|
||||||
right: impl FnOnce(Rect) -> Button,
|
right: impl FnOnce(Rect) -> R,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let grid = Grid::new(area, 5, 2);
|
let layout = DialogLayout::middle(area);
|
||||||
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)));
|
|
||||||
Self {
|
Self {
|
||||||
content,
|
content: content(layout.content).into_child(),
|
||||||
left_btn: Some(left_btn),
|
left: left(layout.left).into_child(),
|
||||||
right_btn: Some(right_btn),
|
right: right(layout.right).into_child(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Component> Component for Dialog<T> {
|
impl<T, L, R> Component for Dialog<T, L, R>
|
||||||
type Msg = DialogMsg<T::Msg>;
|
where
|
||||||
|
T: Component,
|
||||||
|
L: Component,
|
||||||
|
R: Component,
|
||||||
|
{
|
||||||
|
type Msg = DialogMsg<T::Msg, L::Msg, R::Msg>;
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
if let Some(msg) = self.content.event(ctx, event) {
|
self.content
|
||||||
Some(DialogMsg::Content(msg))
|
.event(ctx, event)
|
||||||
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
.map(Self::Msg::Content)
|
||||||
Some(DialogMsg::LeftClicked)
|
.or_else(|| self.left.event(ctx, event).map(Self::Msg::Left))
|
||||||
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
.or_else(|| self.right.event(ctx, event).map(Self::Msg::Right))
|
||||||
Some(DialogMsg::RightClicked)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
if let Some(b) = self.left_btn.as_mut() {
|
self.left.paint();
|
||||||
b.paint();
|
self.right.paint();
|
||||||
}
|
}
|
||||||
if let Some(b) = self.right_btn.as_mut() {
|
}
|
||||||
b.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")]
|
#[cfg(feature = "ui_debug")]
|
||||||
impl<T> crate::trace::Trace for Dialog<T>
|
impl<T, L, R> crate::trace::Trace for Dialog<T, L, R>
|
||||||
where
|
where
|
||||||
T: crate::trace::Trace,
|
T: crate::trace::Trace,
|
||||||
|
L: crate::trace::Trace,
|
||||||
|
R: crate::trace::Trace,
|
||||||
{
|
{
|
||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
t.open("Dialog");
|
t.open("Dialog");
|
||||||
t.field("content", &self.content);
|
t.field("content", &self.content);
|
||||||
t.field("left", &self.left_btn);
|
t.field("left", &self.left);
|
||||||
t.field("right", &self.right_btn);
|
t.field("right", &self.right);
|
||||||
t.close();
|
t.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use core::time::Duration;
|
|
||||||
|
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
use crate::ui::{
|
use crate::{
|
||||||
component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
time::Duration,
|
||||||
|
ui::{
|
||||||
|
component::{base::ComponentExt, Child, Component, Event, EventCtx, Never, TimerToken},
|
||||||
display,
|
display,
|
||||||
geometry::{Grid, Rect},
|
geometry::{Grid, Rect},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -35,10 +36,18 @@ struct Pending {
|
|||||||
timer: TimerToken,
|
timer: TimerToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LENGTH: usize = 50;
|
|
||||||
const STARTING_PAGE: usize = 1;
|
const STARTING_PAGE: usize = 1;
|
||||||
const PAGES: usize = 4;
|
const PAGES: usize = 4;
|
||||||
const KEYS: usize = 10;
|
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);
|
const PENDING_DEADLINE: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
impl PassphraseKeyboard {
|
impl PassphraseKeyboard {
|
||||||
@ -50,17 +59,13 @@ impl PassphraseKeyboard {
|
|||||||
|
|
||||||
let text = Vec::new();
|
let text = Vec::new();
|
||||||
let page_swipe = Swipe::horizontal(area);
|
let page_swipe = Swipe::horizontal(area);
|
||||||
let textbox = Child::new(TextBox::new(textbox_area, text));
|
let textbox = TextBox::new(textbox_area, text).into_child();
|
||||||
let confirm_btn = Child::new(Button::with_text(
|
let confirm_btn = Button::with_text(confirm_btn_area, b"Confirm")
|
||||||
confirm_btn_area,
|
.styled(theme::button_confirm())
|
||||||
b"Confirm",
|
.into_child();
|
||||||
theme::button_confirm(),
|
let back_btn = Button::with_text(back_btn_area, b"Back")
|
||||||
));
|
.styled(theme::button_clear())
|
||||||
let back_btn = Child::new(Button::with_text(
|
.into_child();
|
||||||
back_btn_area,
|
|
||||||
b"Back",
|
|
||||||
theme::button_clear(),
|
|
||||||
));
|
|
||||||
let key_btns = Self::generate_keyboard(&key_grid);
|
let key_btns = Self::generate_keyboard(&key_grid);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -75,38 +80,16 @@ impl PassphraseKeyboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate_keyboard(grid: &Grid) -> [[Child<Button>; KEYS]; PAGES] {
|
fn generate_keyboard(grid: &Grid) -> [[Child<Button>; KEYS]; PAGES] {
|
||||||
[
|
// can't use a range because the result is a fixed-size array
|
||||||
Self::generate_key_page(grid, 0),
|
[0, 1, 2, 3].map(|i| Self::generate_key_page(grid, i))
|
||||||
Self::generate_key_page(grid, 1),
|
|
||||||
Self::generate_key_page(grid, 2),
|
|
||||||
Self::generate_key_page(grid, 3),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_key_page(grid: &Grid, page: usize) -> [Child<Button>; KEYS] {
|
fn generate_key_page(grid: &Grid, page: usize) -> [Child<Button>; KEYS] {
|
||||||
[
|
// can't use a range because the result is a fixed-size array
|
||||||
Self::generate_key(grid, page, 0),
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(|i| Self::generate_key(grid, page, i))
|
||||||
Self::generate_key(grid, page, 1),
|
|
||||||
Self::generate_key(grid, page, 2),
|
|
||||||
Self::generate_key(grid, page, 3),
|
|
||||||
Self::generate_key(grid, page, 4),
|
|
||||||
Self::generate_key(grid, page, 5),
|
|
||||||
Self::generate_key(grid, page, 6),
|
|
||||||
Self::generate_key(grid, page, 7),
|
|
||||||
Self::generate_key(grid, page, 8),
|
|
||||||
Self::generate_key(grid, page, 9),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_key(grid: &Grid, page: usize, key: usize) -> Child<Button> {
|
fn generate_key(grid: &Grid, page: usize, key: usize) -> Child<Button> {
|
||||||
#[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", "*#"],
|
|
||||||
["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
||||||
// second row.
|
// second row.
|
||||||
let area = grid.cell(if key < 9 {
|
let area = grid.cell(if key < 9 {
|
||||||
@ -119,9 +102,9 @@ impl PassphraseKeyboard {
|
|||||||
let text = KEYBOARD[page][key].as_bytes();
|
let text = KEYBOARD[page][key].as_bytes();
|
||||||
if text == b" " {
|
if text == b" " {
|
||||||
let icon = theme::ICON_SPACE;
|
let icon = theme::ICON_SPACE;
|
||||||
Child::new(Button::with_icon(area, icon, theme::button_default()))
|
Child::new(Button::with_icon(area, icon))
|
||||||
} else {
|
} else {
|
||||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
Child::new(Button::with_text(area, text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +266,7 @@ impl TextBox {
|
|||||||
if self.text.push(char).is_err() {
|
if self.text.push(char).is_err() {
|
||||||
// Should not happen unless `self.text` has zero capacity.
|
// Should not happen unless `self.text` has zero capacity.
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
panic!("Textbox has zero capacity");
|
panic!("textbox has zero capacity");
|
||||||
}
|
}
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
@ -292,7 +275,7 @@ impl TextBox {
|
|||||||
if self.text.push(char).is_err() {
|
if self.text.push(char).is_err() {
|
||||||
// `self.text` is full, ignore this change.
|
// `self.text` is full, ignore this change.
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
panic!("Textbox is full");
|
panic!("textbox is full");
|
||||||
}
|
}
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
trezorhal::random,
|
trezorhal::random,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
|
base::ComponentExt,
|
||||||
label::{Label, LabelStyle},
|
label::{Label, LabelStyle},
|
||||||
Child, Component, Event, EventCtx, Never,
|
Child, Component, Event, EventCtx, Never,
|
||||||
},
|
},
|
||||||
@ -57,29 +58,20 @@ impl PinDialog {
|
|||||||
minor_prompt,
|
minor_prompt,
|
||||||
theme::label_default(),
|
theme::label_default(),
|
||||||
);
|
);
|
||||||
let dots = Child::new(PinDots::new(
|
let dots =
|
||||||
grid.row_col(0, 0),
|
PinDots::new(grid.row_col(0, 0), digits.len(), theme::label_default()).into_child();
|
||||||
digits.len(),
|
|
||||||
theme::label_default(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Control buttons.
|
// Control buttons.
|
||||||
let grid = Grid::new(area, 5, 3);
|
let grid = Grid::new(area, 5, 3);
|
||||||
let reset_btn = Child::new(Button::with_text(
|
let reset_btn = Button::with_text(grid.row_col(4, 0), b"Reset")
|
||||||
grid.row_col(4, 0),
|
.styled(theme::button_clear())
|
||||||
b"Reset",
|
.into_child();
|
||||||
theme::button_clear(),
|
let cancel_btn = Button::with_icon(grid.row_col(4, 0), theme::ICON_CANCEL)
|
||||||
));
|
.styled(theme::button_cancel())
|
||||||
let cancel_btn = Child::new(Button::with_icon(
|
.into_child();
|
||||||
grid.row_col(4, 0),
|
let confirm_btn = Button::with_icon(grid.row_col(4, 2), theme::ICON_CONFIRM)
|
||||||
theme::ICON_CANCEL,
|
.styled(theme::button_clear())
|
||||||
theme::button_cancel(),
|
.into_child();
|
||||||
));
|
|
||||||
let confirm_btn = Child::new(Button::with_icon(
|
|
||||||
grid.row_col(4, 2),
|
|
||||||
theme::ICON_CONFIRM,
|
|
||||||
theme::button_clear(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// PIN digit buttons.
|
// PIN digit buttons.
|
||||||
let digit_btns = Self::generate_digit_buttons(&grid);
|
let digit_btns = Self::generate_digit_buttons(&grid);
|
||||||
@ -111,7 +103,7 @@ impl PinDialog {
|
|||||||
i + 1 + 3
|
i + 1 + 3
|
||||||
});
|
});
|
||||||
let text: &[u8; 1] = digits[i];
|
let text: &[u8; 1] = digits[i];
|
||||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
Child::new(Button::with_text(area, text))
|
||||||
};
|
};
|
||||||
[
|
[
|
||||||
btn(0),
|
btn(0),
|
||||||
@ -128,25 +120,17 @@ impl PinDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||||
for btn in &mut self.digit_btns {
|
|
||||||
let is_full = self.digits.is_full();
|
let is_full = self.digits.is_full();
|
||||||
btn.mutate(ctx, |ctx, btn| {
|
for btn in &mut self.digit_btns {
|
||||||
if is_full {
|
btn.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_full));
|
||||||
btn.disable(ctx);
|
|
||||||
} else {
|
|
||||||
btn.enable(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.digits.is_empty() {
|
|
||||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
|
||||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
|
||||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
|
||||||
} else {
|
|
||||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
|
||||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
|
||||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
|
||||||
}
|
}
|
||||||
|
let is_empty = self.digits.is_empty();
|
||||||
|
self.reset_btn
|
||||||
|
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
|
||||||
|
self.cancel_btn
|
||||||
|
.mutate(ctx, |ctx, btn| btn.enabled(ctx, is_empty));
|
||||||
|
self.confirm_btn
|
||||||
|
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
|
||||||
let digit_count = self.digits.len();
|
let digit_count = self.digits.len();
|
||||||
self.dots
|
self.dots
|
||||||
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
||||||
|
@ -4,8 +4,13 @@ use crate::{error, ui::geometry::Point};
|
|||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum TouchEvent {
|
pub enum TouchEvent {
|
||||||
|
/// A person has started touching the screen at given absolute coordinates.
|
||||||
|
/// `TouchMove` will usually follow, and `TouchEnd` should finish the
|
||||||
|
/// interaction.
|
||||||
TouchStart(Point),
|
TouchStart(Point),
|
||||||
|
/// Touch has moved into a different point on the screen.
|
||||||
TouchMove(Point),
|
TouchMove(Point),
|
||||||
|
/// Touch has ended at a point on the screen.
|
||||||
TouchEnd(Point),
|
TouchEnd(Point),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,22 +12,23 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{Button, Dialog, DialogMsg},
|
component::{Button, ButtonMsg, Dialog, DialogMsg},
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl<T> TryFrom<DialogMsg<T>> for Obj
|
impl<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj
|
||||||
where
|
where
|
||||||
Obj: TryFrom<T>,
|
Obj: TryFrom<T>,
|
||||||
Error: From<<T as TryInto<Obj>>::Error>,
|
Error: From<<Obj as TryFrom<T>>::Error>,
|
||||||
{
|
{
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(val: DialogMsg<T>) -> Result<Self, Self::Error> {
|
fn try_from(val: DialogMsg<T, ButtonMsg, ButtonMsg>) -> Result<Self, Self::Error> {
|
||||||
match val {
|
match val {
|
||||||
DialogMsg::Content(c) => Ok(c.try_into()?),
|
DialogMsg::Content(c) => Ok(c.try_into()?),
|
||||||
DialogMsg::LeftClicked => 1.try_into(),
|
DialogMsg::Left(ButtonMsg::Clicked) => 1.try_into(),
|
||||||
DialogMsg::RightClicked => 2.try_into(),
|
DialogMsg::Right(ButtonMsg::Clicked) => 2.try_into(),
|
||||||
|
_ => Ok(Obj::const_none()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,8 +44,8 @@ extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
|
|||||||
.with(b"some", "a few")
|
.with(b"some", "a few")
|
||||||
.with(b"param", "xx")
|
.with(b"param", "xx")
|
||||||
},
|
},
|
||||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
|area| Button::with_text(area, b"Left"),
|
||||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
|area| Button::with_text(area, b"Right"),
|
||||||
)))?;
|
)))?;
|
||||||
Ok(layout.into())
|
Ok(layout.into())
|
||||||
};
|
};
|
||||||
@ -105,8 +106,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.with(b"param", b"parameters!")
|
.with(b"param", b"parameters!")
|
||||||
},
|
},
|
||||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
|area| Button::with_text(area, b"Left"),
|
||||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
|area| Button::with_text(area, b"Right"),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trace(&layout),
|
trace(&layout),
|
||||||
|
Loading…
Reference in New Issue
Block a user