From 6d1227d839ca0c5b146a9e223878fd0353421fb9 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Tue, 18 Jan 2022 14:51:43 +0100 Subject: [PATCH] feat(core/rust/ui): SwipePage: add buttons, auto-disable scrolling [no changelog] --- core/embed/rust/src/ui/component/map.rs | 14 ++- core/embed/rust/src/ui/component/mod.rs | 1 + core/embed/rust/src/ui/component/paginated.rs | 4 +- .../rust/src/ui/component/text/paragraphs.rs | 10 ++ core/embed/rust/src/ui/component/tuple.rs | 42 +++++++ core/embed/rust/src/ui/display.rs | 12 ++ .../rust/src/ui/model_tt/component/button.rs | 42 ++++++- .../rust/src/ui/model_tt/component/page.rs | 117 +++++++++++++++--- .../rust/src/ui/model_tt/component/swipe.rs | 7 ++ 9 files changed, 227 insertions(+), 22 deletions(-) diff --git a/core/embed/rust/src/ui/component/map.rs b/core/embed/rust/src/ui/component/map.rs index 8def0b394..162e091b3 100644 --- a/core/embed/rust/src/ui/component/map.rs +++ b/core/embed/rust/src/ui/component/map.rs @@ -14,15 +14,25 @@ impl Map { impl Component for Map where T: Component, - F: Fn(T::Msg) -> U, + F: Fn(T::Msg) -> Option, { type Msg = U; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - self.inner.event(ctx, event).map(&self.func) + self.inner.event(ctx, event).and_then(&self.func) } fn paint(&mut self) { self.inner.paint() } } + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Map +where + T: Component + crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.inner.trace(t) + } +} diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index 928e9817c..b1e7c1431 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -12,6 +12,7 @@ pub mod tuple; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use empty::Empty; pub use label::{Label, LabelStyle}; +pub use map::Map; pub use pad::Pad; pub use paginated::{PageMsg, Paginate}; pub use text::{ diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index 93715e077..cf74d667a 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -8,8 +8,8 @@ pub enum PageMsg { /// Pass-through from paged component. Content(T), - /// Messages from page controls outside the paged component. Currently only - /// used on T1 for "OK" and "Cancel" buttons. + /// Messages from page controls outside the paged component, like + /// "OK" and "Cancel" buttons. Controls(U), } diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index d0ab6186f..cc38fb21e 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -90,6 +90,16 @@ where } } +impl Dimensions for Paragraphs { + fn get_size(&mut self) -> Offset { + self.area.size() + } + + fn set_area(&mut self, area: Rect) { + self.area = area + } +} + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Paragraphs where diff --git a/core/embed/rust/src/ui/component/tuple.rs b/core/embed/rust/src/ui/component/tuple.rs index b2cb1db52..296408c92 100644 --- a/core/embed/rust/src/ui/component/tuple.rs +++ b/core/embed/rust/src/ui/component/tuple.rs @@ -1,4 +1,5 @@ use super::{Component, Event, EventCtx}; +use crate::ui::geometry::Rect; impl Component for (A, B) where @@ -40,3 +41,44 @@ where self.2.paint(); } } + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for (A, B) +where + A: Component + crate::trace::Trace, + B: Component + crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Tuple"); + t.field("0", &self.0); + t.field("1", &self.1); + t.close(); + } + + fn bounds(&self, sink: &dyn Fn(Rect)) { + self.0.bounds(sink); + self.1.bounds(sink); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for (A, B, C) +where + A: Component + crate::trace::Trace, + B: Component + crate::trace::Trace, + C: Component + crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Tuple"); + t.field("0", &self.0); + t.field("1", &self.1); + t.field("2", &self.2); + t.close(); + } + + fn bounds(&self, sink: &dyn Fn(Rect)) { + self.0.bounds(sink); + self.1.bounds(sink); + self.2.bounds(sink); + } +} diff --git a/core/embed/rust/src/ui/display.rs b/core/embed/rust/src/ui/display.rs index eb62a6b3f..37dc8ad2a 100644 --- a/core/embed/rust/src/ui/display.rs +++ b/core/embed/rust/src/ui/display.rs @@ -155,6 +155,18 @@ pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: ); } +pub fn text_center(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) { + let w = text_width(text, font); + display::text( + baseline.x - w / 2, + baseline.y, + text, + font.0, + fg_color.into(), + bg_color.into(), + ); +} + pub fn text_width(text: &[u8], font: Font) -> i32 { display::text_width(text, font.0) } diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index d4d59bf9d..4eecac57f 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -1,8 +1,8 @@ use super::{event::TouchEvent, theme}; use crate::ui::{ - component::{Component, Event, EventCtx}, + component::{Component, Event, EventCtx, Map}, display::{self, Color, Font}, - geometry::{Offset, Rect}, + geometry::{Grid, Offset, Rect}, }; pub enum ButtonMsg { @@ -19,6 +19,13 @@ pub struct Button { } impl Button { + /// Standard height in pixels. + pub const HEIGHT: i32 = 38; + + /// Offsets the baseline of the button text either up (negative) or down + /// (positive). + pub const BASELINE_OFFSET: i32 = -3; + pub fn new(area: Rect, content: ButtonContent) -> Self { Self { area, @@ -179,9 +186,11 @@ where match &self.content { ButtonContent::Text(text) => { let text = text.as_ref(); - let width = display::text_width(text, style.font); + let width = style.font.text_width(text); let height = style.font.text_height(); - let start_of_baseline = self.area.center() + Offset::new(-width / 2, height / 2); + let start_of_baseline = self.area.center() + + Offset::new(-width / 2, height / 2) + + Offset::y(Self::BASELINE_OFFSET); display::text( start_of_baseline, text, @@ -245,3 +254,28 @@ pub struct ButtonStyle { pub border_radius: u8, pub border_width: i32, } + +impl Button { + pub fn array2( + area: Rect, + left: impl FnOnce(Rect) -> Button, + left_map: F0, + right: impl FnOnce(Rect) -> Button, + right_map: F1, + ) -> (Map, Map) + where + F0: Fn(ButtonMsg) -> Option, + F1: Fn(ButtonMsg) -> Option, + T: AsRef<[u8]>, + { + const BUTTON_SPACING: i32 = 6; + let grid = Grid::new(area, 1, 3).with_spacing(BUTTON_SPACING); + let left = left(grid.row_col(0, 0)); + let right = right(Rect::new( + grid.row_col(0, 1).top_left(), + grid.row_col(0, 2).bottom_right(), + )); + + (Map::new(left, left_map), Map::new(right, right_map)) + } +} 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 4a862a638..1eda1bd01 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -1,35 +1,46 @@ use crate::ui::{ - component::{Component, ComponentExt, Event, EventCtx, Never, Pad, PageMsg, Paginate}, + component::{ + base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Never, Pad, Paginate, + }, display::{self, Color}, - geometry::{Offset, Point, Rect}, + geometry::{Dimensions, Offset, Point, Rect}, }; -use super::{theme, Swipe, SwipeDirection}; +use super::{theme, Button, Swipe, SwipeDirection}; -pub struct SwipePage { +pub struct SwipePage { content: T, + buttons: U, pad: Pad, swipe: Swipe, scrollbar: ScrollBar, fade: Option, } -impl SwipePage +impl SwipePage where T: Paginate, T: Component, + T: Dimensions, + U: Component, { - pub fn new(area: Rect, content: impl FnOnce(Rect) -> T, background: Color) -> Self { - // Content occupies the whole area. - let mut content = content(area); + pub fn new( + area: Rect, + background: Color, + content: impl FnOnce(Rect) -> T, + controls: impl FnOnce(Rect) -> U, + ) -> Self { + let layout = PageLayout::new(area); + let mut content = Self::make_content(&layout, content); // Always start at the first page. - let scrollbar = ScrollBar::vertical_right(area, content.page_count(), 0); + let scrollbar = ScrollBar::vertical_right(layout.scrollbar, content.page_count(), 0); let swipe = Self::make_swipe(area, &scrollbar); let pad = Pad::with_background(area, background); Self { content, + buttons: controls(layout.buttons), scrollbar, swipe, pad, @@ -44,6 +55,16 @@ where swipe } + fn make_content(layout: &PageLayout, content: impl FnOnce(Rect) -> T) -> T { + // Check if content fits on single page. + let mut content = content(layout.content_single_page); + if content.page_count() > 1 { + // Reduce area to make space for scrollbar if it doesn't fit. + content.set_area(layout.content); + } + content + } + fn change_page(&mut self, ctx: &mut EventCtx, page: usize) { // Adjust the swipe parameters. self.swipe = Self::make_swipe(self.swipe.area, &self.scrollbar); @@ -58,14 +79,26 @@ where // paint. self.fade = Some(theme::BACKLIGHT_NORMAL); } + + fn paint_hint(&mut self) { + display::text_center( + Point::new(self.pad.area.center().x, self.pad.area.bottom_right().y - 3), + b"SWIPE TO CONTINUE", + theme::FONT_BOLD, // FIXME: Figma has this as 14px but bold is 16px + theme::GREY_LIGHT, + theme::BG, + ); + } } -impl Component for SwipePage +impl Component for SwipePage where T: Paginate, T: Component, + T: Dimensions, + U: Component, { - 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) { @@ -90,13 +123,25 @@ where 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.buttons.event(ctx, event) { + return Some(PageMsg::Controls(msg)); + } + } None } fn paint(&mut self) { self.pad.paint(); self.content.paint(); - self.scrollbar.paint(); + if self.scrollbar.has_pages() { + self.scrollbar.paint(); + } + if self.scrollbar.has_next_page() { + self.paint_hint(); + } else { + self.buttons.paint(); + } if let Some(val) = self.fade.take() { // Note that this is blocking and takes some time. display::fade_backlight(val); @@ -105,9 +150,10 @@ where } #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for SwipePage +impl crate::trace::Trace for SwipePage where T: crate::trace::Trace, + U: crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("SwipePage"); @@ -116,6 +162,15 @@ where t.field("content", &self.content); t.close(); } + + fn bounds(&self, sink: &dyn Fn(Rect)) { + sink(self.scrollbar.area); + sink(self.pad.area); + self.content.bounds(sink); + if !self.scrollbar.has_next_page() { + self.buttons.bounds(sink); + } + } } pub struct ScrollBar { @@ -135,12 +190,16 @@ impl ScrollBar { pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self { Self { - area: area.cut_from_right(Self::DOT_SIZE.x), + area, page_count, active_page, } } + pub fn has_pages(&self) -> bool { + self.page_count > 1 + } + pub fn has_next_page(&self) -> bool { self.active_page < self.page_count - 1 } @@ -211,3 +270,33 @@ impl Component for ScrollBar { } } } + +pub struct PageLayout { + pub content_single_page: Rect, + pub content: Rect, + pub scrollbar: Rect, + pub buttons: Rect, +} + +impl PageLayout { + const BUTTON_SPACE: i32 = 6; + const SCROLLBAR_WIDTH: i32 = 10; + const SCROLLBAR_SPACE: i32 = 10; + + pub fn new(area: Rect) -> Self { + let (content, buttons) = area.hsplit(-Button::HEIGHT); + let (content, _space) = content.hsplit(-Self::BUTTON_SPACE); + let (buttons, _space) = buttons.vsplit(-theme::CONTENT_BORDER); + let (_space, content) = content.vsplit(theme::CONTENT_BORDER); + let (content_single_page, _space) = content.vsplit(-theme::CONTENT_BORDER); + let (content, scrollbar) = content.vsplit(-(Self::SCROLLBAR_SPACE + Self::SCROLLBAR_WIDTH)); + let (_space, scrollbar) = scrollbar.vsplit(Self::SCROLLBAR_SPACE); + + Self { + content_single_page, + content, + scrollbar, + buttons, + } + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/swipe.rs b/core/embed/rust/src/ui/model_tt/component/swipe.rs index 634178c81..3092eacaa 100644 --- a/core/embed/rust/src/ui/model_tt/component/swipe.rs +++ b/core/embed/rust/src/ui/model_tt/component/swipe.rs @@ -69,6 +69,10 @@ impl Swipe { self } + fn is_active(&self) -> bool { + self.allow_up || self.allow_down || self.allow_left || self.allow_right + } + fn ratio(&self, dist: i32) -> f32 { (dist as f32 / Self::DISTANCE as f32).min(1.0) } @@ -85,6 +89,9 @@ impl Component for Swipe { type Msg = SwipeDirection; fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option { + if !self.is_active() { + return None; + } match (event, self.origin) { (Event::Touch(TouchEvent::TouchStart(pos)), _) if self.area.contains(pos) => { // Mark the starting position of this touch.