mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-23 14:58:09 +00:00
feat(core/rust): Add Paginated component
This commit is contained in:
parent
a7a305d34d
commit
a3c79bf4f7
@ -4,12 +4,14 @@ pub mod base;
|
||||
pub mod empty;
|
||||
pub mod label;
|
||||
pub mod map;
|
||||
pub mod pad;
|
||||
pub mod text;
|
||||
pub mod tuple;
|
||||
|
||||
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
|
||||
pub use empty::Empty;
|
||||
pub use label::{Label, LabelStyle};
|
||||
pub use pad::Pad;
|
||||
pub use text::{
|
||||
formatted::FormattedText,
|
||||
layout::{LineBreaking, PageBreaking, TextLayout},
|
||||
|
32
core/embed/rust/src/ui/component/pad.rs
Normal file
32
core/embed/rust/src/ui/component/pad.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::ui::{
|
||||
display::{self, Color},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
pub struct Pad {
|
||||
area: Rect,
|
||||
color: Color,
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
pub fn with_background(area: Rect, color: Color) -> Self {
|
||||
Self {
|
||||
area,
|
||||
color,
|
||||
clear: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.clear = true;
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) {
|
||||
if self.clear {
|
||||
self.clear = false;
|
||||
|
||||
display::rect(self.area, self.color);
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,8 @@ use crate::ui::{
|
||||
};
|
||||
|
||||
use super::layout::{
|
||||
DefaultTextTheme, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer,
|
||||
DefaultTextTheme, LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout,
|
||||
TextRenderer,
|
||||
};
|
||||
|
||||
pub const MAX_ARGUMENTS: usize = 6;
|
||||
@ -21,6 +22,7 @@ pub struct FormattedText<F, T> {
|
||||
layout: TextLayout,
|
||||
format: F,
|
||||
args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
impl<F, T> FormattedText<F, T> {
|
||||
@ -29,6 +31,7 @@ impl<F, T> FormattedText<F, T> {
|
||||
layout: TextLayout::new::<D>(area),
|
||||
format,
|
||||
args: LinearMap::new(),
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +68,14 @@ impl<F, T> FormattedText<F, T> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_char_offset(&mut self, char_offset: usize) {
|
||||
self.char_offset = char_offset;
|
||||
}
|
||||
|
||||
pub fn char_offset(&mut self) -> usize {
|
||||
self.char_offset
|
||||
}
|
||||
|
||||
pub fn layout_mut(&mut self) -> &mut TextLayout {
|
||||
&mut self.layout
|
||||
}
|
||||
@ -75,19 +86,22 @@ where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn layout_content(&self, sink: &mut dyn LayoutSink) {
|
||||
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
let mut cursor = self.layout.initial_cursor();
|
||||
let mut ops = Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg {
|
||||
Token::Literal(literal) => Some(Op::Text(literal)),
|
||||
Token::Argument(b"mono") => Some(Op::Font(self.layout.mono_font)),
|
||||
Token::Argument(b"bold") => Some(Op::Font(self.layout.bold_font)),
|
||||
Token::Argument(b"normal") => Some(Op::Font(self.layout.normal_font)),
|
||||
Token::Argument(argument) => self
|
||||
.args
|
||||
.get(argument)
|
||||
.map(|value| Op::Text(value.as_ref())),
|
||||
});
|
||||
self.layout.layout_ops(&mut ops, &mut cursor, sink);
|
||||
let mut ops = Op::skip_n_text_bytes(
|
||||
Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg {
|
||||
Token::Literal(literal) => Some(Op::Text(literal)),
|
||||
Token::Argument(b"mono") => Some(Op::Font(self.layout.mono_font)),
|
||||
Token::Argument(b"bold") => Some(Op::Font(self.layout.bold_font)),
|
||||
Token::Argument(b"normal") => Some(Op::Font(self.layout.normal_font)),
|
||||
Token::Argument(argument) => self
|
||||
.args
|
||||
.get(argument)
|
||||
.map(|value| Op::Text(value.as_ref())),
|
||||
}),
|
||||
self.char_offset,
|
||||
);
|
||||
self.layout.layout_ops(&mut ops, &mut cursor, sink)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ impl TextLayout {
|
||||
// cursor.
|
||||
let init_cursor = self.initial_cursor();
|
||||
let mut cursor = init_cursor;
|
||||
self.layout_ops(ops, &mut cursor, &mut TextNoop);
|
||||
self.layout_ops(ops, &mut cursor, &mut TextNoOp);
|
||||
cursor.y - init_cursor.y + self.text_font.line_height()
|
||||
}
|
||||
|
||||
@ -219,7 +219,7 @@ impl TextLayout {
|
||||
// cursor.
|
||||
let init_cursor = self.initial_cursor();
|
||||
let mut cursor = init_cursor;
|
||||
self.layout_text(text, &mut cursor, &mut TextNoop);
|
||||
self.layout_text(text, &mut cursor, &mut TextNoOp);
|
||||
cursor.y - init_cursor.y + self.text_font.line_height()
|
||||
}
|
||||
}
|
||||
@ -238,9 +238,9 @@ pub trait LayoutSink {
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct TextNoop;
|
||||
pub struct TextNoOp;
|
||||
|
||||
impl LayoutSink for TextNoop {}
|
||||
impl LayoutSink for TextNoOp {}
|
||||
|
||||
pub struct TextRenderer;
|
||||
|
||||
@ -325,7 +325,7 @@ impl<'a> Op<'a> {
|
||||
skipped = skipped.saturating_add(text.len());
|
||||
if skipped > skip_bytes {
|
||||
let leave_bytes = skipped - skip_bytes;
|
||||
Some(Op::Text(&text[..text.len() - leave_bytes]))
|
||||
Some(Op::Text(&text[text.len() - leave_bytes..]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
use crate::{
|
||||
time::Instant,
|
||||
ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx},
|
||||
display::{self, Color},
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
@ -110,31 +109,3 @@ where
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
||||
struct Pad {
|
||||
area: Rect,
|
||||
color: Color,
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
fn with_background(area: Rect, color: Color) -> Self {
|
||||
Self {
|
||||
area,
|
||||
color,
|
||||
clear: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.clear = true;
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
if self.clear {
|
||||
self.clear = false;
|
||||
|
||||
display::rect(self.area, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ mod confirm;
|
||||
mod dialog;
|
||||
mod loader;
|
||||
mod page;
|
||||
mod paginated;
|
||||
mod passphrase;
|
||||
mod pin;
|
||||
mod swipe;
|
||||
@ -11,6 +12,7 @@ pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet
|
||||
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
pub use dialog::{Dialog, DialogLayout, DialogMsg};
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||
pub use paginated::{Paginate, Paginated};
|
||||
pub use swipe::{Swipe, SwipeDirection};
|
||||
|
||||
use super::{event, theme};
|
||||
|
@ -21,14 +21,30 @@ impl<T> Page<T> {
|
||||
pub fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self {
|
||||
let scrollbar = ScrollBar::vertical_right(area, page_count, active_page);
|
||||
let mut swipe = Swipe::new(area);
|
||||
swipe.allow_up = scrollbar.has_next_page();
|
||||
swipe.allow_down = scrollbar.has_previous_page();
|
||||
Self::setup_swipe(&scrollbar, &mut swipe);
|
||||
Self {
|
||||
swipe,
|
||||
scrollbar,
|
||||
page,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_swipe(scrollbar: &ScrollBar, swipe: &mut Swipe) {
|
||||
swipe.allow_up = scrollbar.has_next_page();
|
||||
swipe.allow_down = scrollbar.has_previous_page();
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut T {
|
||||
&mut self.page
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.scrollbar.page_count
|
||||
}
|
||||
|
||||
pub fn active_page(&self) -> usize {
|
||||
self.scrollbar.active_page
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component> Component for Page<T> {
|
||||
@ -39,11 +55,15 @@ impl<T: Component> Component for Page<T> {
|
||||
match swipe {
|
||||
SwipeDirection::Up => {
|
||||
// Scroll down, if possible.
|
||||
return Some(PageMsg::ChangePage(self.scrollbar.next_page()));
|
||||
self.scrollbar.go_to_next_page();
|
||||
Self::setup_swipe(&self.scrollbar, &mut self.swipe);
|
||||
return Some(PageMsg::ChangePage(self.active_page()));
|
||||
}
|
||||
SwipeDirection::Down => {
|
||||
// Scroll up, if possible.
|
||||
return Some(PageMsg::ChangePage(self.scrollbar.previous_page()));
|
||||
self.scrollbar.go_to_previous_page();
|
||||
Self::setup_swipe(&self.scrollbar, &mut self.swipe);
|
||||
return Some(PageMsg::ChangePage(self.active_page()));
|
||||
}
|
||||
_ => {
|
||||
// Ignore other directions.
|
||||
@ -62,6 +82,20 @@ impl<T: Component> Component for Page<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Page<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Page");
|
||||
t.field("active_page", &self.active_page());
|
||||
t.field("page_count", &self.page_count());
|
||||
t.field("content", &self.page);
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
page_count: usize,
|
||||
@ -88,12 +122,12 @@ impl ScrollBar {
|
||||
self.active_page > 0
|
||||
}
|
||||
|
||||
pub fn next_page(&self) -> usize {
|
||||
self.active_page.saturating_add(1).min(self.page_count - 1)
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
|
||||
}
|
||||
|
||||
pub fn previous_page(&self) -> usize {
|
||||
self.active_page.saturating_sub(1)
|
||||
pub fn go_to_previous_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
132
core/embed/rust/src/ui/model_tt/component/paginated.rs
Normal file
132
core/embed/rust/src/ui/model_tt/component/paginated.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use crate::ui::{
|
||||
component::{
|
||||
text::layout::{LayoutFit, TextNoOp},
|
||||
Component, ComponentExt, Event, EventCtx, FormattedText, Pad,
|
||||
},
|
||||
display,
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{
|
||||
page::{Page, PageMsg},
|
||||
theme,
|
||||
};
|
||||
|
||||
pub struct Paginated<T> {
|
||||
page: Page<T>,
|
||||
pad: Pad,
|
||||
fade_after_next_paint: Option<i32>,
|
||||
}
|
||||
|
||||
impl<T> Paginated<T>
|
||||
where
|
||||
T: Paginate,
|
||||
{
|
||||
pub fn new(area: Rect, mut content: T) -> Self {
|
||||
let active_page = 0;
|
||||
let page_count = content.page_count();
|
||||
Self {
|
||||
page: Page::new(area, content, page_count, active_page),
|
||||
pad: Pad::with_background(area, theme::BG),
|
||||
fade_after_next_paint: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Paginated<T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.page.event(ctx, event).and_then(|msg| match msg {
|
||||
PageMsg::Content(c) => Some(c),
|
||||
PageMsg::ChangePage(page) => {
|
||||
self.fade_after_next_paint = Some(theme::BACKLIGHT_NORMAL);
|
||||
self.page.inner_mut().change_page(page);
|
||||
self.page.inner_mut().request_complete_repaint(ctx);
|
||||
self.pad.clear();
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.page.paint();
|
||||
if let Some(val) = self.fade_after_next_paint.take() {
|
||||
display::fade_backlight(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Paginated<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
self.page.trace(t);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Paginate {
|
||||
fn page_count(&mut self) -> usize;
|
||||
fn change_page(&mut self, active_page: usize);
|
||||
}
|
||||
|
||||
impl<F, T> Paginate for FormattedText<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
let mut page_count = 1; // There's always at least one page.
|
||||
let mut char_offset = 0;
|
||||
|
||||
loop {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds { processed_chars } => {
|
||||
page_count += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the char offset back to the beginning.
|
||||
self.set_char_offset(0);
|
||||
|
||||
page_count
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
let mut active_page = 0;
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
while active_page < to_page {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds { processed_chars } => {
|
||||
active_page += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -264,7 +264,6 @@ impl TextBox {
|
||||
fn replace_last(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
self.text.pop();
|
||||
if self.text.push(char).is_err() {
|
||||
// Should not happen unless `self.text` has zero capacity.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("textbox has zero capacity");
|
||||
}
|
||||
@ -273,7 +272,6 @@ impl TextBox {
|
||||
|
||||
fn append(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
if self.text.push(char).is_err() {
|
||||
// `self.text` is full, ignore this change.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("textbox is full");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user