diff --git a/core/embed/extmod/rustmods/modtrezorui2.c b/core/embed/extmod/rustmods/modtrezorui2.c index c2dea4076d..b211473a46 100644 --- a/core/embed/extmod/rustmods/modtrezorui2.c +++ b/core/embed/extmod/rustmods/modtrezorui2.c @@ -30,6 +30,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, ui_layout_new_example); #elif TREZOR_MODEL == 1 /// def layout_new_confirm_action( +/// *, /// title: str, /// action: str | None, /// description: str | None, @@ -38,9 +39,18 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, /// hold: bool | None, /// reverse: bool, /// ) -> int: -/// """Example layout. All arguments must be passed as kwargs.""" +/// """Example layout.""" STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_action_obj, 0, ui_layout_new_confirm_action); +/// def layout_new_confirm_text( +/// *, +/// title: str, +/// data: str, +/// description: str | None, +/// ) -> int: +/// """Example layout.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_text_obj, 0, + ui_layout_new_confirm_text); #endif STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { @@ -52,6 +62,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { #elif TREZOR_MODEL == 1 {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_action), MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_action_obj)}, + {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text), + MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)}, #endif }; diff --git a/core/embed/rust/librust.h b/core/embed/rust/librust.h index c1e092c8ff..a9d9bc2acb 100644 --- a/core/embed/rust/librust.h +++ b/core/embed/rust/librust.h @@ -15,6 +15,8 @@ mp_obj_t protobuf_debug_msg_def_type(); mp_obj_t ui_layout_new_example(mp_obj_t); mp_obj_t ui_layout_new_confirm_action(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs); +mp_obj_t ui_layout_new_confirm_text(size_t n_args, const mp_obj_t *args, + mp_map_t *kwargs); #ifdef TREZOR_EMULATOR mp_obj_t ui_debug_layout_type(); diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 9f7b80df33..fb8386d754 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -1,12 +1,12 @@ use heapless::Vec; use crate::ui::{ - component::{Component, Event, EventCtx, Never}, + component::{Component, Event, EventCtx, Never, Paginate}, display::Font, geometry::{Dimensions, LinearLayout, Offset, Rect}, }; -use super::layout::{DefaultTextTheme, TextLayout, TextRenderer}; +use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer}; pub const MAX_PARAGRAPHS: usize = 6; @@ -14,6 +14,8 @@ pub struct Paragraphs { area: Rect, list: Vec, MAX_PARAGRAPHS>, layout: LinearLayout, + para_offset: usize, + char_offset: usize, } impl Paragraphs @@ -25,6 +27,8 @@ where area, list: Vec::new(), layout: LinearLayout::vertical().align_at_center().with_spacing(10), + para_offset: 0, + char_offset: 0, } } @@ -34,6 +38,9 @@ where } pub fn add(mut self, text_font: Font, content: T) -> Self { + if content.as_ref().len() == 0 { + return self; + } let paragraph = Paragraph::new( content, TextLayout { @@ -48,9 +55,12 @@ where self } - pub fn arrange(mut self) -> Self { - self.layout.arrange(self.area, &mut self.list); - self + fn break_pages<'a>(&'a mut self) -> PageBreakIterator<'a, T> { + PageBreakIterator { + paragraphs: self, + para_offset: 0, + char_offset: 0, + } } } @@ -65,8 +75,17 @@ where } fn paint(&mut self) { - for paragraph in &mut self.list { - paragraph.paint(); + let mut char_offset = self.char_offset; + for paragraph in self.list.iter().skip(self.para_offset) { + let fit = paragraph.layout.layout_text( + ¶graph.content.as_ref()[char_offset..], + &mut paragraph.layout.initial_cursor(), + &mut TextRenderer, + ); + if matches!(fit, LayoutFit::OutOfBounds { .. }) { + break; + } + char_offset = 0; } } } @@ -88,7 +107,6 @@ where pub struct Paragraph { content: T, layout: TextLayout, - size: Offset, } impl Paragraph @@ -96,18 +114,7 @@ where T: AsRef<[u8]>, { pub fn new(content: T, layout: TextLayout) -> Self { - Self { - size: Self::measure(&content, layout), - content, - layout, - } - } - - fn measure(content: &T, layout: TextLayout) -> Offset { - Offset::new( - layout.bounds.width(), - layout.measure_text_height(content.as_ref()), - ) + Self { content, layout } } } @@ -116,7 +123,7 @@ where T: AsRef<[u8]>, { fn get_size(&mut self) -> Offset { - self.size + self.layout.bounds.size() } fn set_area(&mut self, area: Rect) { @@ -124,22 +131,108 @@ where } } -impl Component for Paragraph +struct PageBreakIterator<'a, T> { + paragraphs: &'a mut Paragraphs, + para_offset: usize, + char_offset: usize, +} + +/// Yields indices to beginnings of successive pages. As a side effect it +/// updates the bounding box of each paragraph on the page. Because a paragraph +/// can be rendered on multiple pages, such bounding boxes are only valid for +/// paragraphs processed in the last call to `next`. +/// +/// The boxes are simply stacked below each other and may be further arranged +/// before drawing. +impl<'a, T> Iterator for PageBreakIterator<'a, T> where T: AsRef<[u8]>, { - type Msg = Never; + /// Paragraph index, character index, number of paragraphs shown. + type Item = (usize, usize, usize); - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { - None + fn next(&mut self) -> Option { + if self.para_offset >= self.paragraphs.list.len() { + return None; + } + + let old_para_offset = self.para_offset; + let old_char_offset = self.char_offset; + let mut area = self.paragraphs.area; + + for paragraph in self.paragraphs.list.iter_mut().skip(self.para_offset) { + loop { + paragraph.set_area(area); + let fit = paragraph.layout.layout_text( + ¶graph.content.as_ref()[self.char_offset..], + &mut paragraph.layout.initial_cursor(), + &mut TextNoOp, + ); + match fit { + LayoutFit::Fitting { size, .. } => { + // Text fits, update the bounding box. + let (used, free) = area.hsplit(size.y); + paragraph.set_area(used); + // Continue with next paragraph in remaining space. + area = free; + self.char_offset = 0; + self.para_offset += 1; + break; + } + LayoutFit::OutOfBounds { processed_chars } => { + // Text does not fit, assume whatever fits takes the entire remaining area. + self.char_offset += processed_chars; + let visible = if processed_chars > 0 { + self.para_offset - old_para_offset + 1 + } else { + self.para_offset - old_para_offset + }; + // Return pointer to start of page. + return Some((old_para_offset, old_char_offset, visible)); + } + } + } + } + + // Last page. + Some(( + old_para_offset, + old_char_offset, + self.para_offset - old_para_offset, + )) + } +} + +impl Paginate for Paragraphs +where + T: AsRef<[u8]>, +{ + fn page_count(&mut self) -> usize { + // There's always at least one page. + let page_count = self.break_pages().count().max(1); + + // Reset to first page. + self.change_page(0); + + page_count } - fn paint(&mut self) { - self.layout.layout_text( - self.content.as_ref(), - &mut self.layout.initial_cursor(), - &mut TextRenderer, - ); + fn change_page(&mut self, to_page: usize) { + if let Some((para_offset, char_offset, para_visible)) = + self.break_pages().skip(to_page).next() + { + // Set offsets used by `paint`. + self.para_offset = para_offset; + self.char_offset = char_offset; + + // Arrange visible paragraphs. + let visible = &mut self.list[para_offset..para_offset + para_visible]; + self.layout.arrange(self.area, visible); + } else { + // Should not happen, set index past last paragraph to render empty page. + self.para_offset = self.list.len(); + self.char_offset = 0; + } } } diff --git a/core/embed/rust/src/ui/model_t1/layout.rs b/core/embed/rust/src/ui/model_t1/layout.rs index 82177dbda6..810b823f0d 100644 --- a/core/embed/rust/src/ui/model_t1/layout.rs +++ b/core/embed/rust/src/ui/model_t1/layout.rs @@ -4,7 +4,7 @@ use crate::{ error::Error, micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr}, ui::{ - component::{Child, FormattedText, Paginated, PaginatedMsg}, + component::{text::paragraphs::Paragraphs, Child, FormattedText, Paginated, PaginatedMsg}, display, layout::obj::LayoutObj, }, @@ -72,6 +72,37 @@ extern "C" fn ui_layout_new_confirm_action( unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +#[no_mangle] +extern "C" fn ui_layout_new_confirm_text( + n_args: usize, + args: *const Obj, + kwargs: *const Map, +) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let title: Buffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let data: Buffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?; + let description: Option = + kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; + + let obj = LayoutObj::new(Child::new(Title::new(display::screen(), title, |area| { + Paginated::>::new( + area, + |area| { + Paragraphs::new(area) + .add::( + theme::FONT_NORMAL, + description.unwrap_or("".into()), + ) + .add::(theme::FONT_BOLD, data) + }, + theme::BG, + ) + })))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 8cb82dfac3..b2bffe509e 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -8,6 +8,7 @@ def layout_new_example(text: str) -> None: # extmod/rustmods/modtrezorui2.c def layout_new_confirm_action( + *, title: str, action: str | None, description: str | None, @@ -16,4 +17,14 @@ def layout_new_confirm_action( hold: bool | None, reverse: bool, ) -> int: - """Example layout. All arguments must be passed as kwargs.""" + """Example layout.""" + + +# extmod/rustmods/modtrezorui2.c +def layout_new_confirm_text( + *, + title: str, + data: str, + description: str | None, +) -> int: + """Example layout.""" diff --git a/core/src/trezor/ui/layouts/t1.py b/core/src/trezor/ui/layouts/t1.py index 68bb2cae21..db0fd9e48e 100644 --- a/core/src/trezor/ui/layouts/t1.py +++ b/core/src/trezor/ui/layouts/t1.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from trezor import log, ui, wire from trezor.enums import ButtonRequestType -from trezorui2 import layout_new_confirm_action +from trezorui2 import layout_new_confirm_action, layout_new_confirm_text from .common import interact @@ -63,6 +63,32 @@ async def confirm_action( raise exc +async def confirm_text( + ctx: wire.GenericContext, + br_type: str, + title: str, + data: str, + description: str | None = None, + br_code: ButtonRequestType = ButtonRequestType.Other, + icon: str = ui.ICON_SEND, # TODO cleanup @ redesign + icon_color: int = ui.GREEN, # TODO cleanup @ redesign +) -> None: + result = await interact( + ctx, + ui.RustLayout( + layout_new_confirm_text( + title=title.upper(), + data=data, + description=description, + ) + ), + br_type, + br_code, + ) + if result == 0: + raise wire.ActionCancelled + + async def show_error_and_raise( ctx: wire.GenericContext, br_type: str,