From 89d9a066f34b7cf0c249d9678afc101f17590a1e Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Tue, 4 Mar 2025 16:51:51 +0100 Subject: [PATCH] feat(eckhart): full-screen QR screen component --- .../src/ui/layout_eckhart/firmware/mod.rs | 2 + .../ui/layout_eckhart/firmware/qr_screen.rs | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 core/embed/rust/src/ui/layout_eckhart/firmware/qr_screen.rs diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs index 15c428ef80..51b2a74fd3 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -4,6 +4,7 @@ mod hint; mod hold_to_confirm; mod keyboard; mod number_input_screen; +mod qr_screen; mod select_word_screen; mod share_words; mod text_screen; @@ -24,6 +25,7 @@ pub use keyboard::{ word_count_screen::{SelectWordCountMsg, SelectWordCountScreen}, }; pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg}; +pub use qr_screen::{QrMsg, QrScreen}; pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; #[cfg(feature = "translations")] pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg}; diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/qr_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/qr_screen.rs new file mode 100644 index 0000000000..edf75a1a5b --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/qr_screen.rs @@ -0,0 +1,134 @@ +use crate::{ + strutil::TString, + ui::{ + component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Qr}, + flow::Swipable, + geometry::{Insets, Rect}, + shape::{self, Renderer}, + util::Pager, + }, +}; + +use super::super::{ + firmware::{theme, ActionBar, Header, HeaderMsg}, + constant::SCREEN, +}; + +pub enum QrMsg { + Cancelled, +} + +pub struct QrScreen { + header: Header, + qr: Qr, + action_bar: Option, + pad: Rect, +} + +impl QrScreen { + const QR_PADDING: i16 = 8; + const QR_HEIGHT: i16 = 300; + const QR_PAD_RADIUS: i16 = 12; + + pub fn new(qr: Qr) -> Self { + Self { + header: Header::new(TString::empty()), + qr, + action_bar: None, + pad: Rect::zero(), + } + } + + pub fn with_header(mut self, header: Header) -> Self { + self.header = header; + self + } + + pub fn with_action_bar(mut self, action_bar: ActionBar) -> Self { + self.action_bar = Some(action_bar); + self + } +} + +impl Component for QrScreen { + type Msg = QrMsg; + + 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, mut rest) = bounds.split_top(Header::HEADER_HEIGHT); + if let Some(action_bar) = &mut self.action_bar { + let action_bar_area; + (rest, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT); + action_bar.place(action_bar_area); + } + let (qr_pad, _) = rest.split_top(Self::QR_HEIGHT + 2 * Self::QR_PADDING); + + let side_padding = (SCREEN.width() - Self::QR_HEIGHT - 2 * Self::QR_PADDING) / 2; + let qr_pad = qr_pad.inset(Insets::sides(side_padding)); + + self.pad = qr_pad; + + self.header.place(header_area); + self.qr.place(qr_pad.shrink(Self::QR_PADDING)); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(HeaderMsg::Cancelled) = self.header.event(ctx, event) { + return Some(QrMsg::Cancelled); + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Render white QR pad + shape::Bar::new(self.pad) + .with_bg(theme::FG) + .with_fg(theme::FG) + .with_radius(Self::QR_PAD_RADIUS) + .render(target); + + self.header.render(target); + self.qr.render(target); + self.action_bar.render(target); + } +} + +#[cfg(feature = "micropython")] +impl Swipable for QrScreen { + fn get_swipe_config(&self) -> SwipeConfig { + SwipeConfig::new() + } + + fn get_pager(&self) -> Pager { + Pager::single_page() + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for QrScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("QrScreen"); + } +} + +#[cfg(test)] +mod tests { + use super::{super::super::constant::SCREEN, *}; + + #[test] + fn test_component_heights_fit_screen() { + assert!( + QrScreen::QR_HEIGHT + + 2 * QrScreen::QR_PADDING + + Header::HEADER_HEIGHT + + ActionBar::ACTION_BAR_HEIGHT + <= SCREEN.height(), + "Components overflow the screen height", + ); + } +}