From eb027d2ffa72fca15a124c416b167b6d52aa2edd Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sat, 11 Jan 2025 17:22:08 +0100 Subject: [PATCH] feat(eckhart): full-screen textual component - TextScreen is a full-screen component for (paginated) texts - it's supposed to wrap FormattedText or Paragraphs --- .../src/ui/layout_eckhart/component/mod.rs | 2 + .../layout_eckhart/component/text_screen.rs | 165 ++++++++++++++++++ .../ui/layout_eckhart/component_msg_obj.rs | 20 ++- 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/component/text_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 581c266cb5..6888b636f9 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 text_screen; mod welcome_screen; pub use action_bar::ActionBar; @@ -13,6 +14,7 @@ pub use error::ErrorScreen; pub use header::{Header, HeaderMsg}; pub use hint::Hint; pub use result::{ResultFooter, ResultScreen, ResultStyle}; +pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg}; pub use welcome_screen::WelcomeScreen; use super::{constant, theme}; diff --git a/core/embed/rust/src/ui/layout_eckhart/component/text_screen.rs b/core/embed/rust/src/ui/layout_eckhart/component/text_screen.rs new file mode 100644 index 0000000000..3656cdaed1 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/text_screen.rs @@ -0,0 +1,165 @@ +use crate::{ + strutil::TString, + ui::{ + component::{ + swipe_detect::SwipeConfig, + text::paragraphs::{ParagraphSource, Paragraphs}, + Component, Event, EventCtx, FormattedText, PaginateFull, + }, + flow::Swipable, + geometry::{Insets, Rect}, + shape::Renderer, + util::Pager, + }, +}; + +use super::{action_bar::ActionBarMsg, button::Button, ActionBar, Header, HeaderMsg, Hint}; + +/// Full-screen component for rendering text. +/// +/// T should be either `Paragraphs` or `FormattedText`. +/// The component wraps the full content of the generic page spec: +/// - Header (Optional) +/// - Text +/// - Hint (Optional) +/// - Action bar (Optional) +pub struct TextScreen { + header: Option
, + content: T, + hint: Option>, + action_bar: Option, + // TODO: swipe handling + // TODO: animations +} + +pub enum TextScreenMsg { + Cancelled, + Confirmed, + Menu, +} + +impl TextScreen +where + T: AllowedTextContent, +{ + const CONTENT_INSETS: Insets = Insets::sides(24); + + pub fn new(content: T) -> Self { + Self { + header: None, + content, + hint: None, + action_bar: None, + } + } + + pub fn with_header(mut self, header: Header) -> Self { + self.header = Some(header); + self + } + + pub fn with_hint(mut self, hint: Hint<'static>) -> Self { + self.hint = Some(hint); + self + } + + pub fn with_action_bar(mut self, action_bar: ActionBar) -> Self { + self.action_bar = Some(action_bar); + self + } + + fn update_page(&mut self, page_idx: u16) { + self.content.change_page(page_idx); + let pager = self.content.pager(); + self.hint.as_mut().map(|h| h.update(pager)); + self.action_bar.as_mut().map(|ab| ab.update(pager)); + } +} + +impl Component for TextScreen +where + T: AllowedTextContent, +{ + type Msg = TextScreenMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT); + let (rest, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT); + let content_area = if let Some(hint) = &mut self.hint { + let (rest, hint_area) = rest.split_bottom(hint.height()); + hint.place(hint_area); + rest + } else { + rest + }; + self.header.place(header_area); + self.content.place(content_area.inset(Self::CONTENT_INSETS)); + self.action_bar.place(action_bar_area); + + self.update_page(0); + 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(TextScreenMsg::Cancelled), + HeaderMsg::Menu => return Some(TextScreenMsg::Menu), + } + } + if let Some(msg) = self.action_bar.event(ctx, event) { + match msg { + ActionBarMsg::Cancelled => return Some(TextScreenMsg::Cancelled), + ActionBarMsg::Confirmed => return Some(TextScreenMsg::Confirmed), + ActionBarMsg::Prev => { + self.update_page(self.content.pager().prev()); + return None; + } + ActionBarMsg::Next => { + self.update_page(self.content.pager().next()); + return None; + } + } + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.header.render(target); + self.content.render(target); + self.hint.render(target); + self.action_bar.render(target); + } +} + +impl Swipable for TextScreen +where + T: AllowedTextContent, +{ + fn get_pager(&self) -> Pager { + self.content.pager() + } + fn get_swipe_config(&self) -> SwipeConfig { + SwipeConfig::default() + } +} + +/// A marker trait used to constrain the allowed text content types in a +/// TextScreen. +pub trait AllowedTextContent: Component + PaginateFull {} +impl AllowedTextContent for FormattedText {} +impl<'a, T> AllowedTextContent for Paragraphs where T: ParagraphSource<'a> {} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for TextScreen +where + T: AllowedTextContent + crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("TextComponent"); + self.header.as_ref().map(|header| header.trace(t)); + self.content.trace(t); + self.hint.as_ref().map(|hint| hint.trace(t)); + self.action_bar.as_ref().map(|ab| ab.trace(t)); + } +} 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 7ddfbedc87..f5e8f8dbae 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 @@ -6,10 +6,15 @@ use crate::{ text::paragraphs::{ParagraphSource, Paragraphs}, Component, Timeout, }, - layout::{obj::ComponentMsgObj, result::CANCELLED}, + layout::{ + obj::ComponentMsgObj, + result::{CANCELLED, CONFIRMED, INFO}, + }, }, }; +use super::component::{AllowedTextContent, TextScreen, TextScreenMsg}; + // Clippy/compiler complains about conflicting implementations // TODO move the common impls to a common module #[cfg(not(feature = "clippy"))] @@ -32,3 +37,16 @@ where Ok(CANCELLED.as_obj()) } } + +impl ComponentMsgObj for TextScreen +where + T: AllowedTextContent, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + TextScreenMsg::Cancelled => Ok(CANCELLED.as_obj()), + TextScreenMsg::Confirmed => Ok(CONFIRMED.as_obj()), + TextScreenMsg::Menu => Ok(INFO.as_obj()), + } + } +}