1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

feat(core/rust): Add Paginated component

This commit is contained in:
Jan Pochyla 2021-11-26 19:46:55 +01:00 committed by matejcik
parent a7a305d34d
commit a3c79bf4f7
9 changed files with 243 additions and 58 deletions

View File

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

View 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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}
}
}
}
}

View File

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