1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 14:28:07 +00:00

feat(core/rust): impl Paginate for Paragraphs

[no changelog]
This commit is contained in:
Martin Milata 2021-12-13 12:22:52 +01:00 committed by matejcik
parent f29ccf6009
commit 8fb28e4af5
6 changed files with 210 additions and 35 deletions

View File

@ -30,6 +30,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj,
ui_layout_new_example); ui_layout_new_example);
#elif TREZOR_MODEL == 1 #elif TREZOR_MODEL == 1
/// def layout_new_confirm_action( /// def layout_new_confirm_action(
/// *,
/// title: str, /// title: str,
/// action: str | None, /// action: str | None,
/// description: 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, /// hold: bool | None,
/// reverse: bool, /// reverse: bool,
/// ) -> int: /// ) -> 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, STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_action_obj,
0, ui_layout_new_confirm_action); 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 #endif
STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { 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 #elif TREZOR_MODEL == 1
{MP_ROM_QSTR(MP_QSTR_layout_new_confirm_action), {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_action),
MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_action_obj)}, 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 #endif
}; };

View File

@ -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_example(mp_obj_t);
mp_obj_t ui_layout_new_confirm_action(size_t n_args, const mp_obj_t *args, mp_obj_t ui_layout_new_confirm_action(size_t n_args, const mp_obj_t *args,
mp_map_t *kwargs); 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 #ifdef TREZOR_EMULATOR
mp_obj_t ui_debug_layout_type(); mp_obj_t ui_debug_layout_type();

View File

@ -1,12 +1,12 @@
use heapless::Vec; use heapless::Vec;
use crate::ui::{ use crate::ui::{
component::{Component, Event, EventCtx, Never}, component::{Component, Event, EventCtx, Never, Paginate},
display::Font, display::Font,
geometry::{Dimensions, LinearLayout, Offset, Rect}, 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; pub const MAX_PARAGRAPHS: usize = 6;
@ -14,6 +14,8 @@ pub struct Paragraphs<T> {
area: Rect, area: Rect,
list: Vec<Paragraph<T>, MAX_PARAGRAPHS>, list: Vec<Paragraph<T>, MAX_PARAGRAPHS>,
layout: LinearLayout, layout: LinearLayout,
para_offset: usize,
char_offset: usize,
} }
impl<T> Paragraphs<T> impl<T> Paragraphs<T>
@ -25,6 +27,8 @@ where
area, area,
list: Vec::new(), list: Vec::new(),
layout: LinearLayout::vertical().align_at_center().with_spacing(10), layout: LinearLayout::vertical().align_at_center().with_spacing(10),
para_offset: 0,
char_offset: 0,
} }
} }
@ -34,6 +38,9 @@ where
} }
pub fn add<D: DefaultTextTheme>(mut self, text_font: Font, content: T) -> Self { pub fn add<D: DefaultTextTheme>(mut self, text_font: Font, content: T) -> Self {
if content.as_ref().len() == 0 {
return self;
}
let paragraph = Paragraph::new( let paragraph = Paragraph::new(
content, content,
TextLayout { TextLayout {
@ -48,9 +55,12 @@ where
self self
} }
pub fn arrange(mut self) -> Self { fn break_pages<'a>(&'a mut self) -> PageBreakIterator<'a, T> {
self.layout.arrange(self.area, &mut self.list); PageBreakIterator {
self paragraphs: self,
para_offset: 0,
char_offset: 0,
}
} }
} }
@ -65,8 +75,17 @@ where
} }
fn paint(&mut self) { fn paint(&mut self) {
for paragraph in &mut self.list { let mut char_offset = self.char_offset;
paragraph.paint(); for paragraph in self.list.iter().skip(self.para_offset) {
let fit = paragraph.layout.layout_text(
&paragraph.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<T> { pub struct Paragraph<T> {
content: T, content: T,
layout: TextLayout, layout: TextLayout,
size: Offset,
} }
impl<T> Paragraph<T> impl<T> Paragraph<T>
@ -96,18 +114,7 @@ where
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
pub fn new(content: T, layout: TextLayout) -> Self { pub fn new(content: T, layout: TextLayout) -> Self {
Self { Self { content, layout }
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()),
)
} }
} }
@ -116,7 +123,7 @@ where
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
fn get_size(&mut self) -> Offset { fn get_size(&mut self) -> Offset {
self.size self.layout.bounds.size()
} }
fn set_area(&mut self, area: Rect) { fn set_area(&mut self, area: Rect) {
@ -124,22 +131,108 @@ where
} }
} }
impl<T> Component for Paragraph<T> struct PageBreakIterator<'a, T> {
paragraphs: &'a mut Paragraphs<T>,
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 where
T: AsRef<[u8]>, 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<Self::Msg> { fn next(&mut self) -> Option<Self::Item> {
None 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(
&paragraph.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<T> Paginate for Paragraphs<T>
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) { fn change_page(&mut self, to_page: usize) {
self.layout.layout_text( if let Some((para_offset, char_offset, para_visible)) =
self.content.as_ref(), self.break_pages().skip(to_page).next()
&mut self.layout.initial_cursor(), {
&mut TextRenderer, // 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;
}
} }
} }

View File

@ -4,7 +4,7 @@ use crate::{
error::Error, error::Error,
micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr}, micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr},
ui::{ ui::{
component::{Child, FormattedText, Paginated, PaginatedMsg}, component::{text::paragraphs::Paragraphs, Child, FormattedText, Paginated, PaginatedMsg},
display, display,
layout::obj::LayoutObj, 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) } 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<Buffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let obj = LayoutObj::new(Child::new(Title::new(display::screen(), title, |area| {
Paginated::<ButtonPage<_>>::new(
area,
|area| {
Paragraphs::new(area)
.add::<theme::T1DefaultText>(
theme::FONT_NORMAL,
description.unwrap_or("".into()),
)
.add::<theme::T1DefaultText>(theme::FONT_BOLD, data)
},
theme::BG,
)
})))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -8,6 +8,7 @@ def layout_new_example(text: str) -> None:
# extmod/rustmods/modtrezorui2.c # extmod/rustmods/modtrezorui2.c
def layout_new_confirm_action( def layout_new_confirm_action(
*,
title: str, title: str,
action: str | None, action: str | None,
description: str | None, description: str | None,
@ -16,4 +17,14 @@ def layout_new_confirm_action(
hold: bool | None, hold: bool | None,
reverse: bool, reverse: bool,
) -> int: ) -> 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."""

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
from trezor import log, ui, wire from trezor import log, ui, wire
from trezor.enums import ButtonRequestType 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 from .common import interact
@ -63,6 +63,32 @@ async def confirm_action(
raise exc 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( async def show_error_and_raise(
ctx: wire.GenericContext, ctx: wire.GenericContext,
br_type: str, br_type: str,