1
0
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:
Jan Pochyla 2021-10-24 11:37:59 +02:00 committed by matejcik
parent 998210d569
commit 39263144b7
13 changed files with 282 additions and 157 deletions

View File

@ -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
} }

View File

@ -1,4 +1,4 @@
use crate::ui::component::{Component, Event, EventCtx, Never}; use super::{Component, Event, EventCtx, Never};
pub struct Empty; pub struct Empty;

View 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()
}
}

View File

@ -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;

View File

@ -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
} }

View 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();
}
}

View File

@ -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()
} }
} }

View File

@ -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);
} }
_ => { _ => {

View File

@ -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();
} }
} }

View File

@ -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();
} }

View File

@ -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));

View File

@ -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),
} }

View File

@ -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),