1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-23 21:02:23 +00:00

feat(eckhart): full-screen textual component

- TextScreen is a full-screen component for (paginated) texts
- it's supposed to wrap FormattedText or Paragraphs
This commit is contained in:
obrusvit 2025-01-11 17:22:08 +01:00
parent be4d6fa47c
commit eb027d2ffa
3 changed files with 186 additions and 1 deletions

View File

@ -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};

View File

@ -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<T> {
header: Option<Header>,
content: T,
hint: Option<Hint<'static>>,
action_bar: Option<ActionBar>,
// TODO: swipe handling
// TODO: animations
}
pub enum TextScreenMsg {
Cancelled,
Confirmed,
Menu,
}
impl<T> TextScreen<T>
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<T> Component for TextScreen<T>
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<Self::Msg> {
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<T> Swipable for TextScreen<T>
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<T> where T: ParagraphSource<'a> {}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for TextScreen<T>
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));
}
}

View File

@ -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<T> ComponentMsgObj for TextScreen<T>
where
T: AllowedTextContent,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
TextScreenMsg::Cancelled => Ok(CANCELLED.as_obj()),
TextScreenMsg::Confirmed => Ok(CONFIRMED.as_obj()),
TextScreenMsg::Menu => Ok(INFO.as_obj()),
}
}
}