mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-03 16:56:07 +00:00
feat(eckhart): full-screen input number component
This commit is contained in:
parent
013ce4da45
commit
33de234c7a
@ -6,6 +6,7 @@ mod header;
|
|||||||
mod hint;
|
mod hint;
|
||||||
mod hold_to_confirm;
|
mod hold_to_confirm;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
|
mod number_input_screen;
|
||||||
mod result;
|
mod result;
|
||||||
mod select_word_screen;
|
mod select_word_screen;
|
||||||
mod share_words;
|
mod share_words;
|
||||||
@ -29,6 +30,7 @@ pub use keyboard::{
|
|||||||
slip39::Slip39Input,
|
slip39::Slip39Input,
|
||||||
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
|
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
|
||||||
};
|
};
|
||||||
|
pub use number_input_screen::{NumberInputScreenMsg, NumberInputScreen};
|
||||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||||
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
|
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
|
||||||
#[cfg(feature = "translations")]
|
#[cfg(feature = "translations")]
|
||||||
|
@ -0,0 +1,273 @@
|
|||||||
|
use crate::{
|
||||||
|
strutil::{self, TString},
|
||||||
|
translations::TR,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx, Label, Maybe, Never},
|
||||||
|
geometry::{Alignment, Insets, Offset, Rect},
|
||||||
|
shape::{self, Renderer},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
super::{super::constant::SCREEN, fonts, theme},
|
||||||
|
ActionBar, ActionBarMsg, Button, ButtonMsg, Header, HeaderMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum NumberInputScreenMsg {
|
||||||
|
Cancelled,
|
||||||
|
Confirmed(u32),
|
||||||
|
Menu,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NumberInputScreen {
|
||||||
|
/// Screen header
|
||||||
|
header: Header,
|
||||||
|
/// Screeen description
|
||||||
|
description: Label<'static>,
|
||||||
|
/// Number input dialog
|
||||||
|
number_input: NumberInput,
|
||||||
|
/// Screen action bar
|
||||||
|
action_bar: ActionBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumberInputScreen {
|
||||||
|
const DESCRIPTION_HEIGHT: i16 = 123;
|
||||||
|
const INPUT_HEIGHT: i16 = 170;
|
||||||
|
pub fn new(min: u32, max: u32, init_value: u32, text: TString<'static>) -> Self {
|
||||||
|
Self {
|
||||||
|
header: Header::new(TString::empty()),
|
||||||
|
action_bar: ActionBar::new_double(
|
||||||
|
Button::with_icon(theme::ICON_CHEVRON_UP),
|
||||||
|
Button::with_text(TR::buttons__continue.into()),
|
||||||
|
),
|
||||||
|
number_input: NumberInput::new(min, max, init_value),
|
||||||
|
description: Label::new(text, Alignment::Start, theme::TEXT_MEDIUM),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_header(mut self, header: Header) -> Self {
|
||||||
|
self.header = header;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> u32 {
|
||||||
|
self.number_input.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for NumberInputScreen {
|
||||||
|
type Msg = NumberInputScreenMsg;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
// assert full screen
|
||||||
|
debug_assert_eq!(bounds.height(), SCREEN.height());
|
||||||
|
debug_assert_eq!(bounds.width(), SCREEN.width());
|
||||||
|
|
||||||
|
let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT);
|
||||||
|
let (rest, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT);
|
||||||
|
let (description_area, rest) = rest.split_top(Self::DESCRIPTION_HEIGHT);
|
||||||
|
let (input_area, rest) = rest.split_top(Self::INPUT_HEIGHT);
|
||||||
|
|
||||||
|
// Set touch expansion for the action bar not to overlap with the input area
|
||||||
|
self.action_bar
|
||||||
|
.set_touch_expansion(Insets::top(rest.height()));
|
||||||
|
|
||||||
|
let description_area = description_area.inset(Insets::sides(24));
|
||||||
|
|
||||||
|
self.header.place(header_area);
|
||||||
|
self.description.place(description_area);
|
||||||
|
self.number_input.place(input_area);
|
||||||
|
self.action_bar.place(action_bar_area);
|
||||||
|
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
self.number_input.event(ctx, event);
|
||||||
|
|
||||||
|
if let Some(msg) = self.header.event(ctx, event) {
|
||||||
|
match msg {
|
||||||
|
HeaderMsg::Menu => return Some(NumberInputScreenMsg::Menu),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(msg) = self.action_bar.event(ctx, event) {
|
||||||
|
match msg {
|
||||||
|
ActionBarMsg::Confirmed => {
|
||||||
|
return Some(NumberInputScreenMsg::Confirmed(self.value()))
|
||||||
|
}
|
||||||
|
ActionBarMsg::Cancelled => return Some(NumberInputScreenMsg::Cancelled),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.header.render(target);
|
||||||
|
self.description.render(target);
|
||||||
|
self.number_input.render(target);
|
||||||
|
self.action_bar.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for NumberInputScreen {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("NumberInputScreen");
|
||||||
|
t.child("number_input", &self.number_input);
|
||||||
|
t.child("description", &self.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NumberInput {
|
||||||
|
area: Rect,
|
||||||
|
dec: Maybe<Button>,
|
||||||
|
inc: Maybe<Button>,
|
||||||
|
min: u32,
|
||||||
|
max: u32,
|
||||||
|
value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumberInput {
|
||||||
|
const BUTTON_PADDING: i16 = 10;
|
||||||
|
const BUTTON_SIZE: Offset = Offset::new(138, 130);
|
||||||
|
const BORDER_PADDING: i16 = 24;
|
||||||
|
pub fn new(min: u32, max: u32, value: u32) -> Self {
|
||||||
|
let dec = Button::with_icon(theme::ICON_MINUS)
|
||||||
|
.styled(theme::button_keyboard())
|
||||||
|
.with_radius(12);
|
||||||
|
let inc = Button::with_icon(theme::ICON_PLUS)
|
||||||
|
.styled(theme::button_keyboard())
|
||||||
|
.with_radius(12);
|
||||||
|
let value = value.clamp(min, max);
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
dec: Maybe::new(theme::BG, dec, value > min),
|
||||||
|
inc: Maybe::new(theme::BG, inc, value < max),
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increase(&mut self, ctx: &mut EventCtx) {
|
||||||
|
self.value = self.value.saturating_add(1).min(self.max);
|
||||||
|
self.on_change(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrease(&mut self, ctx: &mut EventCtx) {
|
||||||
|
self.value = self.value.saturating_sub(1).max(self.min);
|
||||||
|
self.on_change(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_change(&mut self, ctx: &mut EventCtx) {
|
||||||
|
self.dec.show_if(ctx, self.value > self.min);
|
||||||
|
self.inc.show_if(ctx, self.value < self.max);
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_borders<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let borders = self.area.inset(Insets::sides(Self::BORDER_PADDING));
|
||||||
|
// Render lines as bars with the with 1px
|
||||||
|
let top_line =
|
||||||
|
Rect::from_bottom_right_and_size(borders.top_right(), Offset::new(borders.width(), 1));
|
||||||
|
let bottom_line =
|
||||||
|
Rect::from_top_right_and_size(borders.bottom_right(), Offset::new(borders.width(), 1));
|
||||||
|
|
||||||
|
shape::Bar::new(top_line)
|
||||||
|
.with_fg(theme::GREY_EXTRA_DARK)
|
||||||
|
.with_bg(theme::GREY_EXTRA_DARK)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Bar::new(bottom_line)
|
||||||
|
.with_fg(theme::GREY_EXTRA_DARK)
|
||||||
|
.with_bg(theme::GREY_EXTRA_DARK)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_number<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
|
||||||
|
if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) {
|
||||||
|
let font = fonts::FONT_SATOSHI_EXTRALIGHT_72;
|
||||||
|
let y_offset = Offset::y(font.visible_text_height(text) / 2);
|
||||||
|
shape::Text::new(self.area.center().ofs(y_offset), text, font)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_fg(theme::GREY)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for NumberInput {
|
||||||
|
type Msg = Never;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
|
||||||
|
let button_bounds = self.area.inset(Insets::sides(Self::BUTTON_PADDING));
|
||||||
|
// Equivalent to from_left_center_and_size
|
||||||
|
let dec_bounds = Rect::from_center_and_size(
|
||||||
|
button_bounds
|
||||||
|
.left_center()
|
||||||
|
.ofs(Offset::x(Self::BUTTON_SIZE.x / 2)),
|
||||||
|
Self::BUTTON_SIZE,
|
||||||
|
);
|
||||||
|
// Equivalent to from_right_center_and_size
|
||||||
|
let inc_bounds = Rect::from_center_and_size(
|
||||||
|
button_bounds
|
||||||
|
.right_center()
|
||||||
|
.ofs(Offset::x(-Self::BUTTON_SIZE.x / 2)),
|
||||||
|
Self::BUTTON_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.dec.place(dec_bounds);
|
||||||
|
self.inc.place(inc_bounds);
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
if let Some(ButtonMsg::Clicked) = self.dec.event(ctx, event) {
|
||||||
|
self.decrease(ctx);
|
||||||
|
};
|
||||||
|
if let Some(ButtonMsg::Clicked) = self.inc.event(ctx, event) {
|
||||||
|
self.increase(ctx);
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.render_borders(target);
|
||||||
|
self.render_number(target);
|
||||||
|
self.dec.render(target);
|
||||||
|
self.inc.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for NumberInput {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("NumberInput");
|
||||||
|
t.int("value", self.value as i64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{super::super::constant::SCREEN, *};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_component_heights_fit_screen() {
|
||||||
|
assert!(
|
||||||
|
NumberInputScreen::DESCRIPTION_HEIGHT
|
||||||
|
+ NumberInputScreen::INPUT_HEIGHT
|
||||||
|
+ Header::HEADER_HEIGHT
|
||||||
|
+ ActionBar::ACTION_BAR_HEIGHT
|
||||||
|
<= SCREEN.height(),
|
||||||
|
"Components overflow the screen height",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::component::{
|
use super::component::{
|
||||||
AllowedTextContent, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard,
|
AllowedTextContent, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputScreen,
|
||||||
PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, SelectWordScreen,
|
NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen,
|
||||||
TextScreen, TextScreenMsg,
|
SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ComponentMsgObj for PinKeyboard<'_> {
|
impl ComponentMsgObj for PinKeyboard<'_> {
|
||||||
@ -99,3 +99,13 @@ impl ComponentMsgObj for SelectWordCountScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ComponentMsgObj for NumberInputScreen {
|
||||||
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
|
match msg {
|
||||||
|
NumberInputScreenMsg::Confirmed(i) => i.try_into(),
|
||||||
|
NumberInputScreenMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||||
|
NumberInputScreenMsg::Menu => Ok(INFO.as_obj()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,8 +26,9 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{
|
component::{
|
||||||
ActionBar, Bip39Input, Button, Header, HeaderMsg, Hint, MnemonicKeyboard, PinKeyboard,
|
ActionBar, Bip39Input, Button, Header, HeaderMsg, Hint, MnemonicKeyboard,
|
||||||
SelectWordCountScreen, SelectWordScreen, Slip39Input, TextScreen,
|
NumberInputScreen, PinKeyboard, SelectWordCountScreen, SelectWordScreen, Slip39Input,
|
||||||
|
TextScreen,
|
||||||
},
|
},
|
||||||
flow, fonts, theme, UIEckhart,
|
flow, fonts, theme, UIEckhart,
|
||||||
};
|
};
|
||||||
@ -382,14 +383,20 @@ impl FirmwareUI for UIEckhart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn request_number(
|
fn request_number(
|
||||||
_title: TString<'static>,
|
title: TString<'static>,
|
||||||
_count: u32,
|
count: u32,
|
||||||
_min_count: u32,
|
min_count: u32,
|
||||||
_max_count: u32,
|
max_count: u32,
|
||||||
_description: Option<TString<'static>>,
|
description: Option<TString<'static>>,
|
||||||
_more_info_callback: Option<impl Fn(u32) -> TString<'static> + 'static>,
|
_more_info_callback: Option<impl Fn(u32) -> TString<'static> + 'static>,
|
||||||
) -> Result<impl LayoutMaybeTrace, Error> {
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
|
let description = description.unwrap_or(TString::empty());
|
||||||
|
let component = NumberInputScreen::new(min_count, max_count, count, description)
|
||||||
|
.with_header(Header::new(title).with_menu_button());
|
||||||
|
|
||||||
|
let layout = RootComponent::new(component);
|
||||||
|
|
||||||
|
Ok(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_pin(
|
fn request_pin(
|
||||||
|
Loading…
Reference in New Issue
Block a user