From 7434524d2ad6492350a068d1fbc65550dc4b3577 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Tue, 4 Feb 2025 12:16:57 +0100 Subject: [PATCH] feat(eckhart): full-screen select word component --- .../src/ui/layout_eckhart/component/mod.rs | 2 + .../component/select_word_screen.rs | 108 ++++++++++++++++++ .../ui/layout_eckhart/component_msg_obj.rs | 13 ++- .../rust/src/ui/layout_eckhart/ui_firmware.rs | 17 ++- 4 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/component/select_word_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 6761c94d61..4ef5d37e2f 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 result; +mod select_word_screen; mod share_words; mod text_screen; mod vertical_menu; @@ -19,6 +20,7 @@ pub use header::{Header, HeaderMsg}; pub use hint::Hint; pub use hold_to_confirm::HoldToConfirmAnim; pub use result::{ResultFooter, ResultScreen, ResultStyle}; +pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; #[cfg(feature = "translations")] pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg}; pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg}; diff --git a/core/embed/rust/src/ui/layout_eckhart/component/select_word_screen.rs b/core/embed/rust/src/ui/layout_eckhart/component/select_word_screen.rs new file mode 100644 index 0000000000..ba25df94b0 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/select_word_screen.rs @@ -0,0 +1,108 @@ +use crate::{ + strutil::TString, + ui::{ + component::{Component, Event, EventCtx, Label}, + geometry::{Alignment, Insets, Rect}, + shape::Renderer, + ui_firmware::MAX_WORD_QUIZ_ITEMS, + }, +}; + +use super::super::{ + component::{Button, Header, HeaderMsg, VerticalMenu, VerticalMenuMsg}, + constant::SCREEN, + theme, +}; + +pub struct SelectWordScreen { + header: Header, + description: Label<'static>, + menu: VerticalMenu, +} + +pub enum SelectWordMsg { + Selected(usize), + /// Right header button clicked + Cancelled, +} + +impl SelectWordScreen { + const INSET: i16 = 24; + const DESCRIPTION_HEIGHT: i16 = 52; + const BUTTON_RADIUS: u8 = 12; + + pub fn new( + share_words_vec: [TString<'static>; MAX_WORD_QUIZ_ITEMS], + description: TString<'static>, + ) -> Self { + let mut menu = VerticalMenu::empty().with_separators().with_fit_area(); + + for word in share_words_vec { + menu = menu.item( + Button::with_text(word) + .styled(theme::button_select_word()) + .with_radius(Self::BUTTON_RADIUS), + ); + } + + Self { + header: Header::new(TString::empty()), + description: Label::new(description, Alignment::Start, theme::TEXT_MEDIUM) + .vertically_centered(), + menu, + } + } + + pub fn with_header(mut self, header: Header) -> Self { + self.header = header; + self + } +} + +impl Component for SelectWordScreen { + type Msg = SelectWordMsg; + + 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 (description_area, rest) = rest.split_top(Self::DESCRIPTION_HEIGHT); + let (_, rest) = rest.split_top(Self::INSET); + let (menu_area, _) = rest.split_bottom(Self::INSET); + + let description_area = description_area.inset(Insets::sides(Self::INSET)); + + self.menu.place(menu_area); + self.description.place(description_area); + self.header.place(header_area); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(HeaderMsg::Cancelled) = self.header.event(ctx, event) { + return Some(SelectWordMsg::Cancelled); + } + + if let Some(VerticalMenuMsg::Selected(i)) = self.menu.event(ctx, event) { + return Some(SelectWordMsg::Selected(i)); + } + + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.header.render(target); + self.description.render(target); + self.menu.render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for SelectWordScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("SelectWordScreen"); + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index f5e8f8dbae..244d020b50 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -13,7 +13,9 @@ use crate::{ }, }; -use super::component::{AllowedTextContent, TextScreen, TextScreenMsg}; +use super::component::{ + AllowedTextContent, SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg, +}; // Clippy/compiler complains about conflicting implementations // TODO move the common impls to a common module @@ -50,3 +52,12 @@ where } } } + +impl ComponentMsgObj for SelectWordScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + SelectWordMsg::Selected(i) => i.try_into(), + SelectWordMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 6e436ca8ce..b500595372 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -24,7 +24,7 @@ use crate::{ }; use super::{ - component::{ActionBar, Button, Header, HeaderMsg, Hint, TextScreen}, + component::{ActionBar, Button, Header, HeaderMsg, Hint, SelectWordScreen, TextScreen}, flow, fonts, theme, UIEckhart, }; @@ -389,11 +389,18 @@ impl FirmwareUI for UIEckhart { } fn select_word( - _title: TString<'static>, - _description: TString<'static>, - _words: [TString<'static>; MAX_WORD_QUIZ_ITEMS], + title: TString<'static>, + description: TString<'static>, + words: [TString<'static>; MAX_WORD_QUIZ_ITEMS], ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + let component = SelectWordScreen::new(words, description).with_header( + Header::new(title) + .with_right_button(Button::with_icon(theme::ICON_MENU), HeaderMsg::Cancelled), + ); + + let layout = RootComponent::new(component); + + Ok(layout) } fn select_word_count(_recovery_type: RecoveryType) -> Result {