mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-17 19:00:58 +00:00
feat(core/rust): autoclosing popup for Model R with success animation
This commit is contained in:
parent
1697be2de8
commit
cc2bfd9c39
@ -376,6 +376,10 @@ impl Insets {
|
|||||||
pub const fn left(d: i32) -> Self {
|
pub const fn left(d: i32) -> Self {
|
||||||
Self::new(0, 0, 0, d)
|
Self::new(0, 0, 0, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn sides(d: i32) -> Self {
|
||||||
|
Self::new(0, d, 0, d)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
@ -4,6 +4,8 @@ mod dialog;
|
|||||||
mod frame;
|
mod frame;
|
||||||
mod loader;
|
mod loader;
|
||||||
mod page;
|
mod page;
|
||||||
|
mod result_anim;
|
||||||
|
mod result_popup;
|
||||||
|
|
||||||
use super::theme;
|
use super::theme;
|
||||||
|
|
||||||
@ -13,3 +15,5 @@ pub use dialog::{Dialog, DialogMsg};
|
|||||||
pub use frame::Frame;
|
pub use frame::Frame;
|
||||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||||
pub use page::ButtonPage;
|
pub use page::ButtonPage;
|
||||||
|
pub use result_anim::{ResultAnim, ResultAnimMsg};
|
||||||
|
pub use result_popup::{ResultPopup, ResultPopupMsg};
|
||||||
|
149
core/embed/rust/src/ui/model_tr/component/result_anim.rs
Normal file
149
core/embed/rust/src/ui/model_tr/component/result_anim.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use crate::{
|
||||||
|
time::{Duration, Instant},
|
||||||
|
ui::{
|
||||||
|
animation::Animation,
|
||||||
|
component::{Component, Event, EventCtx},
|
||||||
|
display,
|
||||||
|
geometry::Rect,
|
||||||
|
model_tr::theme,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ResultAnimMsg {
|
||||||
|
FullyGrown,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Initial,
|
||||||
|
Growing(Animation<u16>),
|
||||||
|
Grown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResultAnim {
|
||||||
|
area: Rect,
|
||||||
|
state: State,
|
||||||
|
growing_duration: Duration,
|
||||||
|
icon: &'static [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResultAnim {
|
||||||
|
pub fn new(icon: &'static [u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
state: State::Initial,
|
||||||
|
growing_duration: Duration::from_millis(2000),
|
||||||
|
icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
|
||||||
|
let anim = Animation::new(
|
||||||
|
display::LOADER_MIN,
|
||||||
|
display::LOADER_MAX,
|
||||||
|
self.growing_duration,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.state = State::Growing(anim);
|
||||||
|
|
||||||
|
// The animation is starting, request an animation frame event.
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
|
||||||
|
// We don't have to wait for the animation frame event with the first paint,
|
||||||
|
// let's do that now.
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.state = State::Initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animation(&self) -> Option<&Animation<u16>> {
|
||||||
|
match &self.state {
|
||||||
|
State::Initial => None,
|
||||||
|
State::Grown => None,
|
||||||
|
State::Growing(a) => Some(a),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn progress(&self, now: Instant) -> Option<u16> {
|
||||||
|
self.animation().map(|a| a.value(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_animating(&self) -> bool {
|
||||||
|
self.animation().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_completely_grown(&self, now: Instant) -> bool {
|
||||||
|
matches!(self.progress(now), Some(display::LOADER_MAX))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint_anim(&mut self, done: i32) {
|
||||||
|
display::rect_rounded2_partial(
|
||||||
|
self.area,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
100 * done / 1000,
|
||||||
|
Some((self.icon, theme::FG)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ResultAnim {
|
||||||
|
type Msg = ResultAnimMsg;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
||||||
|
if let State::Growing(_) = self.state {
|
||||||
|
// We have something to paint, so request to be painted in the next pass.
|
||||||
|
ctx.request_paint();
|
||||||
|
|
||||||
|
if self.is_completely_grown(now) {
|
||||||
|
self.state = State::Grown;
|
||||||
|
return Some(ResultAnimMsg::FullyGrown);
|
||||||
|
} else {
|
||||||
|
// There is further progress in the animation, request an animation frame event.
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
// TODO: Consider passing the current instant along with the event -- that way,
|
||||||
|
// we could synchronize painting across the component tree. Also could be useful
|
||||||
|
// in automated tests.
|
||||||
|
// In practice, taking the current instant here is more precise in case some
|
||||||
|
// other component in the tree takes a long time to draw.
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if let State::Initial = self.state {
|
||||||
|
self.paint_anim(0);
|
||||||
|
} else if let State::Grown = self.state {
|
||||||
|
self.paint_anim(display::LOADER_MAX as i32);
|
||||||
|
} else {
|
||||||
|
let progress = self.progress(now);
|
||||||
|
if let Some(done) = progress {
|
||||||
|
self.paint_anim(done as i32);
|
||||||
|
} else {
|
||||||
|
self.paint_anim(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for ResultAnim {
|
||||||
|
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||||
|
d.open("ResultAnim");
|
||||||
|
d.close();
|
||||||
|
}
|
||||||
|
}
|
193
core/embed/rust/src/ui/model_tr/component/result_popup.rs
Normal file
193
core/embed/rust/src/ui/model_tr/component/result_popup.rs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
use crate::{
|
||||||
|
time::Instant,
|
||||||
|
ui::{
|
||||||
|
component::{
|
||||||
|
text::{layout::DefaultTextTheme, paragraphs::Paragraphs},
|
||||||
|
Child, Component, ComponentExt, Event, EventCtx, Label, LabelStyle, Pad,
|
||||||
|
},
|
||||||
|
constant::screen,
|
||||||
|
display::{Color, Font},
|
||||||
|
geometry::{Alignment, Insets, LinearPlacement, Point, Rect},
|
||||||
|
model_tr::{
|
||||||
|
component::{Button, ButtonMsg, ButtonPos, ResultAnim, ResultAnimMsg},
|
||||||
|
theme,
|
||||||
|
theme::{TRDefaultText, FONT_BOLD, FONT_MEDIUM},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ResultPopupMsg {
|
||||||
|
Confirmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResultPopup {
|
||||||
|
area: Rect,
|
||||||
|
pad: Pad,
|
||||||
|
result_anim: Child<ResultAnim>,
|
||||||
|
headline_baseline: Point,
|
||||||
|
headline: Option<Label<&'static str>>,
|
||||||
|
text: Child<Paragraphs<&'static str>>,
|
||||||
|
button: Option<Child<Button<&'static str>>>,
|
||||||
|
autoclose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageText;
|
||||||
|
|
||||||
|
impl DefaultTextTheme for MessageText {
|
||||||
|
const BACKGROUND_COLOR: Color = theme::BG;
|
||||||
|
const TEXT_FONT: Font = FONT_MEDIUM;
|
||||||
|
const TEXT_COLOR: Color = theme::FG;
|
||||||
|
const HYPHEN_FONT: Font = FONT_MEDIUM;
|
||||||
|
const HYPHEN_COLOR: Color = theme::FG;
|
||||||
|
const ELLIPSIS_FONT: Font = FONT_MEDIUM;
|
||||||
|
const ELLIPSIS_COLOR: Color = theme::FG;
|
||||||
|
|
||||||
|
const NORMAL_FONT: Font = FONT_MEDIUM;
|
||||||
|
const MEDIUM_FONT: Font = theme::FONT_MEDIUM;
|
||||||
|
const BOLD_FONT: Font = theme::FONT_BOLD;
|
||||||
|
const MONO_FONT: Font = theme::FONT_MONO;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANIM_SIZE: i32 = 18;
|
||||||
|
const BUTTON_HEIGHT: i32 = 13;
|
||||||
|
const ANIM_SPACE: i32 = 11;
|
||||||
|
const ANIM_POS: i32 = 32;
|
||||||
|
const ANIM_POS_ADJ_HEADLINE: i32 = 10;
|
||||||
|
const ANIM_POS_ADJ_BUTTON: i32 = 6;
|
||||||
|
|
||||||
|
impl ResultPopup {
|
||||||
|
pub fn new(
|
||||||
|
icon: &'static [u8],
|
||||||
|
text: &'static str,
|
||||||
|
headline: Option<&'static str>,
|
||||||
|
button_text: Option<&'static str>,
|
||||||
|
) -> Self {
|
||||||
|
let p1 = Paragraphs::new()
|
||||||
|
.add::<TRDefaultText>(FONT_MEDIUM, text)
|
||||||
|
.with_placement(LinearPlacement::vertical().align_at_center());
|
||||||
|
|
||||||
|
let button = button_text.map(|t| {
|
||||||
|
Child::new(Button::with_text(
|
||||||
|
ButtonPos::Right,
|
||||||
|
t,
|
||||||
|
theme::button_default(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
let headline_style = LabelStyle {
|
||||||
|
background_color: theme::BG,
|
||||||
|
text_color: theme::FG,
|
||||||
|
font: FONT_BOLD,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pad = Pad::with_background(theme::BG);
|
||||||
|
pad.clear();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
pad,
|
||||||
|
result_anim: Child::new(ResultAnim::new(icon)),
|
||||||
|
headline: headline.map(|a| Label::new(a, Alignment::Center, headline_style)),
|
||||||
|
headline_baseline: Point::zero(),
|
||||||
|
text: Child::new(p1),
|
||||||
|
button,
|
||||||
|
autoclose: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoclose even if button is used
|
||||||
|
pub fn autoclose(&mut self) {
|
||||||
|
self.autoclose = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self, ctx: &mut EventCtx) {
|
||||||
|
self.text.request_complete_repaint(ctx);
|
||||||
|
self.headline.request_complete_repaint(ctx);
|
||||||
|
self.button.request_complete_repaint(ctx);
|
||||||
|
self.result_anim.mutate(ctx, |ctx, c| {
|
||||||
|
let now = Instant::now();
|
||||||
|
c.start_growing(ctx, now);
|
||||||
|
});
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ResultPopup {
|
||||||
|
type Msg = ResultPopupMsg;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
|
||||||
|
let anim_margins = (screen().width() - ANIM_SIZE) / 2;
|
||||||
|
let mut anim_adjust = 0;
|
||||||
|
let mut headline_height = 0;
|
||||||
|
let mut button_height = 0;
|
||||||
|
|
||||||
|
if let Some(h) = self.headline.as_mut() {
|
||||||
|
headline_height = h.size().y;
|
||||||
|
anim_adjust += ANIM_POS_ADJ_HEADLINE;
|
||||||
|
}
|
||||||
|
if self.button.is_some() {
|
||||||
|
button_height = BUTTON_HEIGHT;
|
||||||
|
anim_adjust += ANIM_POS_ADJ_BUTTON;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, rest) = bounds.split_top(ANIM_POS - anim_adjust);
|
||||||
|
let (anim, rest) = rest.split_top(ANIM_SIZE);
|
||||||
|
let (_, rest) = rest.split_top(ANIM_SPACE);
|
||||||
|
let (headline, rest) = rest.split_top(headline_height);
|
||||||
|
let (text, buttons) = rest.split_bottom(button_height);
|
||||||
|
|
||||||
|
self.pad.place(bounds);
|
||||||
|
self.button.place(buttons);
|
||||||
|
self.headline.place(headline);
|
||||||
|
self.text.place(text);
|
||||||
|
self.result_anim
|
||||||
|
.place(anim.inset(Insets::sides(anim_margins)));
|
||||||
|
|
||||||
|
self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
let mut button_confirmed = false;
|
||||||
|
|
||||||
|
self.text.event(ctx, event);
|
||||||
|
self.headline.event(ctx, event);
|
||||||
|
|
||||||
|
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
|
||||||
|
button_confirmed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ResultAnimMsg::FullyGrown) = self.result_anim.event(ctx, event) {
|
||||||
|
if self.button.is_none() || self.autoclose {
|
||||||
|
return Some(ResultPopupMsg::Confirmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_confirmed {
|
||||||
|
return Some(ResultPopupMsg::Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
self.pad.paint();
|
||||||
|
self.text.paint();
|
||||||
|
self.button.paint();
|
||||||
|
self.headline.paint();
|
||||||
|
self.result_anim.paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for ResultPopup {
|
||||||
|
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||||
|
d.open("ResultPopup");
|
||||||
|
self.text.trace(d);
|
||||||
|
self.button.trace(d);
|
||||||
|
self.headline.trace(d);
|
||||||
|
self.result_anim.trace(d);
|
||||||
|
d.close();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user