You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/embed/rust/src/ui/model_tt/component/page.rs

680 lines
23 KiB

use crate::ui::{
component::{
base::ComponentExt,
paginated::{AuxPageMsg, PageMsg},
Component, Event, EventCtx, FixedHeightBar, Pad, Paginate,
},
display::{self, toif::Icon, Color},
geometry::{Grid, Insets, Rect},
model_tt::component::{Button, ButtonContent, ButtonMsg},
};
use super::{
hold_to_confirm::{handle_hold_event, CancelHold, CancelHoldMsg},
theme, CancelConfirmMsg, Loader, ScrollBar, Swipe, SwipeDirection,
};
/// Describes behavior of left button.
enum ButtonPrevCancels {
/// Button never causes `PageMsg::Aux(AuxPageMsg::GoBack)` to be emitted.
Never,
/// Button cancels the layout if pressed on the first page. Otherwise it
/// goes to previous page.
FirstPage,
/// Button cancels the layout on any page, except the last where controls
/// are displayed.
AnyPage,
}
impl ButtonPrevCancels {
fn should_cancel(&self, is_first_page: bool) -> bool {
match self {
ButtonPrevCancels::Never => false,
ButtonPrevCancels::FirstPage => is_first_page,
ButtonPrevCancels::AnyPage => true,
}
}
fn icon(&self, is_first_page: bool) -> Icon {
let data = match self {
ButtonPrevCancels::Never => theme::ICON_UP,
ButtonPrevCancels::FirstPage if is_first_page => theme::ICON_CANCEL,
ButtonPrevCancels::FirstPage => theme::ICON_UP,
ButtonPrevCancels::AnyPage => theme::ICON_BACK,
};
Icon::new(data)
}
}
pub struct SwipePage<T, U>
where
U: Component,
{
content: T,
controls: U,
pad: Pad,
swipe: Swipe,
scrollbar: ScrollBar,
button_prev: Button<&'static str>,
button_next: Button<&'static str>,
button_prev_cancels: ButtonPrevCancels,
is_go_back: Option<fn(&U::Msg) -> bool>,
swipe_left: bool,
fade: Option<u16>,
}
impl<T, U> SwipePage<T, U>
where
T: Paginate,
T: Component,
U: Component,
{
pub fn new(content: T, controls: U, background: Color) -> Self {
Self {
content,
controls,
scrollbar: ScrollBar::vertical(),
swipe: Swipe::new(),
pad: Pad::with_background(background),
button_prev: Button::with_icon(Icon::new(theme::ICON_UP)).initially_enabled(false),
button_next: Button::with_icon(Icon::new(theme::ICON_DOWN)),
button_prev_cancels: ButtonPrevCancels::Never,
is_go_back: None,
swipe_left: false,
fade: None,
}
}
pub fn with_back_button(mut self) -> Self {
self.button_prev_cancels = ButtonPrevCancels::AnyPage;
self.button_prev = Button::with_icon(Icon::new(theme::ICON_BACK)).initially_enabled(true);
self
}
pub fn with_cancel_on_first_page(mut self) -> Self {
self.button_prev_cancels = ButtonPrevCancels::FirstPage;
self.button_prev = Button::with_icon(Icon::new(theme::ICON_CANCEL)).initially_enabled(true);
self
}
/// If `controls` message matches the function then we will go page back
/// instead of propagating the message to parent component.
pub fn with_go_back(mut self, is_go_back: fn(&U::Msg) -> bool) -> Self {
self.is_go_back = Some(is_go_back);
self
}
pub fn with_swipe_left(mut self) -> Self {
self.swipe_left = true;
self
}
fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
self.swipe.allow_left = self.swipe_left;
}
fn on_page_change(&mut self, ctx: &mut EventCtx) {
// Adjust the swipe parameters according to the scrollbar.
self.setup_swipe();
// Enable/disable prev/next buttons.
self.button_prev.set_content(
ctx,
ButtonContent::Icon(
self.button_prev_cancels
.icon(self.scrollbar.active_page == 0),
),
);
self.button_prev.enable_if(
ctx,
self.scrollbar.has_previous_page()
|| matches!(
self.button_prev_cancels,
ButtonPrevCancels::FirstPage | ButtonPrevCancels::AnyPage
),
);
self.button_next
.enable_if(ctx, self.scrollbar.has_next_page());
// Change the page in the content, make sure it gets completely repainted and
// clear the background under it.
self.content.change_page(self.scrollbar.active_page);
self.content.request_complete_repaint(ctx);
self.pad.clear();
// Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint.
self.fade = Some(theme::BACKLIGHT_NORMAL);
}
/// Like `place()` but returns area for loader (content + scrollbar) to be
/// used in SwipeHoldPage.
fn place_get_content_area(&mut self, bounds: Rect) -> Rect {
let mut layout = PageLayout::new(bounds);
self.pad.place(bounds);
self.swipe.place(bounds);
self.button_prev.place(layout.button_prev);
self.button_next.place(layout.button_next);
let buttons_area = self.controls.place(layout.controls);
layout.set_buttons_height(buttons_area.height());
self.scrollbar.place(layout.scrollbar);
// Layout the content. Try to fit it on a single page first, and reduce the area
// to make space for a scrollbar if it doesn't fit.
self.content.place(layout.content_single_page);
let page_count = {
let count = self.content.page_count();
if count > 1 {
self.content.place(layout.content);
self.content.page_count() // Make sure to re-count it with the
// new size.
} else {
count // Content fits on a single page.
}
};
// Now that we finally have the page count, we can setup the scrollbar and the
// swiper.
self.scrollbar.set_count_and_active_page(page_count, 0);
self.setup_swipe();
layout.content_single_page.union(layout.scrollbar)
}
}
impl<T, U> Component for SwipePage<T, U>
where
T: Paginate,
T: Component,
U: Component,
{
type Msg = PageMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.place_get_content_area(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.scrollbar.page_count);
if let Some(swipe) = self.swipe.event(ctx, event) {
match swipe {
SwipeDirection::Up => {
// Scroll down, if possible.
self.scrollbar.go_to_next_page();
self.on_page_change(ctx);
return None;
}
SwipeDirection::Down => {
// Scroll up, if possible.
self.scrollbar.go_to_previous_page();
self.on_page_change(ctx);
return None;
}
SwipeDirection::Left if self.swipe_left => {
return Some(PageMsg::Aux(AuxPageMsg::SwipeLeft));
}
_ => {
// Ignore other directions.
}
}
}
if let Some(msg) = self.content.event(ctx, event) {
return Some(PageMsg::Content(msg));
}
if !self.scrollbar.has_next_page() {
if let Some(msg) = self.controls.event(ctx, event) {
// Handle the case when one of the controls buttons is configured to go back a
// page.
if let Some(f) = self.is_go_back {
if f(&msg) {
self.scrollbar.go_to_previous_page();
self.on_page_change(ctx);
return None;
}
}
return Some(PageMsg::Controls(msg));
}
} else {
if let Some(ButtonMsg::Clicked) = self.button_prev.event(ctx, event) {
if self
.button_prev_cancels
.should_cancel(self.scrollbar.active_page == 0)
{
return Some(PageMsg::Aux(AuxPageMsg::GoBack));
}
self.scrollbar.go_to_previous_page();
self.on_page_change(ctx);
return None;
}
if let Some(ButtonMsg::Clicked) = self.button_next.event(ctx, event) {
self.scrollbar.go_to_next_page();
self.on_page_change(ctx);
return None;
}
}
None
}
fn paint(&mut self) {
self.pad.paint();
self.content.paint();
if self.scrollbar.has_pages() {
self.scrollbar.paint();
}
if self.scrollbar.has_next_page() {
self.button_prev.paint();
self.button_next.paint();
} else {
self.controls.paint();
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
self.scrollbar.bounds(sink);
self.content.bounds(sink);
if !self.scrollbar.has_next_page() {
self.controls.bounds(sink);
} else {
self.button_prev.bounds(sink);
self.button_next.bounds(sink);
}
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for SwipePage<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace + Component,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("SwipePage");
t.field("active_page", &self.scrollbar.active_page);
t.field("page_count", &self.scrollbar.page_count);
t.field("content", &self.content);
t.field("controls", &self.controls);
t.close();
}
}
pub struct PageLayout {
/// Content when it fits on single page (no scrollbar).
pub content_single_page: Rect,
/// Content when multiple pages.
pub content: Rect,
/// Scroll bar when multiple pages.
pub scrollbar: Rect,
/// Controls displayed on last page.
pub controls: Rect,
pub button_prev: Rect,
pub button_next: Rect,
}
impl PageLayout {
const SCROLLBAR_WIDTH: i16 = 8;
const SCROLLBAR_SPACE: i16 = 5;
pub fn new(area: Rect) -> Self {
let (controls, _space) = area.split_right(theme::CONTENT_BORDER);
let (_space, content) = area.split_left(theme::CONTENT_BORDER);
let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER);
let (content, scrollbar) =
content.split_right(Self::SCROLLBAR_SPACE + Self::SCROLLBAR_WIDTH);
let (_space, scrollbar) = scrollbar.split_left(Self::SCROLLBAR_SPACE);
let (_, one_row_buttons) = area.split_bottom(theme::BUTTON_HEIGHT);
let grid = Grid::new(one_row_buttons, 1, 2).with_spacing(theme::BUTTON_SPACING);
let button_prev = grid.row_col(0, 0);
let button_next = grid.row_col(0, 1);
Self {
content_single_page,
content,
scrollbar,
controls,
button_prev,
button_next,
}
}
pub fn set_buttons_height(&mut self, height: i16) {
let buttons_inset = Insets::bottom(height + theme::BUTTON_SPACING);
self.content_single_page = self.content_single_page.inset(buttons_inset);
self.content = self.content.inset(buttons_inset);
self.scrollbar = self.scrollbar.inset(buttons_inset);
}
}
pub struct SwipeHoldPage<T> {
inner: SwipePage<T, FixedHeightBar<CancelHold>>,
loader: Loader,
pad: Pad,
}
impl<T> SwipeHoldPage<T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
let buttons = CancelHold::new(theme::button_confirm());
Self {
inner: SwipePage::new(content, buttons, background).with_cancel_on_first_page(),
loader: Loader::new(),
pad: Pad::with_background(background),
}
}
pub fn with_danger(content: T, background: Color) -> Self {
let buttons = CancelHold::new(theme::button_danger());
Self {
inner: SwipePage::new(content, buttons, background).with_cancel_on_first_page(),
loader: Loader::new(),
pad: Pad::with_background(background),
}
}
pub fn without_cancel(content: T, background: Color) -> Self {
let buttons = CancelHold::without_cancel();
Self {
inner: SwipePage::new(content, buttons, background),
loader: Loader::new(),
pad: Pad::with_background(background),
}
}
}
impl<T> Component for SwipeHoldPage<T>
where
T: Paginate,
T: Component,
{
type Msg = PageMsg<T::Msg, CancelConfirmMsg>;
fn place(&mut self, bounds: Rect) -> Rect {
let content_area = self.inner.place_get_content_area(bounds);
self.loader.place(content_area);
self.pad.place(content_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.inner.event(ctx, event);
let button_msg = match msg {
Some(PageMsg::Content(c)) => return Some(PageMsg::Content(c)),
Some(PageMsg::Controls(CancelHoldMsg::Cancelled)) => {
return Some(PageMsg::Controls(CancelConfirmMsg::Cancelled))
}
Some(PageMsg::Controls(CancelHoldMsg::HoldButton(b))) => Some(b),
_ => None,
};
if handle_hold_event(
ctx,
event,
button_msg,
&mut self.loader,
&mut self.pad,
&mut self.inner.content,
) {
return Some(PageMsg::Controls(CancelConfirmMsg::Confirmed));
}
if self.inner.pad.will_paint().is_some() {
self.inner.controls.request_complete_repaint(ctx);
}
None
}
fn paint(&mut self) {
self.pad.paint();
self.inner.pad.paint();
if self.loader.is_animating() {
self.loader.paint()
} else {
self.inner.content.paint();
if self.inner.scrollbar.has_pages() {
self.inner.scrollbar.paint();
}
}
if self.inner.scrollbar.has_next_page() {
self.inner.button_prev.paint();
self.inner.button_next.paint();
} else {
self.inner.controls.paint();
}
if let Some(val) = self.inner.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.loader.bounds(sink);
self.inner.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SwipeHoldPage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.inner.trace(t)
}
}
#[cfg(test)]
mod tests {
use crate::{
trace::Trace,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
Empty,
},
event::TouchEvent,
geometry::Point,
model_tt::{component::Button, constant, theme},
},
};
use super::*;
const SCREEN: Rect = constant::screen().inset(theme::borders());
impl ParagraphStrType for &'static str {
fn skip_prefix(&self, chars: usize) -> Self {
&self[chars..]
}
}
fn trace(val: &impl Trace) -> String {
let mut t = Vec::new();
val.trace(&mut t);
String::from_utf8(t).unwrap()
}
fn swipe(component: &mut impl Component, points: &[(i16, i16)]) {
let last = points.len().saturating_sub(1);
let mut first = true;
let mut ctx = EventCtx::new();
for (i, &(x, y)) in points.iter().enumerate() {
let p = Point::new(x, y);
let ev = if first {
TouchEvent::TouchStart(p)
} else if i == last {
TouchEvent::TouchEnd(p)
} else {
TouchEvent::TouchMove(p)
};
component.event(&mut ctx, Event::Touch(ev));
ctx.clear();
first = false;
}
}
fn swipe_up(component: &mut impl Component) {
swipe(component, &[(20, 100), (20, 60), (20, 20)])
}
fn swipe_down(component: &mut impl Component) {
swipe(component, &[(20, 20), (20, 60), (20, 100)])
}
#[test]
fn paragraphs_empty() {
let mut page = SwipePage::new(
Paragraphs::<[Paragraph<&'static str>; 0]>::new([]),
Empty,
theme::BG,
);
page.place(SCREEN);
let expected =
"<SwipePage active_page:0 page_count:1 content:<Paragraphs > controls:<Empty > >";
assert_eq!(trace(&page), expected);
swipe_up(&mut page);
assert_eq!(trace(&page), expected);
swipe_down(&mut page);
assert_eq!(trace(&page), expected);
}
#[test]
fn paragraphs_single() {
let mut page = SwipePage::new(
Paragraphs::new([
Paragraph::new(
&theme::TEXT_NORMAL,
"This is the first paragraph and it should fit on the screen entirely.",
),
Paragraph::new(
&theme::TEXT_BOLD,
"Second, bold, paragraph should also fit on the screen whole I think.",
),
]),
Empty,
theme::BG,
);
page.place(SCREEN);
let expected = "<SwipePage active_page:0 page_count:1 content:<Paragraphs This is the first\nparagraph and it should\nfit on the screen\nentirely.\nSecond, bold, paragraph\nshould also fit on the\nscreen whole I think.\n> controls:<Empty > >";
assert_eq!(trace(&page), expected);
swipe_up(&mut page);
assert_eq!(trace(&page), expected);
swipe_down(&mut page);
assert_eq!(trace(&page), expected);
}
#[test]
fn paragraphs_one_long() {
let mut page = SwipePage::new(
Paragraphs::new(
Paragraph::new(
&theme::TEXT_BOLD,
"This is somewhat long paragraph that goes on and on and on and on and on and will definitely not fit on just a single screen. You have to swipe a bit to see all the text it contains I guess. There's just so much letters in it.",
)
),
theme::button_bar(Button::with_text("NO")),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on and\non and will definitely not\nfit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I guess....\n> controls:<FixedHeightBar inner:<Button text:NO > > >";
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs There's just so much\nletters in it.\n> controls:<FixedHeightBar inner:<Button text:NO > > >";
assert_eq!(trace(&page), expected1);
swipe_down(&mut page);
assert_eq!(trace(&page), expected1);
swipe_up(&mut page);
assert_eq!(trace(&page), expected2);
swipe_up(&mut page);
assert_eq!(trace(&page), expected2);
swipe_down(&mut page);
assert_eq!(trace(&page), expected1);
}
#[test]
fn paragraphs_three_long() {
let mut page = SwipePage::new(
Paragraphs::new([
Paragraph::new(
&theme::TEXT_BOLD,
"This paragraph is using a bold font. It doesn't need to be all that long.",
),
Paragraph::new(
&theme::TEXT_MONO,
"And this one is using MONO. Monospace is nice for numbers, they have the same width and can be scanned quickly. Even if they span several pages or something.",
),
Paragraph::new(
&theme::TEXT_BOLD,
"Let's add another one for a good measure. This one should overflow all the way to the third page with a bit of luck.",
),
]),
theme::button_bar(Button::with_text("IDK")),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't need\nto be all that long.\nAnd this one is u\nsing MONO. Monosp\nace is nice for n\numbers, they...\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs ...have the same\nwidth and can be\nscanned quickly.\nEven if they span\nseveral pages or\nsomething.\nLet's add another one...\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs for a good measure. This\none should overflow all\nthe way to the third page\nwith a bit of luck.\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
assert_eq!(trace(&page), expected1);
swipe_down(&mut page);
assert_eq!(trace(&page), expected1);
swipe_up(&mut page);
assert_eq!(trace(&page), expected2);
swipe_up(&mut page);
assert_eq!(trace(&page), expected3);
swipe_up(&mut page);
assert_eq!(trace(&page), expected3);
swipe_down(&mut page);
assert_eq!(trace(&page), expected2);
swipe_down(&mut page);
assert_eq!(trace(&page), expected1);
swipe_down(&mut page);
assert_eq!(trace(&page), expected1);
}
#[test]
fn paragraphs_hard_break() {
let mut page = SwipePage::new(
Paragraphs::new([
Paragraph::new(&theme::TEXT_NORMAL, "Short one.").break_after(),
Paragraph::new(&theme::TEXT_NORMAL, "Short two.").break_after(),
Paragraph::new(&theme::TEXT_NORMAL, "Short three.").break_after(),
]),
theme::button_bar(Empty),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs Short one.\n> controls:<FixedHeightBar inner:<Empty > > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs Short two.\n> controls:<FixedHeightBar inner:<Empty > > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs Short three.\n> controls:<FixedHeightBar inner:<Empty > > >";
assert_eq!(trace(&page), expected1);
swipe_up(&mut page);
assert_eq!(trace(&page), expected2);
swipe_up(&mut page);
assert_eq!(trace(&page), expected3);
swipe_up(&mut page);
assert_eq!(trace(&page), expected3);
}
}