From a48061d36de33635265635dd123e652f4e2f448a Mon Sep 17 00:00:00 2001 From: obrusvit Date: Mon, 20 Jan 2025 11:21:47 +0100 Subject: [PATCH] WIP: feat(eckhart): full-screen vertical menu component --- .../src/ui/layout_eckhart/component/mod.rs | 2 + .../component/vertical_menu_page.rs | 133 ++++++++++++++++++ .../rust/src/ui/layout_eckhart/theme/mod.rs | 25 ++++ 3 files changed, 160 insertions(+) create mode 100644 core/embed/rust/src/ui/layout_eckhart/component/vertical_menu_page.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 6888b636f9..9732074a52 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 result; mod text_screen; +mod vertical_menu_page; mod welcome_screen; pub use action_bar::ActionBar; @@ -15,6 +16,7 @@ pub use header::{Header, HeaderMsg}; pub use hint::Hint; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg}; +pub use vertical_menu_page::VerticalMenuPage; pub use welcome_screen::WelcomeScreen; use super::{constant, theme}; diff --git a/core/embed/rust/src/ui/layout_eckhart/component/vertical_menu_page.rs b/core/embed/rust/src/ui/layout_eckhart/component/vertical_menu_page.rs new file mode 100644 index 0000000000..cb30a10962 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/vertical_menu_page.rs @@ -0,0 +1,133 @@ +use crate::{ + strutil::TString, + ui::{ + component::{Component, Event, EventCtx}, + display::Icon, + geometry::Rect, + layout_eckhart::{component::button::IconText, theme}, + shape::Renderer, + }, +}; + +use heapless::Vec; + +use super::{button::ButtonMsg, constant, Button, Header, HeaderMsg}; + +/// Number of buttons. +/// Presently, VerticalMenu holds only fixed number of buttons. +const MENU_MAX_ITEMS: usize = 4; + +type VerticalMenuButtons = Vec; + +/// TODO: this is just a mockup for now +pub struct VerticalMenuPage { + header: Header, + buttons: VerticalMenuButtons, +} + +pub enum VerticalMenuMsg { + Selected(usize), + /// Left header button clicked + Back, + /// Right header button clicked + Close, +} + +impl VerticalMenuPage { + fn new(buttons: VerticalMenuButtons) -> Self { + Self { + header: Header::new(TString::empty()), + buttons, + } + } + + pub fn empty() -> Self { + Self::new(VerticalMenuButtons::new()) + } + + pub fn with_header(mut self, header: Header) -> Self { + self.header = header; + self + } + + pub fn item(mut self, icon: Icon, text: TString<'static>) -> Self { + unwrap!(self.buttons.push( + Button::with_icon_and_text(IconText::new(text, icon)).styled(theme::button_default()) + )); + self + } + + pub fn danger(mut self, icon: Icon, text: TString<'static>) -> Self { + unwrap!( + (self.buttons.push( + Button::with_icon_and_text(IconText::new(text, icon)) + .styled(theme::button_warning_high()) + )), + "unwrap failed" + ); + self + } +} + +impl Component for VerticalMenuPage { + type Msg = VerticalMenuMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + // assert full screen + debug_assert_eq!(bounds.height(), constant::HEIGHT); + debug_assert_eq!(bounds.width(), constant::WIDTH); + + const MENU_BUTTON_HEIGHT: i16 = 64; // TODO: variable height buttons + /// Fixed height of a separator. + const MENU_SEP_HEIGHT: i16 = 2; + let n_seps = self.buttons.len() - 1; + let (header_area, mut rest) = bounds.split_top(Header::HEADER_HEIGHT); + for (i, button) in self.buttons.iter_mut().enumerate() { + let (area_button, new_remaining) = rest.split_top(MENU_BUTTON_HEIGHT); + button.place(area_button); + rest = new_remaining; + if i < n_seps { + let (_area_sep, new_remaining) = rest.split_top(MENU_SEP_HEIGHT); + rest = new_remaining; + } + } + + self.header.place(header_area); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(msg) = self.header.event(ctx, event) { + match msg { + HeaderMsg::Cancelled => return Some(VerticalMenuMsg::Close), + _ => {} + } + } + + for (i, button) in self.buttons.iter_mut().enumerate() { + if let Some(ButtonMsg::Clicked) = button.event(ctx, event) { + return Some(VerticalMenuMsg::Selected(i)); + } + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + for (i, button) in (&self.buttons).into_iter().enumerate() { + button.render(target); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for VerticalMenuPage { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("VerticalMenuPage"); + t.in_list("buttons", &|button_list| { + for button in &self.buttons { + button_list.child(button); + } + }); + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs index 097d3564df..ce5b53813b 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs @@ -200,6 +200,31 @@ pub const fn button_confirm() -> ButtonStyleSheet { } } +pub const fn button_warning_high() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: ORANGE, + button_color: BG, + icon_color: ORANGE, + background_color: BG, + }, + active: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: ORANGE, + button_color: GREY_SUPER_DARK, + icon_color: GREEN, + background_color: BG, + }, + disabled: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: ORANGE, + button_color: GREY_DARK, + icon_color: GREY_LIGHT, + background_color: GREY_DARK, + }, + } +} pub const fn button_header() -> ButtonStyleSheet { ButtonStyleSheet {