From be3e99b96d88df805b73681c1afd70f8f71700ef Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Sun, 24 Oct 2021 11:38:56 +0200 Subject: [PATCH] feat(core): Add WiP Loader & generic Animation type --- core/embed/rust/src/trezorhal/display.rs | 35 ++++ core/embed/rust/src/ui/animation.rs | 153 ++++++++++++++++++ core/embed/rust/src/ui/display.rs | 55 +++++-- core/embed/rust/src/ui/mod.rs | 1 + .../rust/src/ui/model_tt/component/loader.rs | 139 ++++++++++++++++ .../rust/src/ui/model_tt/component/mod.rs | 1 + core/embed/rust/src/ui/model_tt/theme.rs | 17 +- 7 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 core/embed/rust/src/ui/animation.rs create mode 100644 core/embed/rust/src/ui/model_tt/component/loader.rs diff --git a/core/embed/rust/src/trezorhal/display.rs b/core/embed/rust/src/trezorhal/display.rs index 15cecef0d..33111963b 100644 --- a/core/embed/rust/src/trezorhal/display.rs +++ b/core/embed/rust/src/trezorhal/display.rs @@ -1,3 +1,5 @@ +use core::ptr; + extern "C" { // trezorhal/display.c fn display_backlight(val: cty::c_int) -> cty::c_int; @@ -42,6 +44,16 @@ extern "C" { out_h: *mut cty::uint16_t, out_grayscale: *mut bool, ) -> bool; + fn display_loader( + progress: cty::uint16_t, + indeterminate: bool, + yoffset: cty::c_int, + fgcolor: cty::uint16_t, + bgcolor: cty::uint16_t, + icon: *const cty::uint8_t, + iconlen: cty::uint32_t, + iconfgcolor: cty::uint16_t, + ); } #[cfg(not(feature = "model_tt"))] @@ -130,3 +142,26 @@ pub fn toif_info(data: &[u8]) -> Result { Err(()) } } + +pub fn loader( + progress: u16, + indeterminate: bool, + yoffset: i32, + fgcolor: u16, + bgcolor: u16, + icon: Option<&[u8]>, + iconfgcolor: u16, +) { + unsafe { + display_loader( + progress, + indeterminate, + yoffset, + fgcolor, + bgcolor, + icon.map(|i| i.as_ptr()).unwrap_or(ptr::null()), + icon.map(|i| i.len()).unwrap_or(0) as _, + iconfgcolor, + ); + } +} diff --git a/core/embed/rust/src/ui/animation.rs b/core/embed/rust/src/ui/animation.rs new file mode 100644 index 000000000..5f21404fc --- /dev/null +++ b/core/embed/rust/src/ui/animation.rs @@ -0,0 +1,153 @@ +use crate::time::{Duration, Instant}; + +/// Running, time-based linear progression of a value. +pub struct Animation { + /// Starting value. + pub from: T, + /// Ending value. + pub to: T, + /// Total duration of the animation. + pub duration: Duration, + /// Instant the animation was started on. + pub started: Instant, +} + +impl Animation { + pub fn new(from: T, to: T, duration: Duration, started: Instant) -> Self { + Self { + from, + to, + duration, + started, + } + } + + /// Time elapsed between `now` and the starting instant. + pub fn elapsed(&self, now: Instant) -> Duration { + now.saturating_duration_since(self.started) + } + + /// Value of this animation at `now` instant. + pub fn value(&self, now: Instant) -> T + where + T: Lerp, + { + let factor = self.elapsed(now) / self.duration; + T::lerp(self.from, self.to, factor) + } + + /// Seek the animation such that `value` would be the current value. + pub fn seek_to_value(&mut self, value: T) + where + T: InvLerp, + { + let factor = T::inv_lerp(self.from, self.to, value); + let offset = self.duration * factor; + self.seek_forward(offset); + } + + /// Seek the animation forward by moving the starting instant back in time. + pub fn seek_forward(&mut self, offset: Duration) { + if let Some(started) = self.started.checked_sub(offset) { + self.started = started; + } else { + // Duration is too large to be added to an `Instant`. + #[cfg(feature = "ui_debug")] + panic!("offset is too large"); + } + } +} + +/// Describes a type that can linearly interpolate (and extrapolate) based on +/// two values and a `f32` factor. +pub trait Lerp: Copy { + /// Interpolate/extrapolate between `a` and `b` and `t` as the factor. + fn lerp(a: Self, b: Self, t: f32) -> Self; + + /// Interpolate between `a` and `b` by bounding the factor `t` in the range + /// `0..=1.0`. + fn lerp_bounded(a: Self, b: Self, t: f32) -> Self + where + Self: Sized, + { + match t { + t if t < 0.0 => a, + t if t > 1.0 => b, + t => Self::lerp(a, b, t), + } + } +} + +/// Type that can compute an inverse of linear interpolation. +pub trait InvLerp: Copy { + /// Find a factor between `0.0` and `1.0` that defines the position of + /// `value` in the `min` and `max` closed interval. + fn inv_lerp(min: Self, max: Self, value: Self) -> f32; +} + +macro_rules! impl_lerp_for_int { + ($int: ident) => { + impl Lerp for $int { + fn lerp(a: Self, b: Self, t: f32) -> Self { + (a as f32 + t * (b - a) as f32) as Self + } + } + + impl InvLerp for $int { + fn inv_lerp(min: Self, max: Self, value: Self) -> f32 { + (value - min) as f32 / (max - min) as f32 + } + } + }; +} + +macro_rules! impl_lerp_for_uint { + ($uint: ident) => { + impl Lerp for $uint { + fn lerp(a: Self, b: Self, t: f32) -> Self { + if a <= b { + (a as f32 + t * (b - a) as f32) as Self + } else { + (a as f32 - t * (a - b) as f32) as Self + } + } + } + + impl InvLerp for $uint { + fn inv_lerp(min: Self, max: Self, value: Self) -> f32 { + if min <= max { + (value - min) as f32 / (max - min) as f32 + } else { + (value - max) as f32 / (min - max) as f32 + } + } + } + }; +} + +impl_lerp_for_int!(i32); +impl_lerp_for_uint!(u8); +impl_lerp_for_uint!(u16); +impl_lerp_for_uint!(u32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lerp_for_int_and_uint() { + assert_eq!(i32::lerp(0, 8, 0.5), 4); + assert_eq!(i32::lerp(0, 8, -1.0), -8); + assert_eq!(i32::lerp(8, 0, 0.5), 4); + assert_eq!(u32::lerp(0, 8, 0.5), 4); + assert_eq!(u32::lerp(8, 0, -1.0), 16); + } + + #[test] + fn inv_lerp_for_int_and_uint() { + assert!((i32::inv_lerp(0, 8, 4) - 0.5).abs() < f32::EPSILON); + assert!((i32::inv_lerp(0, 8, -8) - -1.0).abs() < f32::EPSILON); + assert!((i32::inv_lerp(8, 0, 4) - 0.5).abs() < f32::EPSILON); + assert!((u32::inv_lerp(0, 8, 4) - 0.5).abs() < f32::EPSILON); + } +} diff --git a/core/embed/rust/src/ui/display.rs b/core/embed/rust/src/ui/display.rs index 4ad89cd79..dcbb0cbcf 100644 --- a/core/embed/rust/src/ui/display.rs +++ b/core/embed/rust/src/ui/display.rs @@ -71,14 +71,14 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) { let r = Rect::from_center_and_size( center, - Offset::new(toif_info.width as _, toif_info.height as _), + Offset::new(toif_info.width.into(), toif_info.height.into()), ); display::icon( r.x0, r.y0, r.width(), r.height(), - &data[12..], // skip TOIF header + &data[12..], // Skip TOIF header. fg_color.into(), bg_color.into(), ); @@ -105,6 +105,45 @@ pub fn dotted_line(start: Point, width: i32, color: Color) { } } +pub const LOADER_MIN: u16 = 0; +pub const LOADER_MAX: u16 = 1000; + +pub fn loader( + progress: u16, + y_offset: i32, + fg_color: Color, + bg_color: Color, + icon: Option<(&[u8], Color)>, +) { + display::loader( + progress, + false, + y_offset, + fg_color.into(), + bg_color.into(), + icon.map(|i| i.0), + icon.map(|i| i.1.into()).unwrap_or(0), + ); +} + +pub fn loader_indeterminate( + progress: u16, + y_offset: i32, + fg_color: Color, + bg_color: Color, + icon: Option<(&[u8], Color)>, +) { + display::loader( + progress, + true, + y_offset, + fg_color.into(), + bg_color.into(), + icon.map(|i| i.0), + icon.map(|i| i.1.into()).unwrap_or(0), + ); +} + pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) { display::text( baseline.x, @@ -172,14 +211,6 @@ impl Color { (self.0 << 3) as u8 & 0xF8 } - pub fn blend(self, other: Self, t: f32) -> Self { - Self::rgb( - lerp(self.r(), other.r(), t), - lerp(self.g(), other.g(), t), - lerp(self.b(), other.b(), t), - ) - } - pub fn to_u16(self) -> u16 { self.0 } @@ -200,7 +231,3 @@ impl From for u16 { val.to_u16() } } - -fn lerp(a: u8, b: u8, t: f32) -> u8 { - (a as f32 + t * (b - a) as f32) as u8 -} diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index 7a9f827f0..13c3885df 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -1,6 +1,7 @@ #[macro_use] pub mod macros; +pub mod animation; pub mod component; pub mod display; pub mod geometry; diff --git a/core/embed/rust/src/ui/model_tt/component/loader.rs b/core/embed/rust/src/ui/model_tt/component/loader.rs new file mode 100644 index 000000000..26ec7dbd8 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/loader.rs @@ -0,0 +1,139 @@ +use crate::{ + time::{Duration, Instant}, + ui::{ + animation::Animation, + component::{Component, Event, EventCtx, Never}, + display::{self, Color}, + }, +}; + +use super::theme; + +enum State { + Initial, + Growing(Animation), + Shrinking(Animation), +} + +pub struct Loader { + offset_y: i32, + state: State, + growing_duration: Duration, + shrinking_duration: Duration, + styles: LoaderStyleSheet, +} + +impl Loader { + pub fn new(offset_y: i32) -> Self { + Self { + offset_y, + state: State::Initial, + growing_duration: Duration::from_millis(1000), + shrinking_duration: Duration::from_millis(500), + styles: theme::loader_default(), + } + } + + pub fn start(&mut self, now: Instant) { + let mut anim = Animation::new( + display::LOADER_MIN, + display::LOADER_MAX, + self.growing_duration, + now, + ); + if let State::Shrinking(shrinking) = &self.state { + anim.seek_to_value(shrinking.value(now)); + } + self.state = State::Growing(anim); + } + + pub fn stop(&mut self, now: Instant) { + let mut anim = Animation::new( + display::LOADER_MAX, + display::LOADER_MIN, + self.shrinking_duration, + now, + ); + if let State::Growing(growing) = &self.state { + anim.seek_to_value(growing.value(now)); + } + self.state = State::Shrinking(anim); + } + + pub fn reset(&mut self) { + self.state = State::Initial; + } + + pub fn progress(&self, now: Instant) -> Option { + match &self.state { + State::Initial => None, + State::Growing(animation) | State::Shrinking(animation) => Some(animation.value(now)), + } + } + + pub fn is_finished(&self, now: Instant) -> bool { + self.progress(now) == Some(display::LOADER_MAX) + } +} + +impl Component for Loader { + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + if let Some(progress) = self.progress(Instant::now()) { + let style = if progress < display::LOADER_MAX { + self.styles.normal + } else { + self.styles.active + }; + display::loader( + progress, + self.offset_y, + style.loader_color, + style.background_color, + style.icon, + ); + } + } +} + +pub struct LoaderStyleSheet { + pub normal: &'static LoaderStyle, + pub active: &'static LoaderStyle, +} + +pub struct LoaderStyle { + pub icon: Option<(&'static [u8], Color)>, + pub loader_color: Color, + pub background_color: Color, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn loader_yields_expected_progress() { + let mut l = Loader::new(0); + let t = Instant::now(); + assert_eq!(l.progress(t), None); + l.start(t); + assert_eq!(l.progress(t), Some(0)); + let t = add_millis(t, 500); + assert_eq!(l.progress(t), Some(500)); + l.stop(t); + assert_eq!(l.progress(t), Some(500)); + let t = add_millis(t, 125); + assert_eq!(l.progress(t), Some(250)); + let t = add_millis(t, 125); + assert_eq!(l.progress(t), Some(0)); + } + + fn add_millis(inst: Instant, millis: u32) -> Instant { + inst.checked_add(Duration::from_millis(millis)).unwrap() + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index b9e0eccd6..35310adc9 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -1,5 +1,6 @@ mod button; mod dialog; +mod loader; mod page; mod passphrase; mod pin; diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 877beccbc..21ab0a4d0 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -3,7 +3,7 @@ use crate::ui::{ display::{Color, Font}, }; -use super::component::{ButtonStyle, ButtonStyleSheet}; +use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet}; // Font constants. pub const FONT_NORMAL: Font = Font::new(-1); @@ -92,6 +92,21 @@ pub fn button_clear() -> ButtonStyleSheet { button_default() } +pub fn loader_default() -> LoaderStyleSheet { + LoaderStyleSheet { + normal: &LoaderStyle { + icon: None, + loader_color: FG, + background_color: BG, + }, + active: &LoaderStyle { + icon: None, + loader_color: GREEN, + background_color: BG, + }, + } +} + pub struct TTDefaultText; impl DefaultTextTheme for TTDefaultText {