mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-18 05:28:40 +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 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<Self::Msg>;
|
||||
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> {
|
||||
component: T,
|
||||
marked_for_paint: bool,
|
||||
@ -39,18 +48,26 @@ impl<T> Child<T> {
|
||||
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
|
||||
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::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) {
|
||||
@ -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)]
|
||||
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
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::ui::component::{Component, Event, EventCtx, Never};
|
||||
use super::{Component, Event, EventCtx, Never};
|
||||
|
||||
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 label;
|
||||
pub mod map;
|
||||
pub mod text;
|
||||
pub mod tuple;
|
||||
|
||||
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
|
||||
pub use empty::Empty;
|
||||
|
@ -38,7 +38,7 @@ impl<F, T> Text<F, T> {
|
||||
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
|
||||
}
|
||||
|
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::{
|
||||
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<Duration> for Obj {
|
||||
type Error = 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()
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
_ => {
|
||||
|
@ -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<T> {
|
||||
pub enum DialogMsg<T, L, R> {
|
||||
Content(T),
|
||||
LeftClicked,
|
||||
RightClicked,
|
||||
Left(L),
|
||||
Right(R),
|
||||
}
|
||||
|
||||
pub struct Dialog<T> {
|
||||
pub struct Dialog<T, L, R> {
|
||||
content: Child<T>,
|
||||
left_btn: Option<Child<Button>>,
|
||||
right_btn: Option<Child<Button>>,
|
||||
left: Child<L>,
|
||||
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(
|
||||
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<T: Component> Component for Dialog<T> {
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
impl<T, L, R> Component for Dialog<T, L, R>
|
||||
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> {
|
||||
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<T> crate::trace::Trace for Dialog<T>
|
||||
impl<T, L, R> crate::trace::Trace for Dialog<T, L, R>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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<Button>; KEYS]; PAGES] {
|
||||
[
|
||||
Self::generate_key_page(grid, 0),
|
||||
Self::generate_key_page(grid, 1),
|
||||
Self::generate_key_page(grid, 2),
|
||||
Self::generate_key_page(grid, 3),
|
||||
]
|
||||
// can't use a range because the result is a fixed-size array
|
||||
[0, 1, 2, 3].map(|i| Self::generate_key_page(grid, i))
|
||||
}
|
||||
|
||||
fn generate_key_page(grid: &Grid, page: usize) -> [Child<Button>; KEYS] {
|
||||
[
|
||||
Self::generate_key(grid, page, 0),
|
||||
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),
|
||||
]
|
||||
// can't use a range because the result is a fixed-size array
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(|i| Self::generate_key(grid, page, i))
|
||||
}
|
||||
|
||||
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
|
||||
// second row.
|
||||
let area = grid.cell(if key < 9 {
|
||||
@ -119,9 +102,9 @@ impl PassphraseKeyboard {
|
||||
let text = KEYBOARD[page][key].as_bytes();
|
||||
if text == b" " {
|
||||
let icon = theme::ICON_SPACE;
|
||||
Child::new(Button::with_icon(area, icon, theme::button_default()))
|
||||
Child::new(Button::with_icon(area, icon))
|
||||
} 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() {
|
||||
// Should not happen unless `self.text` has zero capacity.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox has zero capacity");
|
||||
panic!("textbox has zero capacity");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
@ -292,7 +275,7 @@ impl TextBox {
|
||||
if self.text.push(char).is_err() {
|
||||
// `self.text` is full, ignore this change.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox is full");
|
||||
panic!("textbox is full");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{
|
||||
base::ComponentExt,
|
||||
label::{Label, LabelStyle},
|
||||
Child, Component, Event, EventCtx, Never,
|
||||
},
|
||||
@ -57,29 +58,20 @@ impl PinDialog {
|
||||
minor_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let dots = Child::new(PinDots::new(
|
||||
grid.row_col(0, 0),
|
||||
digits.len(),
|
||||
theme::label_default(),
|
||||
));
|
||||
let dots =
|
||||
PinDots::new(grid.row_col(0, 0), digits.len(), theme::label_default()).into_child();
|
||||
|
||||
// Control buttons.
|
||||
let grid = Grid::new(area, 5, 3);
|
||||
let reset_btn = Child::new(Button::with_text(
|
||||
grid.row_col(4, 0),
|
||||
b"Reset",
|
||||
theme::button_clear(),
|
||||
));
|
||||
let cancel_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 0),
|
||||
theme::ICON_CANCEL,
|
||||
theme::button_cancel(),
|
||||
));
|
||||
let confirm_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 2),
|
||||
theme::ICON_CONFIRM,
|
||||
theme::button_clear(),
|
||||
));
|
||||
let reset_btn = Button::with_text(grid.row_col(4, 0), b"Reset")
|
||||
.styled(theme::button_clear())
|
||||
.into_child();
|
||||
let cancel_btn = Button::with_icon(grid.row_col(4, 0), theme::ICON_CANCEL)
|
||||
.styled(theme::button_cancel())
|
||||
.into_child();
|
||||
let confirm_btn = Button::with_icon(grid.row_col(4, 2), theme::ICON_CONFIRM)
|
||||
.styled(theme::button_clear())
|
||||
.into_child();
|
||||
|
||||
// PIN digit buttons.
|
||||
let digit_btns = Self::generate_digit_buttons(&grid);
|
||||
@ -111,7 +103,7 @@ impl PinDialog {
|
||||
i + 1 + 3
|
||||
});
|
||||
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),
|
||||
@ -128,25 +120,17 @@ impl PinDialog {
|
||||
}
|
||||
|
||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||
let is_full = self.digits.is_full();
|
||||
for btn in &mut self.digit_btns {
|
||||
let is_full = self.digits.is_full();
|
||||
btn.mutate(ctx, |ctx, btn| {
|
||||
if 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));
|
||||
btn.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_full));
|
||||
}
|
||||
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();
|
||||
self.dots
|
||||
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
||||
|
@ -4,8 +4,13 @@ use crate::{error, ui::geometry::Point};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
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),
|
||||
/// Touch has moved into a different point on the screen.
|
||||
TouchMove(Point),
|
||||
/// Touch has ended at a point on the screen.
|
||||
TouchEnd(Point),
|
||||
}
|
||||
|
||||
|
@ -12,22 +12,23 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
component::{Button, Dialog, DialogMsg},
|
||||
component::{Button, ButtonMsg, Dialog, DialogMsg},
|
||||
theme,
|
||||
};
|
||||
|
||||
impl<T> TryFrom<DialogMsg<T>> for Obj
|
||||
impl<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj
|
||||
where
|
||||
Obj: TryFrom<T>,
|
||||
Error: From<<T as TryInto<Obj>>::Error>,
|
||||
Error: From<<Obj as TryFrom<T>>::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 {
|
||||
DialogMsg::Content(c) => Ok(c.try_into()?),
|
||||
DialogMsg::LeftClicked => 1.try_into(),
|
||||
DialogMsg::RightClicked => 2.try_into(),
|
||||
DialogMsg::Left(ButtonMsg::Clicked) => 1.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"param", "xx")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Left"),
|
||||
|area| Button::with_text(area, b"Right"),
|
||||
)))?;
|
||||
Ok(layout.into())
|
||||
};
|
||||
@ -105,8 +106,8 @@ mod tests {
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Left"),
|
||||
|area| Button::with_text(area, b"Right"),
|
||||
));
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
|
Loading…
Reference in New Issue
Block a user