mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-26 09:28:13 +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
|
||||
* wrapping around. This works by setting a maximum allowable difference
|
||||
* 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
|
||||
/// that elapses between its start and stop points. It can be used in various
|
||||
/// situations - animation timing, event timing, testing and debugging.
|
||||
#[derive(Clone)]
|
||||
pub enum Stopwatch {
|
||||
Stopped(Duration),
|
||||
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 {
|
||||
matches!(*self, Self::Running(_))
|
||||
}
|
||||
|
@ -104,6 +104,11 @@ impl<'a> Label<'a> {
|
||||
};
|
||||
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<'_> {
|
||||
|
@ -239,10 +239,19 @@ impl TextLayout {
|
||||
|
||||
/// Draw as much text as possible on the current screen.
|
||||
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(
|
||||
text,
|
||||
&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
|
||||
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>
|
||||
where
|
||||
R: Renderer<'s>,
|
||||
{
|
||||
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)
|
||||
.with_font(layout.style.text_font)
|
||||
.with_fg(layout.style.text_color)
|
||||
.render(self.0);
|
||||
.with_alpha(self.alpha)
|
||||
.render(self.renderer);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
shape::Text::new(cursor, "-")
|
||||
.with_font(layout.style.text_font)
|
||||
.with_fg(layout.style.hyphen_color)
|
||||
.render(self.0);
|
||||
.with_alpha(self.alpha)
|
||||
.render(self.renderer);
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
@ -578,12 +602,14 @@ where
|
||||
shape::ToifImage::new(bottom_left, icon.toif)
|
||||
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||
.with_fg(layout.style.ellipsis_color)
|
||||
.render(self.0);
|
||||
.with_alpha(self.alpha)
|
||||
.render(self.renderer);
|
||||
} else {
|
||||
shape::Text::new(cursor, ELLIPSIS)
|
||||
.with_font(layout.style.text_font)
|
||||
.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)
|
||||
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||
.with_fg(layout.style.ellipsis_color)
|
||||
.render(self.0);
|
||||
.with_alpha(self.alpha)
|
||||
.render(self.renderer);
|
||||
} else {
|
||||
shape::Text::new(cursor, ELLIPSIS)
|
||||
.with_font(layout.style.text_font)
|
||||
.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,
|
||||
long_press: Option<Duration>,
|
||||
long_timer: Option<TimerToken>,
|
||||
haptic: bool,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
@ -54,6 +55,7 @@ impl Button {
|
||||
state: State::Initial,
|
||||
long_press: None,
|
||||
long_timer: None,
|
||||
haptic: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +104,11 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_haptics(mut self) -> Self {
|
||||
self.haptic = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
|
||||
if enabled {
|
||||
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 {
|
||||
ButtonContent::IconBlend(_, _, _) => {}
|
||||
_ => {
|
||||
@ -192,11 +204,13 @@ impl Button {
|
||||
.with_radius(self.radius.unwrap() as i16)
|
||||
.with_thickness(2)
|
||||
.with_fg(style.button_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
} else {
|
||||
shape::Bar::new(self.area)
|
||||
.with_bg(style.button_color)
|
||||
.with_fg(style.button_color)
|
||||
.with_alpha(alpha)
|
||||
.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 {
|
||||
ButtonContent::Empty => {}
|
||||
ButtonContent::Text(text) => {
|
||||
@ -255,6 +274,7 @@ impl Button {
|
||||
.with_font(style.font)
|
||||
.with_fg(style.text_color)
|
||||
.with_align(self.text_align)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
});
|
||||
}
|
||||
@ -262,24 +282,40 @@ impl Button {
|
||||
shape::ToifImage::new(self.area.center(), icon.toif)
|
||||
.with_align(Alignment2D::CENTER)
|
||||
.with_fg(style.icon_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
}
|
||||
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) => {
|
||||
shape::Bar::new(self.area)
|
||||
.with_bg(style.background_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
shape::ToifImage::new(self.area.top_left(), bg.toif)
|
||||
.with_fg(style.button_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
|
||||
.with_fg(style.icon_color)
|
||||
.with_alpha(alpha)
|
||||
.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 {
|
||||
@ -307,7 +343,9 @@ impl Component for Button {
|
||||
// Touch started in our area, transform to `Pressed` state.
|
||||
if touch_area.contains(pos) {
|
||||
#[cfg(feature = "haptic")]
|
||||
play(HapticEffect::ButtonPress);
|
||||
if self.haptic {
|
||||
play(HapticEffect::ButtonPress);
|
||||
}
|
||||
self.set(ctx, State::Pressed);
|
||||
if let Some(duration) = self.long_press {
|
||||
self.long_timer = Some(ctx.request_timer(duration));
|
||||
@ -351,7 +389,9 @@ impl Component for Button {
|
||||
self.long_timer = None;
|
||||
if matches!(self.state, State::Pressed) {
|
||||
#[cfg(feature = "haptic")]
|
||||
play(HapticEffect::ButtonPress);
|
||||
if self.haptic {
|
||||
play(HapticEffect::ButtonPress);
|
||||
}
|
||||
self.set(ctx, State::Initial);
|
||||
return Some(ButtonMsg::LongPressed);
|
||||
}
|
||||
@ -370,8 +410,8 @@ impl Component for Button {
|
||||
|
||||
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||
let style = self.style();
|
||||
self.render_background(target, style);
|
||||
self.render_content(target, style);
|
||||
self.render_background(target, style, 0xFF);
|
||||
self.render_content(target, style, 0xFF);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_bounds")]
|
||||
@ -605,6 +645,7 @@ impl IconText {
|
||||
area: Rect,
|
||||
style: &ButtonStyle,
|
||||
baseline_offset: Offset,
|
||||
alpha: u8,
|
||||
) {
|
||||
let width = self.text.map(|t| style.font.text_width(t));
|
||||
|
||||
@ -635,6 +676,7 @@ impl IconText {
|
||||
shape::Text::new(text_pos, t)
|
||||
.with_font(style.font)
|
||||
.with_fg(style.text_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target)
|
||||
});
|
||||
}
|
||||
@ -643,6 +685,7 @@ impl IconText {
|
||||
shape::ToifImage::new(icon_pos, self.icon.toif)
|
||||
.with_align(Alignment2D::CENTER)
|
||||
.with_fg(style.icon_color)
|
||||
.with_alpha(alpha)
|
||||
.render(target);
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,13 @@ use crate::{
|
||||
},
|
||||
display::Icon,
|
||||
geometry::{Alignment, Insets, Rect},
|
||||
model_mercury::theme::TITLE_HEIGHT,
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{theme, Button, ButtonMsg, ButtonStyleSheet, CancelInfoConfirmMsg, Footer};
|
||||
|
||||
const TITLE_HEIGHT: i16 = 42;
|
||||
const BUTTON_EXPAND_BORDER: i16 = 32;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -24,6 +24,7 @@ pub struct Frame<T> {
|
||||
button_msg: CancelInfoConfirmMsg,
|
||||
content: Child<T>,
|
||||
footer: Option<Footer<'static>>,
|
||||
overlapping_content: bool,
|
||||
}
|
||||
|
||||
pub enum FrameMsg<T> {
|
||||
@ -46,6 +47,7 @@ where
|
||||
button_msg: CancelInfoConfirmMsg::Cancelled,
|
||||
content: Child::new(content),
|
||||
footer: None,
|
||||
overlapping_content: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +190,11 @@ where
|
||||
footer.place(footer_area);
|
||||
content_area = remaining;
|
||||
}
|
||||
self.content.place(content_area);
|
||||
if self.overlapping_content {
|
||||
self.content.place(bounds);
|
||||
} else {
|
||||
self.content.place(content_area);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
@ -209,15 +215,15 @@ where
|
||||
self.title.paint();
|
||||
self.subtitle.paint();
|
||||
self.button.paint();
|
||||
self.content.paint();
|
||||
self.footer.paint();
|
||||
self.content.paint();
|
||||
}
|
||||
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||
self.title.render(target);
|
||||
self.subtitle.render(target);
|
||||
self.button.render(target);
|
||||
self.content.render(target);
|
||||
self.footer.render(target);
|
||||
self.content.render(target);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_bounds")]
|
||||
@ -225,8 +231,8 @@ where
|
||||
self.title.bounds(sink);
|
||||
self.subtitle.bounds(sink);
|
||||
self.button.bounds(sink);
|
||||
self.content.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 error;
|
||||
mod frame;
|
||||
|
||||
#[cfg(feature = "translations")]
|
||||
mod hold_to_confirm;
|
||||
#[cfg(feature = "micropython")]
|
||||
mod homescreen;
|
||||
#[cfg(feature = "translations")]
|
||||
@ -48,6 +51,8 @@ pub use error::ErrorScreen;
|
||||
pub use fido::{FidoConfirm, FidoMsg};
|
||||
pub use footer::Footer;
|
||||
pub use frame::{Frame, FrameMsg};
|
||||
#[cfg(feature = "translations")]
|
||||
pub use hold_to_confirm::HoldToConfirm;
|
||||
#[cfg(feature = "micropython")]
|
||||
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg, Lockscreen};
|
||||
#[cfg(feature = "translations")]
|
||||
|
@ -133,7 +133,8 @@ impl Component for PromptScreen {
|
||||
.with_thickness(2)
|
||||
.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::{
|
||||
error,
|
||||
micropython::{map::Map, obj::Obj, util},
|
||||
strutil::TString,
|
||||
translations::TR,
|
||||
ui::{
|
||||
@ -9,11 +10,12 @@ use crate::{
|
||||
ButtonRequestExt, ComponentExt, SwipeDirection,
|
||||
},
|
||||
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
|
||||
layout::obj::LayoutObj,
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg},
|
||||
component::{Frame, FrameMsg, HoldToConfirm, VerticalMenu, VerticalMenuChoiceMsg},
|
||||
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)]
|
||||
pub extern "C" fn new_confirm_reset_create(
|
||||
n_args: usize,
|
||||
@ -100,16 +97,16 @@ impl ConfirmResetCreate {
|
||||
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
|
||||
});
|
||||
|
||||
let content_confirm = Frame::left_aligned(
|
||||
TR::reset__title_create_wallet.into(),
|
||||
PromptScreen::new_hold_to_confirm(),
|
||||
)
|
||||
.with_footer(TR::instructions__hold_to_confirm.into(), None)
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
||||
_ => Some(FlowMsg::Cancelled),
|
||||
})
|
||||
.one_button_request(ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"));
|
||||
let content_confirm =
|
||||
Frame::left_aligned(TR::reset__title_create_wallet.into(), HoldToConfirm::new())
|
||||
.with_footer(TR::instructions__hold_to_confirm.into(), None)
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
||||
_ => Some(FlowMsg::Cancelled),
|
||||
})
|
||||
.one_button_request(
|
||||
ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"),
|
||||
);
|
||||
|
||||
let store = flow_store()
|
||||
.add(content_intro)?
|
||||
|
@ -794,6 +794,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::SUB, GREY, BG, G
|
||||
/// the header. [px]
|
||||
pub const SPACING: i16 = 2;
|
||||
|
||||
pub const TITLE_HEIGHT: i16 = 42;
|
||||
pub const CONTENT_BORDER: i16 = 0;
|
||||
pub const BUTTON_HEIGHT: i16 = 62;
|
||||
pub const BUTTON_WIDTH: i16 = 78;
|
||||
|
@ -388,6 +388,8 @@
|
||||
"instructions__swipe_up": "Swipe up",
|
||||
"instructions__tap_to_confirm": "Tap to confirm",
|
||||
"instructions__tap_to_start": "Tap to start",
|
||||
"instructions__hold_to_confirm": "Hold to confirm",
|
||||
"instructions__continue_holding": "Continue\nholding",
|
||||
"joint__title": "Joint transaction",
|
||||
"joint__to_the_total_amount": "To the total amount:",
|
||||
"joint__you_are_contributing": "You are contributing:",
|
||||
|
Loading…
Reference in New Issue
Block a user