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 e6527c8f53..317b132dd0 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs @@ -5,6 +5,7 @@ mod error; mod header; mod hint; mod result; +mod select_word_screen; mod share_words; mod text_screen; mod vertical_menu; @@ -17,6 +18,7 @@ pub use error::ErrorScreen; pub use header::{Header, HeaderMsg}; pub use hint::Hint; 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..163a61256e --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/select_word_screen.rs @@ -0,0 +1,107 @@ +use crate::{ + strutil::TString, + ui::{ + component::{Component, Event, EventCtx, Label}, + geometry::{Alignment, Insets, Rect}, + layout_eckhart::{ + component::{Button, Header, HeaderMsg, VerticalMenu, VerticalMenuMsg}, + constant::SCREEN, + theme, + }, + shape::Renderer, + ui_firmware::MAX_WORD_QUIZ_ITEMS, + }, +}; + +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 569930b0ac..6047a5db3a 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 {