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:
parent
f29ccf6009
commit
8fb28e4af5
@ -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
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
@ -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(
|
||||||
|
¶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<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(
|
||||||
|
¶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<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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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::{
|
||||||
|
@ -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."""
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user