From bea696dfc913da75005a1f8344a468e580790eac Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Wed, 8 Dec 2021 12:10:18 +0100 Subject: [PATCH] feat(core/rust): Paginated for T1 [no changelog] --- core/embed/rust/src/ui/component/mod.rs | 2 +- core/embed/rust/src/ui/component/paginated.rs | 41 ++- .../rust/src/ui/model_t1/component/mod.rs | 2 + .../rust/src/ui/model_t1/component/page.rs | 235 ++++++++++++++++++ core/embed/rust/src/ui/model_t1/layout.rs | 55 ++-- .../rust/src/ui/model_tt/component/page.rs | 6 +- 6 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 core/embed/rust/src/ui/model_t1/component/page.rs diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index bb1d0a6f1..10a92b09e 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -13,7 +13,7 @@ pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToke pub use empty::Empty; pub use label::{Label, LabelStyle}; pub use pad::Pad; -pub use paginated::{Paginate, Paginated}; +pub use paginated::{Paginate, Paginated, PaginatedMsg}; pub use text::{ formatted::FormattedText, layout::{LineBreaking, PageBreaking, TextLayout}, diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index d6f25a893..8ae6aabfd 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -7,27 +7,50 @@ use crate::ui::{ geometry::Rect, }; +/// Implementations of `Page` wrap the component being paged. They also contain +/// model-dependent logic like: +/// +/// * rendering scrollbar +/// * detecting swipe on TT +/// * buttons for changing pages on T1 +/// * fading backlight pub trait Page { type Content; - fn new(area: Rect, page: Self::Content, page_count: usize, active_page: usize) -> Self; - fn inner_mut(&mut self) -> &mut Self::Content; fn page_count(&self) -> usize; fn active_page(&self) -> usize; fn fade_after_next_paint(&mut self); + fn content_area(area: Rect) -> Rect; } -pub enum PageMsg { +/// Implementation of `Page` is a `Component` returning this message. +pub enum PageMsg { + /// Pass-through from paged component. Content(T), + + /// Pass-through from other `Component`s. + Controls(U), + + /// Page change requested. ChangePage(usize), } +/// Handles page redraw on `ChangePage` message, and other model-agnostic logic. pub struct Paginated

{ page: P, pad: Pad, } +pub enum PaginatedMsg { + /// Pass-through from the paged `Component`. + Content(T), + + /// Messages from page controls outside the paged component. Currently only + /// used on T1 for "OK" and "Cancel" buttons. + Controls(U), +} + impl

Paginated

where P: Page, @@ -35,7 +58,7 @@ where { pub fn new(area: Rect, content: impl FnOnce(Rect) -> P::Content, background: Color) -> Self { let active_page = 0; - let mut content = content(area); + let mut content = content(P::content_area(area)); let page_count = content.page_count(); Self { page: P::new(area, content, page_count, active_page), @@ -44,18 +67,20 @@ where } } -impl

Component for Paginated

+// C is type of message returned by page controls. +impl Component for Paginated

where P: Page, - P: Component::Content as Component>::Msg>>, + P: Component::Content as Component>::Msg, C>>, P::Content: Paginate, P::Content: Component, { - type Msg = <

::Content as Component>::Msg; + type Msg = PaginatedMsg<<

::Content as Component>::Msg, C>; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.page.event(ctx, event).and_then(|msg| match msg { - PageMsg::Content(c) => Some(c), + PageMsg::Content(c) => Some(PaginatedMsg::Content(c)), + PageMsg::Controls(c) => Some(PaginatedMsg::Controls(c)), PageMsg::ChangePage(page) => { self.page.fade_after_next_paint(); self.page.inner_mut().change_page(page); diff --git a/core/embed/rust/src/ui/model_t1/component/mod.rs b/core/embed/rust/src/ui/model_t1/component/mod.rs index 89307f4b1..9e614d679 100644 --- a/core/embed/rust/src/ui/model_t1/component/mod.rs +++ b/core/embed/rust/src/ui/model_t1/component/mod.rs @@ -1,9 +1,11 @@ mod button; mod dialog; +mod page; mod title; use super::{event, theme}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet}; pub use dialog::{Dialog, DialogMsg}; +pub use page::ButtonPage; pub use title::Title; diff --git a/core/embed/rust/src/ui/model_t1/component/page.rs b/core/embed/rust/src/ui/model_t1/component/page.rs new file mode 100644 index 000000000..9757b3a47 --- /dev/null +++ b/core/embed/rust/src/ui/model_t1/component/page.rs @@ -0,0 +1,235 @@ +use crate::ui::{ + component::{ + paginated::{Page, PageMsg}, + Component, Event, EventCtx, Never, + }, + display, + geometry::{Offset, Point, Rect}, +}; + +use super::{theme, Button, ButtonMsg, ButtonPos}; + +pub struct ButtonPage { + scrollbar: ScrollBar, + prev: Button<&'static str>, + next: Button<&'static str>, + cancel: Button<&'static str>, + confirm: Button<&'static str>, + page: T, +} + +impl ButtonPage { + fn areas(area: Rect) -> (Rect, Rect, Rect) { + let button_height = theme::FONT_BOLD.line_height() + 2; + let (content_area, button_area) = area.hsplit(-button_height); + let (content_area, scrollbar_area) = content_area.vsplit(-ScrollBar::WIDTH); + (content_area, scrollbar_area, button_area) + } +} + +impl Page for ButtonPage { + type Content = T; + + fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self { + let (_content_area, scrollbar_area, button_area) = Self::areas(area); + let scrollbar = ScrollBar::vertical_right(scrollbar_area, page_count, active_page); + let prev = Button::with_text(button_area, ButtonPos::Left, "BACK", theme::button_cancel()); + let next = Button::with_text( + button_area, + ButtonPos::Right, + "NEXT", + theme::button_default(), + ); + let cancel = Button::with_text( + button_area, + ButtonPos::Left, + "CANCEL", + theme::button_cancel(), + ); + let confirm = Button::with_text( + button_area, + ButtonPos::Right, + "CONFIRM", + theme::button_default(), + ); + Self { + scrollbar, + prev, + next, + cancel, + confirm, + page, + } + } + + fn inner_mut(&mut self) -> &mut T { + &mut self.page + } + + fn page_count(&self) -> usize { + self.scrollbar.page_count + } + + fn active_page(&self) -> usize { + self.scrollbar.active_page + } + + fn fade_after_next_paint(&mut self) {} + + fn content_area(area: Rect) -> Rect { + Self::areas(area).0 + } +} + +impl Component for ButtonPage { + type Msg = PageMsg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if self.scrollbar.has_previous_page() { + if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) { + // Scroll up. + self.scrollbar.go_to_previous_page(); + return Some(PageMsg::ChangePage(self.active_page())); + } + } else { + if let Some(ButtonMsg::Clicked) = self.cancel.event(ctx, event) { + return Some(PageMsg::Controls(false)); + } + } + + if self.scrollbar.has_next_page() { + if let Some(ButtonMsg::Clicked) = self.next.event(ctx, event) { + // Scroll down. + self.scrollbar.go_to_next_page(); + return Some(PageMsg::ChangePage(self.active_page())); + } + } else { + if let Some(ButtonMsg::Clicked) = self.confirm.event(ctx, event) { + return Some(PageMsg::Controls(true)); + } + } + + if let Some(msg) = self.page.event(ctx, event) { + return Some(PageMsg::Content(msg)); + } + None + } + + fn paint(&mut self) { + self.page.paint(); + self.scrollbar.paint(); + if self.scrollbar.has_previous_page() { + self.prev.paint(); + } else { + self.cancel.paint(); + } + if self.scrollbar.has_next_page() { + self.next.paint(); + } else { + self.confirm.paint(); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ButtonPage +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("ButtonPage"); + 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, + active_page: usize, +} + +impl ScrollBar { + pub const WIDTH: i32 = 8; + pub const DOT_SIZE: Offset = Offset::new(4, 4); + pub const DOT_INTERVAL: i32 = 6; + + pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self { + Self { + area, + page_count, + active_page, + } + } + + pub fn has_next_page(&self) -> bool { + self.active_page < self.page_count - 1 + } + + pub fn has_previous_page(&self) -> bool { + self.active_page > 0 + } + + pub fn go_to_next_page(&mut self) { + self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1); + } + + pub fn go_to_previous_page(&mut self) { + self.active_page = self.active_page.saturating_sub(1); + } + + fn paint_dot(&self, active: bool, top_left: Point) { + let sides = [ + Rect::from_top_left_and_size(top_left + Offset::new(1, 0), Offset::new(2, 1)), + Rect::from_top_left_and_size(top_left + Offset::new(0, 1), Offset::new(1, 2)), + Rect::from_top_left_and_size( + top_left + Offset::new(1, Self::DOT_SIZE.y - 1), + Offset::new(2, 1), + ), + Rect::from_top_left_and_size( + top_left + Offset::new(Self::DOT_SIZE.x - 1, 1), + Offset::new(1, 2), + ), + ]; + for side in sides { + display::rect(side, theme::FG) + } + if active { + display::rect( + Rect::from_top_left_and_size(top_left, Self::DOT_SIZE).inset(1), + theme::FG, + ) + } + } +} + +impl Component for ScrollBar { + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + let count = self.page_count as i32; + let interval = { + let available_height = self.area.height(); + let naive_height = count * Self::DOT_INTERVAL; + if naive_height > available_height { + available_height / count + } else { + Self::DOT_INTERVAL + } + }; + let mut dot = Point::new( + self.area.center().x - Self::DOT_SIZE.x / 2, + self.area.center().y - (count / 2) * interval, + ); + for i in 0..self.page_count { + self.paint_dot(i == self.active_page, dot); + dot.y += interval + } + } +} diff --git a/core/embed/rust/src/ui/model_t1/layout.rs b/core/embed/rust/src/ui/model_t1/layout.rs index 43c8c3e3f..82177dbda 100644 --- a/core/embed/rust/src/ui/model_t1/layout.rs +++ b/core/embed/rust/src/ui/model_t1/layout.rs @@ -4,7 +4,7 @@ use crate::{ error::Error, micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr}, ui::{ - component::{Child, FormattedText}, + component::{Child, FormattedText, Paginated, PaginatedMsg}, display, layout::obj::LayoutObj, }, @@ -12,22 +12,17 @@ use crate::{ }; use super::{ - component::{Button, Dialog, DialogMsg, Title}, + component::{Button, ButtonPage, Title}, theme, }; -impl TryFrom> for Obj -where - Obj: TryFrom, - Error: From<>::Error>, -{ +impl TryFrom> for Obj { type Error = Error; - fn try_from(val: DialogMsg) -> Result { + fn try_from(val: PaginatedMsg) -> Result { match val { - DialogMsg::Content(c) => Ok(c.try_into()?), - DialogMsg::LeftClicked => 1.try_into(), - DialogMsg::RightClicked => 2.try_into(), + PaginatedMsg::Content(_) => 2.try_into(), + PaginatedMsg::Controls(c) => Ok(c.into()), } } } @@ -61,18 +56,17 @@ extern "C" fn ui_layout_new_confirm_action( let right = verb .map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_default())); - let obj = LayoutObj::new(Child::new(Dialog::new( - display::screen(), - |area| { - Title::new(area, title, |area| { + let obj = LayoutObj::new(Child::new(Title::new(display::screen(), title, |area| { + Paginated::>::new( + area, + |area| { FormattedText::new::(area, format) .with(b"action", action.unwrap_or("".into())) .with(b"description", description.unwrap_or("".into())) - }) - }, - left, - right, - )))?; + }, + theme::BG, + ) + })))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -80,7 +74,10 @@ extern "C" fn ui_layout_new_confirm_action( #[cfg(test)] mod tests { - use crate::trace::{Trace, Tracer}; + use crate::{ + trace::{Trace, Tracer}, + ui::model_t1::component::{Dialog, DialogMsg}, + }; use super::*; @@ -125,6 +122,22 @@ mod tests { String::from_utf8(t).unwrap() } + impl TryFrom> for Obj + where + Obj: TryFrom, + Error: From<>::Error>, + { + type Error = Error; + + fn try_from(val: DialogMsg) -> Result { + match val { + DialogMsg::Content(c) => Ok(c.try_into()?), + DialogMsg::LeftClicked => 1.try_into(), + DialogMsg::RightClicked => 2.try_into(), + } + } + } + #[test] fn trace_example_layout() { let layout = Child::new(Dialog::new( diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 37feca9b7..f4834d6c0 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -53,10 +53,14 @@ impl Page for SwipePage { fn fade_after_next_paint(&mut self) { self.fade = Some(theme::BACKLIGHT_NORMAL); } + + fn content_area(area: Rect) -> Rect { + area + } } impl Component for SwipePage { - type Msg = PageMsg; + type Msg = PageMsg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if let Some(swipe) = self.swipe.event(ctx, event) {