mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-09 06:02:40 +00:00
feat(core): Add WiP Loader & generic Animation type
This commit is contained in:
parent
87a7e94f5c
commit
be3e99b96d
@ -1,3 +1,5 @@
|
|||||||
|
use core::ptr;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
// trezorhal/display.c
|
// trezorhal/display.c
|
||||||
fn display_backlight(val: cty::c_int) -> cty::c_int;
|
fn display_backlight(val: cty::c_int) -> cty::c_int;
|
||||||
@ -42,6 +44,16 @@ extern "C" {
|
|||||||
out_h: *mut cty::uint16_t,
|
out_h: *mut cty::uint16_t,
|
||||||
out_grayscale: *mut bool,
|
out_grayscale: *mut bool,
|
||||||
) -> 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"))]
|
#[cfg(not(feature = "model_tt"))]
|
||||||
@ -130,3 +142,26 @@ pub fn toif_info(data: &[u8]) -> Result<ToifInfo, ()> {
|
|||||||
Err(())
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
153
core/embed/rust/src/ui/animation.rs
Normal file
153
core/embed/rust/src/ui/animation.rs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
use crate::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Running, time-based linear progression of a value.
|
||||||
|
pub struct Animation<T> {
|
||||||
|
/// 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<T> Animation<T> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -71,14 +71,14 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
|
|||||||
|
|
||||||
let r = Rect::from_center_and_size(
|
let r = Rect::from_center_and_size(
|
||||||
center,
|
center,
|
||||||
Offset::new(toif_info.width as _, toif_info.height as _),
|
Offset::new(toif_info.width.into(), toif_info.height.into()),
|
||||||
);
|
);
|
||||||
display::icon(
|
display::icon(
|
||||||
r.x0,
|
r.x0,
|
||||||
r.y0,
|
r.y0,
|
||||||
r.width(),
|
r.width(),
|
||||||
r.height(),
|
r.height(),
|
||||||
&data[12..], // skip TOIF header
|
&data[12..], // Skip TOIF header.
|
||||||
fg_color.into(),
|
fg_color.into(),
|
||||||
bg_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) {
|
pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) {
|
||||||
display::text(
|
display::text(
|
||||||
baseline.x,
|
baseline.x,
|
||||||
@ -172,14 +211,6 @@ impl Color {
|
|||||||
(self.0 << 3) as u8 & 0xF8
|
(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 {
|
pub fn to_u16(self) -> u16 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
@ -200,7 +231,3 @@ impl From<Color> for u16 {
|
|||||||
val.to_u16()
|
val.to_u16()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lerp(a: u8, b: u8, t: f32) -> u8 {
|
|
||||||
(a as f32 + t * (b - a) as f32) as u8
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
|
||||||
|
pub mod animation;
|
||||||
pub mod component;
|
pub mod component;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
|
139
core/embed/rust/src/ui/model_tt/component/loader.rs
Normal file
139
core/embed/rust/src/ui/model_tt/component/loader.rs
Normal file
@ -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<u16>),
|
||||||
|
Shrinking(Animation<u16>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u16> {
|
||||||
|
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<Self::Msg> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
mod button;
|
mod button;
|
||||||
mod dialog;
|
mod dialog;
|
||||||
|
mod loader;
|
||||||
mod page;
|
mod page;
|
||||||
mod passphrase;
|
mod passphrase;
|
||||||
mod pin;
|
mod pin;
|
||||||
|
@ -3,7 +3,7 @@ use crate::ui::{
|
|||||||
display::{Color, Font},
|
display::{Color, Font},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::component::{ButtonStyle, ButtonStyleSheet};
|
use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet};
|
||||||
|
|
||||||
// Font constants.
|
// Font constants.
|
||||||
pub const FONT_NORMAL: Font = Font::new(-1);
|
pub const FONT_NORMAL: Font = Font::new(-1);
|
||||||
@ -92,6 +92,21 @@ pub fn button_clear() -> ButtonStyleSheet {
|
|||||||
button_default()
|
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;
|
pub struct TTDefaultText;
|
||||||
|
|
||||||
impl DefaultTextTheme for TTDefaultText {
|
impl DefaultTextTheme for TTDefaultText {
|
||||||
|
Loading…
Reference in New Issue
Block a user