From 33de234c7ad8e31c19c1c71972f26ef8f90585c9 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Tue, 25 Feb 2025 15:29:45 +0100 Subject: [PATCH] feat(eckhart): full-screen input number component --- .../src/ui/layout_eckhart/component/mod.rs | 2 + .../component/number_input_screen.rs | 273 ++++++++++++++++++ .../ui/layout_eckhart/component_msg_obj.rs | 16 +- .../rust/src/ui/layout_eckhart/ui_firmware.rs | 23 +- 4 files changed, 303 insertions(+), 11 deletions(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/component/number_input_screen.rs diff --git a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs index 24013c36bd..9ced467c0c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs @@ -6,6 +6,7 @@ mod header; mod hint; mod hold_to_confirm; mod keyboard; +mod number_input_screen; mod result; mod select_word_screen; mod share_words; @@ -29,6 +30,7 @@ pub use keyboard::{ slip39::Slip39Input, word_count_screen::{SelectWordCountMsg, SelectWordCountScreen}, }; +pub use number_input_screen::{NumberInputScreenMsg, NumberInputScreen}; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; #[cfg(feature = "translations")] diff --git a/core/embed/rust/src/ui/layout_eckhart/component/number_input_screen.rs b/core/embed/rust/src/ui/layout_eckhart/component/number_input_screen.rs new file mode 100644 index 0000000000..f21e7dbdad --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/number_input_screen.rs @@ -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.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