mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-18 10:32:02 +00:00
feat(core): add hold-to-confirm animation to mercury UI
[no changelog]
This commit is contained in:
parent
1f01150e7c
commit
67fe334dc2
@ -67,18 +67,6 @@ impl Div<Duration> for Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<f32> for Duration {
|
|
||||||
fn from(value: f32) -> Self {
|
|
||||||
Self::from_millis((value * 1000.0) as u32)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Duration> for f32 {
|
|
||||||
fn from(value: Duration) -> Self {
|
|
||||||
value.to_millis() as f32 / 1000.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Instants can wrap around and we want them to be comparable even after
|
/* Instants can wrap around and we want them to be comparable even after
|
||||||
* wrapping around. This works by setting a maximum allowable difference
|
* wrapping around. This works by setting a maximum allowable difference
|
||||||
* between two Instants to half the range. In checked_add and checked_sub, we
|
* between two Instants to half the range. In checked_add and checked_sub, we
|
||||||
@ -163,6 +151,7 @@ impl Ord for Instant {
|
|||||||
/// A stopwatch is a utility designed for measuring the amount of time
|
/// A stopwatch is a utility designed for measuring the amount of time
|
||||||
/// that elapses between its start and stop points. It can be used in various
|
/// that elapses between its start and stop points. It can be used in various
|
||||||
/// situations - animation timing, event timing, testing and debugging.
|
/// situations - animation timing, event timing, testing and debugging.
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum Stopwatch {
|
pub enum Stopwatch {
|
||||||
Stopped(Duration),
|
Stopped(Duration),
|
||||||
Running(Instant),
|
Running(Instant),
|
||||||
@ -214,7 +203,7 @@ impl Stopwatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the stopwatch is currenly running.
|
/// Returns `true` if the stopwatch is currently running.
|
||||||
pub fn is_running(&self) -> bool {
|
pub fn is_running(&self) -> bool {
|
||||||
matches!(*self, Self::Running(_))
|
matches!(*self, Self::Running(_))
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,11 @@ impl<'a> Label<'a> {
|
|||||||
};
|
};
|
||||||
Rect::from_bottom_left_and_size(baseline, Offset::new(width, height))
|
Rect::from_bottom_left_and_size(baseline, Offset::new(width, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
|
||||||
|
self.text
|
||||||
|
.map(|c| self.layout.render_text_with_alpha(c, target, alpha));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Label<'_> {
|
impl Component for Label<'_> {
|
||||||
|
@ -239,10 +239,19 @@ impl TextLayout {
|
|||||||
|
|
||||||
/// Draw as much text as possible on the current screen.
|
/// Draw as much text as possible on the current screen.
|
||||||
pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit {
|
pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit {
|
||||||
|
self.render_text_with_alpha(text, target, 255)
|
||||||
|
}
|
||||||
|
/// Draw as much text as possible on the current screen.
|
||||||
|
pub fn render_text_with_alpha<'s>(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
alpha: u8,
|
||||||
|
) -> LayoutFit {
|
||||||
self.layout_text(
|
self.layout_text(
|
||||||
text,
|
text,
|
||||||
&mut self.initial_cursor(),
|
&mut self.initial_cursor(),
|
||||||
&mut TextRenderer2::new(target),
|
&mut TextRenderer2::new(target).with_alpha(alpha),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,16 +550,29 @@ impl LayoutSink for TextRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextRenderer2<'a, 's, R>(pub &'a mut R, core::marker::PhantomData<&'s ()>)
|
pub struct TextRenderer2<'a, 's, R>
|
||||||
where
|
where
|
||||||
R: Renderer<'s>;
|
R: Renderer<'s>,
|
||||||
|
{
|
||||||
|
pub renderer: &'a mut R,
|
||||||
|
pd: core::marker::PhantomData<&'s ()>,
|
||||||
|
alpha: u8,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, 's, R> TextRenderer2<'a, 's, R>
|
impl<'a, 's, R> TextRenderer2<'a, 's, R>
|
||||||
where
|
where
|
||||||
R: Renderer<'s>,
|
R: Renderer<'s>,
|
||||||
{
|
{
|
||||||
pub fn new(target: &'a mut R) -> Self {
|
pub fn new(target: &'a mut R) -> Self {
|
||||||
Self(target, core::marker::PhantomData)
|
Self {
|
||||||
|
renderer: target,
|
||||||
|
pd: core::marker::PhantomData,
|
||||||
|
alpha: 255,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_alpha(self, alpha: u8) -> Self {
|
||||||
|
Self { alpha, ..self }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,14 +584,16 @@ where
|
|||||||
shape::Text::new(cursor, text)
|
shape::Text::new(cursor, text)
|
||||||
.with_font(layout.style.text_font)
|
.with_font(layout.style.text_font)
|
||||||
.with_fg(layout.style.text_color)
|
.with_fg(layout.style.text_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||||
shape::Text::new(cursor, "-")
|
shape::Text::new(cursor, "-")
|
||||||
.with_font(layout.style.text_font)
|
.with_font(layout.style.text_font)
|
||||||
.with_fg(layout.style.hyphen_color)
|
.with_fg(layout.style.hyphen_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||||
@ -578,12 +602,14 @@ where
|
|||||||
shape::ToifImage::new(bottom_left, icon.toif)
|
shape::ToifImage::new(bottom_left, icon.toif)
|
||||||
.with_align(Alignment2D::BOTTOM_LEFT)
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
.with_fg(layout.style.ellipsis_color)
|
.with_fg(layout.style.ellipsis_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
} else {
|
} else {
|
||||||
shape::Text::new(cursor, ELLIPSIS)
|
shape::Text::new(cursor, ELLIPSIS)
|
||||||
.with_font(layout.style.text_font)
|
.with_font(layout.style.text_font)
|
||||||
.with_fg(layout.style.ellipsis_color)
|
.with_fg(layout.style.ellipsis_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,12 +618,14 @@ where
|
|||||||
shape::ToifImage::new(cursor, icon.toif)
|
shape::ToifImage::new(cursor, icon.toif)
|
||||||
.with_align(Alignment2D::BOTTOM_LEFT)
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
.with_fg(layout.style.ellipsis_color)
|
.with_fg(layout.style.ellipsis_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
} else {
|
} else {
|
||||||
shape::Text::new(cursor, ELLIPSIS)
|
shape::Text::new(cursor, ELLIPSIS)
|
||||||
.with_font(layout.style.text_font)
|
.with_font(layout.style.text_font)
|
||||||
.with_fg(layout.style.ellipsis_color)
|
.with_fg(layout.style.ellipsis_color)
|
||||||
.render(self.0);
|
.with_alpha(self.alpha)
|
||||||
|
.render(self.renderer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ pub struct Button {
|
|||||||
state: State,
|
state: State,
|
||||||
long_press: Option<Duration>,
|
long_press: Option<Duration>,
|
||||||
long_timer: Option<TimerToken>,
|
long_timer: Option<TimerToken>,
|
||||||
|
haptic: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
@ -54,6 +55,7 @@ impl Button {
|
|||||||
state: State::Initial,
|
state: State::Initial,
|
||||||
long_press: None,
|
long_press: None,
|
||||||
long_timer: None,
|
long_timer: None,
|
||||||
|
haptic: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +104,11 @@ impl Button {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn without_haptics(mut self) -> Self {
|
||||||
|
self.haptic = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
|
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
|
||||||
if enabled {
|
if enabled {
|
||||||
self.enable(ctx);
|
self.enable(ctx);
|
||||||
@ -182,7 +189,12 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
pub fn render_background<'s>(
|
||||||
|
&self,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
alpha: u8,
|
||||||
|
) {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ButtonContent::IconBlend(_, _, _) => {}
|
ButtonContent::IconBlend(_, _, _) => {}
|
||||||
_ => {
|
_ => {
|
||||||
@ -192,11 +204,13 @@ impl Button {
|
|||||||
.with_radius(self.radius.unwrap() as i16)
|
.with_radius(self.radius.unwrap() as i16)
|
||||||
.with_thickness(2)
|
.with_thickness(2)
|
||||||
.with_fg(style.button_color)
|
.with_fg(style.button_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
} else {
|
} else {
|
||||||
shape::Bar::new(self.area)
|
shape::Bar::new(self.area)
|
||||||
.with_bg(style.button_color)
|
.with_bg(style.button_color)
|
||||||
.with_fg(style.button_color)
|
.with_fg(style.button_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,7 +252,12 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
pub fn render_content<'s>(
|
||||||
|
&self,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
alpha: u8,
|
||||||
|
) {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ButtonContent::Empty => {}
|
ButtonContent::Empty => {}
|
||||||
ButtonContent::Text(text) => {
|
ButtonContent::Text(text) => {
|
||||||
@ -255,6 +274,7 @@ impl Button {
|
|||||||
.with_font(style.font)
|
.with_font(style.font)
|
||||||
.with_fg(style.text_color)
|
.with_fg(style.text_color)
|
||||||
.with_align(self.text_align)
|
.with_align(self.text_align)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -262,24 +282,40 @@ impl Button {
|
|||||||
shape::ToifImage::new(self.area.center(), icon.toif)
|
shape::ToifImage::new(self.area.center(), icon.toif)
|
||||||
.with_align(Alignment2D::CENTER)
|
.with_align(Alignment2D::CENTER)
|
||||||
.with_fg(style.icon_color)
|
.with_fg(style.icon_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
ButtonContent::IconAndText(child) => {
|
ButtonContent::IconAndText(child) => {
|
||||||
child.render(target, self.area, self.style(), Self::BASELINE_OFFSET);
|
child.render(
|
||||||
|
target,
|
||||||
|
self.area,
|
||||||
|
self.style(),
|
||||||
|
Self::BASELINE_OFFSET,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ButtonContent::IconBlend(bg, fg, offset) => {
|
ButtonContent::IconBlend(bg, fg, offset) => {
|
||||||
shape::Bar::new(self.area)
|
shape::Bar::new(self.area)
|
||||||
.with_bg(style.background_color)
|
.with_bg(style.background_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
shape::ToifImage::new(self.area.top_left(), bg.toif)
|
shape::ToifImage::new(self.area.top_left(), bg.toif)
|
||||||
.with_fg(style.button_color)
|
.with_fg(style.button_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
|
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
|
||||||
.with_fg(style.icon_color)
|
.with_fg(style.icon_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
|
||||||
|
let style = self.style();
|
||||||
|
self.render_background(target, style, alpha);
|
||||||
|
self.render_content(target, style, alpha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Button {
|
impl Component for Button {
|
||||||
@ -307,7 +343,9 @@ impl Component for Button {
|
|||||||
// Touch started in our area, transform to `Pressed` state.
|
// Touch started in our area, transform to `Pressed` state.
|
||||||
if touch_area.contains(pos) {
|
if touch_area.contains(pos) {
|
||||||
#[cfg(feature = "haptic")]
|
#[cfg(feature = "haptic")]
|
||||||
play(HapticEffect::ButtonPress);
|
if self.haptic {
|
||||||
|
play(HapticEffect::ButtonPress);
|
||||||
|
}
|
||||||
self.set(ctx, State::Pressed);
|
self.set(ctx, State::Pressed);
|
||||||
if let Some(duration) = self.long_press {
|
if let Some(duration) = self.long_press {
|
||||||
self.long_timer = Some(ctx.request_timer(duration));
|
self.long_timer = Some(ctx.request_timer(duration));
|
||||||
@ -351,7 +389,9 @@ impl Component for Button {
|
|||||||
self.long_timer = None;
|
self.long_timer = None;
|
||||||
if matches!(self.state, State::Pressed) {
|
if matches!(self.state, State::Pressed) {
|
||||||
#[cfg(feature = "haptic")]
|
#[cfg(feature = "haptic")]
|
||||||
play(HapticEffect::ButtonPress);
|
if self.haptic {
|
||||||
|
play(HapticEffect::ButtonPress);
|
||||||
|
}
|
||||||
self.set(ctx, State::Initial);
|
self.set(ctx, State::Initial);
|
||||||
return Some(ButtonMsg::LongPressed);
|
return Some(ButtonMsg::LongPressed);
|
||||||
}
|
}
|
||||||
@ -370,8 +410,8 @@ impl Component for Button {
|
|||||||
|
|
||||||
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||||
let style = self.style();
|
let style = self.style();
|
||||||
self.render_background(target, style);
|
self.render_background(target, style, 0xFF);
|
||||||
self.render_content(target, style);
|
self.render_content(target, style, 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
@ -605,6 +645,7 @@ impl IconText {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
style: &ButtonStyle,
|
style: &ButtonStyle,
|
||||||
baseline_offset: Offset,
|
baseline_offset: Offset,
|
||||||
|
alpha: u8,
|
||||||
) {
|
) {
|
||||||
let width = self.text.map(|t| style.font.text_width(t));
|
let width = self.text.map(|t| style.font.text_width(t));
|
||||||
|
|
||||||
@ -635,6 +676,7 @@ impl IconText {
|
|||||||
shape::Text::new(text_pos, t)
|
shape::Text::new(text_pos, t)
|
||||||
.with_font(style.font)
|
.with_font(style.font)
|
||||||
.with_fg(style.text_color)
|
.with_fg(style.text_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target)
|
.render(target)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -643,6 +685,7 @@ impl IconText {
|
|||||||
shape::ToifImage::new(icon_pos, self.icon.toif)
|
shape::ToifImage::new(icon_pos, self.icon.toif)
|
||||||
.with_align(Alignment2D::CENTER)
|
.with_align(Alignment2D::CENTER)
|
||||||
.with_fg(style.icon_color)
|
.with_fg(style.icon_color)
|
||||||
|
.with_alpha(alpha)
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,13 @@ use crate::{
|
|||||||
},
|
},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::{Alignment, Insets, Rect},
|
geometry::{Alignment, Insets, Rect},
|
||||||
|
model_mercury::theme::TITLE_HEIGHT,
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{theme, Button, ButtonMsg, ButtonStyleSheet, CancelInfoConfirmMsg, Footer};
|
use super::{theme, Button, ButtonMsg, ButtonStyleSheet, CancelInfoConfirmMsg, Footer};
|
||||||
|
|
||||||
const TITLE_HEIGHT: i16 = 42;
|
|
||||||
const BUTTON_EXPAND_BORDER: i16 = 32;
|
const BUTTON_EXPAND_BORDER: i16 = 32;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -24,6 +24,7 @@ pub struct Frame<T> {
|
|||||||
button_msg: CancelInfoConfirmMsg,
|
button_msg: CancelInfoConfirmMsg,
|
||||||
content: Child<T>,
|
content: Child<T>,
|
||||||
footer: Option<Footer<'static>>,
|
footer: Option<Footer<'static>>,
|
||||||
|
overlapping_content: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum FrameMsg<T> {
|
pub enum FrameMsg<T> {
|
||||||
@ -46,6 +47,7 @@ where
|
|||||||
button_msg: CancelInfoConfirmMsg::Cancelled,
|
button_msg: CancelInfoConfirmMsg::Cancelled,
|
||||||
content: Child::new(content),
|
content: Child::new(content),
|
||||||
footer: None,
|
footer: None,
|
||||||
|
overlapping_content: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +190,11 @@ where
|
|||||||
footer.place(footer_area);
|
footer.place(footer_area);
|
||||||
content_area = remaining;
|
content_area = remaining;
|
||||||
}
|
}
|
||||||
self.content.place(content_area);
|
if self.overlapping_content {
|
||||||
|
self.content.place(bounds);
|
||||||
|
} else {
|
||||||
|
self.content.place(content_area);
|
||||||
|
}
|
||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,15 +215,15 @@ where
|
|||||||
self.title.paint();
|
self.title.paint();
|
||||||
self.subtitle.paint();
|
self.subtitle.paint();
|
||||||
self.button.paint();
|
self.button.paint();
|
||||||
self.content.paint();
|
|
||||||
self.footer.paint();
|
self.footer.paint();
|
||||||
|
self.content.paint();
|
||||||
}
|
}
|
||||||
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||||
self.title.render(target);
|
self.title.render(target);
|
||||||
self.subtitle.render(target);
|
self.subtitle.render(target);
|
||||||
self.button.render(target);
|
self.button.render(target);
|
||||||
self.content.render(target);
|
|
||||||
self.footer.render(target);
|
self.footer.render(target);
|
||||||
|
self.content.render(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
@ -225,8 +231,8 @@ where
|
|||||||
self.title.bounds(sink);
|
self.title.bounds(sink);
|
||||||
self.subtitle.bounds(sink);
|
self.subtitle.bounds(sink);
|
||||||
self.button.bounds(sink);
|
self.button.bounds(sink);
|
||||||
self.content.bounds(sink);
|
|
||||||
self.footer.bounds(sink);
|
self.footer.bounds(sink);
|
||||||
|
self.content.bounds(sink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,331 @@
|
|||||||
|
use crate::{
|
||||||
|
time::Duration,
|
||||||
|
translations::TR,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx},
|
||||||
|
display::Color,
|
||||||
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
lerp::Lerp,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{theme, Button, ButtonContent, ButtonMsg};
|
||||||
|
|
||||||
|
#[cfg(feature = "haptic")]
|
||||||
|
use crate::trezorhal::haptic::{self, HapticEffect};
|
||||||
|
use crate::{
|
||||||
|
time::Stopwatch,
|
||||||
|
ui::{
|
||||||
|
component::Label,
|
||||||
|
constant::screen,
|
||||||
|
geometry::{Alignment, Point},
|
||||||
|
model_mercury::theme::TITLE_HEIGHT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use pareen;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct HoldToConfirmAnim {
|
||||||
|
pub timer: Stopwatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HoldToConfirmAnim {
|
||||||
|
const DURATION_MS: u32 = 2200;
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.timer
|
||||||
|
.is_running_within(Duration::from_millis(Self::DURATION_MS))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval(&self) -> f32 {
|
||||||
|
self.timer.elapsed().to_millis() as f32 / 1000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_parent_cover_opacity(&self, t: f32) -> u8 {
|
||||||
|
let parent_cover_opacity = pareen::constant(0.0).seq_ease_in_out(
|
||||||
|
0.0,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.2,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
u8::lerp(0, 255, parent_cover_opacity.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_header_opacity(&self, t: f32) -> u8 {
|
||||||
|
let header_opacity = pareen::constant(0.0).seq_ease_out(
|
||||||
|
0.1,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.3,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
u8::lerp(0, 255, header_opacity.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_header_opacity2(&self, t: f32) -> u8 {
|
||||||
|
let header_opacity2 = pareen::constant(1.0).seq_ease_in(
|
||||||
|
2.0,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.2,
|
||||||
|
pareen::constant(0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
u8::lerp(0, 255, header_opacity2.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_circle_opacity(&self, t: f32) -> u8 {
|
||||||
|
let circle_opacity = pareen::constant(1.0).seq_ease_out(
|
||||||
|
0.1,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.1,
|
||||||
|
pareen::constant(0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
u8::lerp(0, 255, circle_opacity.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pad_color(&self, t: f32) -> Color {
|
||||||
|
let pad_color = pareen::constant(0.0).seq_ease_in_out(
|
||||||
|
0.1,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
1.9,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
Color::lerp(theme::GREY_EXTRA_DARK, theme::GREEN, pad_color.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_circle_max_height(&self, t: f32) -> i16 {
|
||||||
|
let circle_max_height = pareen::constant(0.0).seq_ease_in(
|
||||||
|
0.1,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
1.5,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
i16::lerp(266, 0, circle_max_height.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_circle_radius(&self, t: f32) -> i16 {
|
||||||
|
let circle_radius = pareen::constant(0.0).seq_ease_in(
|
||||||
|
1.6,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.6,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
i16::lerp(0, 100, circle_radius.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_haptic(&self, t: f32) -> u8 {
|
||||||
|
let haptic = pareen::constant(0.0).seq_ease_in(
|
||||||
|
0.0,
|
||||||
|
easer::functions::Linear,
|
||||||
|
Self::DURATION_MS as f32 / 1000.0,
|
||||||
|
pareen::constant(1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
u8::lerp(0, 20, haptic.eval(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.timer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.timer = Stopwatch::new_stopped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component requesting a hold to confirm action from a user. Most typically
|
||||||
|
/// embedded as a content of a Frame.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HoldToConfirm {
|
||||||
|
title: Label<'static>,
|
||||||
|
area: Rect,
|
||||||
|
button: Button,
|
||||||
|
circle_color: Color,
|
||||||
|
circle_pad_color: Color,
|
||||||
|
circle_inner_color: Color,
|
||||||
|
anim: HoldToConfirmAnim,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum DismissType {
|
||||||
|
Tap,
|
||||||
|
Hold,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HoldToConfirm {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let button = Button::new(ButtonContent::Empty)
|
||||||
|
.styled(theme::button_default())
|
||||||
|
.with_long_press(Duration::from_millis(2200))
|
||||||
|
.without_haptics();
|
||||||
|
Self {
|
||||||
|
title: Label::new(
|
||||||
|
TR::instructions__continue_holding.into(),
|
||||||
|
Alignment::Start,
|
||||||
|
theme::label_title_main(),
|
||||||
|
)
|
||||||
|
.vertically_centered(),
|
||||||
|
area: Rect::zero(),
|
||||||
|
circle_color: theme::GREEN,
|
||||||
|
circle_pad_color: theme::GREY_EXTRA_DARK,
|
||||||
|
circle_inner_color: theme::GREEN_LIGHT,
|
||||||
|
button,
|
||||||
|
anim: HoldToConfirmAnim::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for HoldToConfirm {
|
||||||
|
type Msg = ();
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
self.button.place(Rect::snap(
|
||||||
|
self.area.center(),
|
||||||
|
Offset::uniform(80),
|
||||||
|
Alignment2D::CENTER,
|
||||||
|
));
|
||||||
|
self.title.place(screen().split_top(TITLE_HEIGHT).0);
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
let btn_msg = self.button.event(ctx, event);
|
||||||
|
match btn_msg {
|
||||||
|
Some(ButtonMsg::Pressed) => {
|
||||||
|
self.anim.start();
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Some(ButtonMsg::Released) => {
|
||||||
|
self.anim.reset();
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Some(ButtonMsg::Clicked) => {
|
||||||
|
self.anim.reset();
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Some(ButtonMsg::LongPressed) => {
|
||||||
|
#[cfg(feature = "haptic")]
|
||||||
|
haptic::play(HapticEffect::HoldToConfirm);
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
if self.anim.is_active() {
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||||
|
let elapsed = self.anim.eval();
|
||||||
|
|
||||||
|
shape::Bar::new(screen())
|
||||||
|
.with_fg(theme::BLACK)
|
||||||
|
.with_bg(theme::BLACK)
|
||||||
|
.with_alpha(self.anim.get_parent_cover_opacity(elapsed))
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let center = self.area.center();
|
||||||
|
|
||||||
|
const PROGRESS_CIRCLE_RADIUS: i16 = 88;
|
||||||
|
const PAD_RADIUS: i16 = 70;
|
||||||
|
const CIRCLE_RADIUS: i16 = 50;
|
||||||
|
const INNER_CIRCLE_RADIUS: i16 = 40;
|
||||||
|
const CIRCLE_THICKNESS: i16 = 2;
|
||||||
|
|
||||||
|
if self.anim.get_parent_cover_opacity(elapsed) == 255 {
|
||||||
|
shape::Circle::new(center, PROGRESS_CIRCLE_RADIUS)
|
||||||
|
.with_fg(self.circle_inner_color)
|
||||||
|
.with_bg(theme::BLACK)
|
||||||
|
.with_thickness(CIRCLE_THICKNESS)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Bar::new(Rect::new(
|
||||||
|
Point::zero(),
|
||||||
|
Point::new(screen().width(), self.anim.get_circle_max_height(elapsed)),
|
||||||
|
))
|
||||||
|
.with_fg(theme::BLACK)
|
||||||
|
.with_bg(theme::BLACK)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title_alpha = if self.anim.get_header_opacity2(elapsed) < 255 {
|
||||||
|
self.anim.get_header_opacity2(elapsed)
|
||||||
|
} else {
|
||||||
|
self.anim.get_header_opacity(elapsed)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.title.render_with_alpha(target, title_alpha);
|
||||||
|
|
||||||
|
let pad_color = self.anim.get_pad_color(elapsed);
|
||||||
|
let circle_alpha = self.anim.get_circle_opacity(elapsed);
|
||||||
|
|
||||||
|
shape::Circle::new(center, PAD_RADIUS)
|
||||||
|
.with_fg(pad_color)
|
||||||
|
.with_bg(pad_color)
|
||||||
|
.render(target);
|
||||||
|
shape::Circle::new(center, CIRCLE_RADIUS)
|
||||||
|
.with_fg(self.circle_color)
|
||||||
|
.with_bg(self.circle_pad_color)
|
||||||
|
.with_thickness(CIRCLE_THICKNESS)
|
||||||
|
.with_alpha(circle_alpha)
|
||||||
|
.render(target);
|
||||||
|
shape::Circle::new(center, CIRCLE_RADIUS - CIRCLE_THICKNESS)
|
||||||
|
.with_fg(self.circle_pad_color)
|
||||||
|
.with_bg(self.circle_pad_color)
|
||||||
|
.with_thickness(CIRCLE_RADIUS - CIRCLE_THICKNESS - INNER_CIRCLE_RADIUS)
|
||||||
|
.with_alpha(circle_alpha)
|
||||||
|
.render(target);
|
||||||
|
shape::Circle::new(center, INNER_CIRCLE_RADIUS)
|
||||||
|
.with_fg(self.circle_inner_color)
|
||||||
|
.with_bg(theme::BLACK)
|
||||||
|
.with_thickness(CIRCLE_THICKNESS)
|
||||||
|
.with_alpha(circle_alpha)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(center, theme::ICON_SIGN.toif)
|
||||||
|
.with_fg(theme::GREY)
|
||||||
|
.with_alpha(circle_alpha)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, self.anim.get_circle_radius(elapsed))
|
||||||
|
.with_fg(theme::BLACK)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
#[cfg(feature = "haptic")]
|
||||||
|
{
|
||||||
|
let hap = self.anim.get_haptic(elapsed);
|
||||||
|
if hap > 0 {
|
||||||
|
haptic::play_custom(hap as i8, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "micropython")]
|
||||||
|
impl crate::ui::flow::Swipable<()> for HoldToConfirm {}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for HoldToConfirm {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("StatusScreen");
|
||||||
|
t.child("button", &self.button);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,9 @@ mod vertical_menu;
|
|||||||
mod fido_icons;
|
mod fido_icons;
|
||||||
mod error;
|
mod error;
|
||||||
mod frame;
|
mod frame;
|
||||||
|
|
||||||
|
#[cfg(feature = "translations")]
|
||||||
|
mod hold_to_confirm;
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
mod homescreen;
|
mod homescreen;
|
||||||
#[cfg(feature = "translations")]
|
#[cfg(feature = "translations")]
|
||||||
@ -48,6 +51,8 @@ pub use error::ErrorScreen;
|
|||||||
pub use fido::{FidoConfirm, FidoMsg};
|
pub use fido::{FidoConfirm, FidoMsg};
|
||||||
pub use footer::Footer;
|
pub use footer::Footer;
|
||||||
pub use frame::{Frame, FrameMsg};
|
pub use frame::{Frame, FrameMsg};
|
||||||
|
#[cfg(feature = "translations")]
|
||||||
|
pub use hold_to_confirm::HoldToConfirm;
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg, Lockscreen};
|
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg, Lockscreen};
|
||||||
#[cfg(feature = "translations")]
|
#[cfg(feature = "translations")]
|
||||||
|
@ -133,7 +133,8 @@ impl Component for PromptScreen {
|
|||||||
.with_thickness(2)
|
.with_thickness(2)
|
||||||
.render(target);
|
.render(target);
|
||||||
});
|
});
|
||||||
self.button.render_content(target, self.button.style());
|
self.button
|
||||||
|
.render_content(target, self.button.style(), 0xff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
error,
|
error,
|
||||||
|
micropython::{map::Map, obj::Obj, util},
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
translations::TR,
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
@ -9,11 +10,12 @@ use crate::{
|
|||||||
ButtonRequestExt, ComponentExt, SwipeDirection,
|
ButtonRequestExt, ComponentExt, SwipeDirection,
|
||||||
},
|
},
|
||||||
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
|
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
|
||||||
|
layout::obj::LayoutObj,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::{
|
use super::super::{
|
||||||
component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg},
|
component::{Frame, FrameMsg, HoldToConfirm, VerticalMenu, VerticalMenuChoiceMsg},
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,11 +62,6 @@ impl FlowState for ConfirmResetCreate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{
|
|
||||||
micropython::{map::Map, obj::Obj, util},
|
|
||||||
ui::layout::obj::LayoutObj,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
pub extern "C" fn new_confirm_reset_create(
|
pub extern "C" fn new_confirm_reset_create(
|
||||||
n_args: usize,
|
n_args: usize,
|
||||||
@ -100,16 +97,16 @@ impl ConfirmResetCreate {
|
|||||||
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
|
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
|
||||||
});
|
});
|
||||||
|
|
||||||
let content_confirm = Frame::left_aligned(
|
let content_confirm =
|
||||||
TR::reset__title_create_wallet.into(),
|
Frame::left_aligned(TR::reset__title_create_wallet.into(), HoldToConfirm::new())
|
||||||
PromptScreen::new_hold_to_confirm(),
|
.with_footer(TR::instructions__hold_to_confirm.into(), None)
|
||||||
)
|
.map(|msg| match msg {
|
||||||
.with_footer(TR::instructions__hold_to_confirm.into(), None)
|
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
||||||
.map(|msg| match msg {
|
_ => Some(FlowMsg::Cancelled),
|
||||||
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
})
|
||||||
_ => Some(FlowMsg::Cancelled),
|
.one_button_request(
|
||||||
})
|
ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"),
|
||||||
.one_button_request(ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"));
|
);
|
||||||
|
|
||||||
let store = flow_store()
|
let store = flow_store()
|
||||||
.add(content_intro)?
|
.add(content_intro)?
|
||||||
|
@ -794,6 +794,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::SUB, GREY, BG, G
|
|||||||
/// the header. [px]
|
/// the header. [px]
|
||||||
pub const SPACING: i16 = 2;
|
pub const SPACING: i16 = 2;
|
||||||
|
|
||||||
|
pub const TITLE_HEIGHT: i16 = 42;
|
||||||
pub const CONTENT_BORDER: i16 = 0;
|
pub const CONTENT_BORDER: i16 = 0;
|
||||||
pub const BUTTON_HEIGHT: i16 = 62;
|
pub const BUTTON_HEIGHT: i16 = 62;
|
||||||
pub const BUTTON_WIDTH: i16 = 78;
|
pub const BUTTON_WIDTH: i16 = 78;
|
||||||
|
@ -388,6 +388,8 @@
|
|||||||
"instructions__swipe_up": "Swipe up",
|
"instructions__swipe_up": "Swipe up",
|
||||||
"instructions__tap_to_confirm": "Tap to confirm",
|
"instructions__tap_to_confirm": "Tap to confirm",
|
||||||
"instructions__tap_to_start": "Tap to start",
|
"instructions__tap_to_start": "Tap to start",
|
||||||
|
"instructions__hold_to_confirm": "Hold to confirm",
|
||||||
|
"instructions__continue_holding": "Continue\nholding",
|
||||||
"joint__title": "Joint transaction",
|
"joint__title": "Joint transaction",
|
||||||
"joint__to_the_total_amount": "To the total amount:",
|
"joint__to_the_total_amount": "To the total amount:",
|
||||||
"joint__you_are_contributing": "You are contributing:",
|
"joint__you_are_contributing": "You are contributing:",
|
||||||
|
Loading…
Reference in New Issue
Block a user