mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-06-27 10:22:34 +00:00
feat(eckhart): render_loader and ProgressScreen
- `render_loader` shows classic circular loader going around the display, implemented by rendering `ScreenBorder` and hiding it by 5 black rectangles which progressively diminish - `render_loader_indeterminate` shows progress loader without known duration, implemented by rendering `ScreenBorder` in clip which travels around the SCREEN in an octagonal shape. This achieves an effect of a constant length line going around the screen shape - implement `show_progress` and `show_progress_coinjoin` FirmwareUI functions chore(eckhart): remove animation code from Header
This commit is contained in:
parent
843ef7d441
commit
04b61ea6af
@ -16,9 +16,9 @@ use crate::{
|
|||||||
use super::firmware::{
|
use super::firmware::{
|
||||||
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen,
|
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen,
|
||||||
Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg,
|
Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg,
|
||||||
NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg,
|
NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, ProgressScreen,
|
||||||
SelectWordCountScreen, SelectWordMsg, SelectWordScreen, SetBrightnessScreen, TextScreen,
|
SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, SelectWordScreen,
|
||||||
TextScreenMsg,
|
SetBrightnessScreen, TextScreen, TextScreenMsg,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ComponentMsgObj for PinKeyboard<'_> {
|
impl ComponentMsgObj for PinKeyboard<'_> {
|
||||||
@ -80,6 +80,12 @@ impl ComponentMsgObj for Homescreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ComponentMsgObj for ProgressScreen {
|
||||||
|
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> ComponentMsgObj for TextScreen<T>
|
impl<T> ComponentMsgObj for TextScreen<T>
|
||||||
where
|
where
|
||||||
T: AllowedTextContent,
|
T: AllowedTextContent,
|
||||||
|
@ -1,96 +1,179 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
constant::SCREEN,
|
constant::SCREEN,
|
||||||
geometry::{Offset, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
lerp::Lerp,
|
lerp::Lerp,
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{super::theme::BG, ScreenBorder};
|
use super::{super::theme, ScreenBorder};
|
||||||
|
|
||||||
pub fn render_loader<'s>(
|
/// Renders the loader. Higher `progress` reveals the `border` from the top in
|
||||||
progress: u16,
|
/// clock-wise direction. Used in ProgressScreen and Bootloader. `progress` goes
|
||||||
border: &'static ScreenBorder,
|
/// from 0 to 1000.
|
||||||
target: &mut impl Renderer<'s>,
|
pub fn render_loader<'s>(progress: u16, border: &'s ScreenBorder, target: &mut impl Renderer<'s>) {
|
||||||
) {
|
let progress_ratio = progress_to_ratio(progress);
|
||||||
// convert to ration from 0.0 to 1.0
|
// Draw the border first
|
||||||
let progress_ratio = (progress as f32 / 1000.0).clamp(0.0, 1.0);
|
border.render(u8::MAX, target);
|
||||||
let (clip, top_gap) = get_clips(progress_ratio);
|
// Draw the progressively shrinking covers
|
||||||
render_clipped_border(border, clip, top_gap, u8::MAX, target);
|
for cover in get_progress_covers(progress_ratio) {
|
||||||
}
|
shape::Bar::new(cover)
|
||||||
|
.with_bg(theme::BLACK)
|
||||||
fn get_clips(progress_ratio: f32) -> (Rect, Rect) {
|
.with_alpha(u8::MAX)
|
||||||
/// Ratio of total_duration for the bottom part of the border
|
|
||||||
const BOTTOM_DURATION_RATIO: f32 = 0.125;
|
|
||||||
/// Ratio of total_duration for the side parts of the border
|
|
||||||
const SIDES_DURATION_RATIO: f32 = 0.5;
|
|
||||||
/// Ratio of total_duration for the top part of the border
|
|
||||||
const TOP_DURATION_RATIO: f32 = 0.375;
|
|
||||||
|
|
||||||
const TOP_GAP_ZERO: Rect = Rect::from_center_and_size(
|
|
||||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
|
||||||
Offset::zero(),
|
|
||||||
);
|
|
||||||
const TOP_GAP_FULL: Rect = Rect::from_center_and_size(
|
|
||||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
|
||||||
Offset::new(SCREEN.width(), ScreenBorder::WIDTH),
|
|
||||||
);
|
|
||||||
|
|
||||||
match progress_ratio {
|
|
||||||
// Bottom phase growing linearly
|
|
||||||
p if p < BOTTOM_DURATION_RATIO => {
|
|
||||||
let bottom_progress = (p / BOTTOM_DURATION_RATIO).clamp(0.0, 1.0);
|
|
||||||
let width = i16::lerp(0, SCREEN.width(), bottom_progress);
|
|
||||||
let clip = Rect::from_center_and_size(
|
|
||||||
SCREEN
|
|
||||||
.bottom_center()
|
|
||||||
.ofs(Offset::y(-ScreenBorder::WIDTH / 2)),
|
|
||||||
Offset::new(width, ScreenBorder::WIDTH),
|
|
||||||
);
|
|
||||||
(clip, TOP_GAP_FULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sides phase growing up linearly
|
|
||||||
p if p < (BOTTOM_DURATION_RATIO + SIDES_DURATION_RATIO) => {
|
|
||||||
let sides_progress =
|
|
||||||
((p - BOTTOM_DURATION_RATIO) / SIDES_DURATION_RATIO).clamp(0.0, 1.0);
|
|
||||||
let height = i16::lerp(ScreenBorder::WIDTH, SCREEN.height(), sides_progress);
|
|
||||||
let clip = Rect::from_bottom_left_and_size(
|
|
||||||
SCREEN.bottom_left(),
|
|
||||||
Offset::new(SCREEN.width(), height),
|
|
||||||
);
|
|
||||||
(clip, TOP_GAP_FULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top gap shrinking linearly
|
|
||||||
p if p < 1.0 => {
|
|
||||||
let top_progress = ((p - BOTTOM_DURATION_RATIO - SIDES_DURATION_RATIO)
|
|
||||||
/ TOP_DURATION_RATIO)
|
|
||||||
.clamp(0.0, 1.0);
|
|
||||||
let width = i16::lerp(SCREEN.width(), 0, top_progress);
|
|
||||||
let top_gap = Rect::from_center_and_size(
|
|
||||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
|
||||||
Offset::new(width, ScreenBorder::WIDTH),
|
|
||||||
);
|
|
||||||
(SCREEN, top_gap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation complete
|
|
||||||
_ => (SCREEN, TOP_GAP_ZERO),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_clipped_border<'s>(
|
|
||||||
border: &'static ScreenBorder,
|
|
||||||
clip: Rect,
|
|
||||||
top_gap: Rect,
|
|
||||||
alpha: u8,
|
|
||||||
target: &mut impl Renderer<'s>,
|
|
||||||
) {
|
|
||||||
target.in_clip(clip, &|target| {
|
|
||||||
border.render(alpha, target);
|
|
||||||
});
|
|
||||||
shape::Bar::new(top_gap)
|
|
||||||
.with_bg(BG)
|
|
||||||
.with_fg(BG)
|
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the loader in indeterminate mode. A constant size portion of the
|
||||||
|
/// border is rendered at each time moving around in clock-wise direction.
|
||||||
|
pub fn render_loader_indeterminate<'s>(
|
||||||
|
progress: u16,
|
||||||
|
border: &'s ScreenBorder,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
) {
|
||||||
|
let progress_ratio = progress_to_ratio(progress);
|
||||||
|
let clip = get_clip_indeterminate(progress_ratio);
|
||||||
|
// Draw the border in clip
|
||||||
|
target.in_clip(clip, &|target| {
|
||||||
|
border.render(u8::MAX, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress_to_ratio(progress: u16) -> f32 {
|
||||||
|
// convert to ratio from 0.0 to 1.0
|
||||||
|
(progress as f32 / 1000.0).clamp(0.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clip_indeterminate(progress_ratio: f32) -> Rect {
|
||||||
|
const CLIP_SIZE: i16 = 190;
|
||||||
|
|
||||||
|
// Define 8 points (+1 duplicate) for an octagonal path around the display
|
||||||
|
// Position them to ensure the clip always shows `CLIP_SIZE`-wide part of the
|
||||||
|
// border "Right end" and "Left start" points are shifted in y-axis a little
|
||||||
|
// bit upwards to account for the irregular corner shape
|
||||||
|
const PATH_POINTS: [Point; 9] = [
|
||||||
|
// Top start
|
||||||
|
Point::new(CLIP_SIZE / 2, -CLIP_SIZE / 2 + ScreenBorder::WIDTH),
|
||||||
|
// Top end
|
||||||
|
Point::new(
|
||||||
|
SCREEN.width() - CLIP_SIZE / 2,
|
||||||
|
-CLIP_SIZE / 2 + ScreenBorder::WIDTH,
|
||||||
|
),
|
||||||
|
// Right start
|
||||||
|
Point::new(
|
||||||
|
SCREEN.width() + CLIP_SIZE / 2 - ScreenBorder::WIDTH,
|
||||||
|
CLIP_SIZE / 2,
|
||||||
|
),
|
||||||
|
// Right end
|
||||||
|
Point::new(
|
||||||
|
SCREEN.width() + CLIP_SIZE / 2 - ScreenBorder::WIDTH,
|
||||||
|
SCREEN.height() - CLIP_SIZE / 2 - 60,
|
||||||
|
),
|
||||||
|
// Bottom start
|
||||||
|
Point::new(
|
||||||
|
SCREEN.width() - CLIP_SIZE / 2 - ScreenBorder::WIDTH,
|
||||||
|
SCREEN.height() + CLIP_SIZE / 2 - ScreenBorder::WIDTH,
|
||||||
|
),
|
||||||
|
// Bottom end
|
||||||
|
Point::new(
|
||||||
|
CLIP_SIZE / 2,
|
||||||
|
SCREEN.height() + CLIP_SIZE / 2 - ScreenBorder::WIDTH,
|
||||||
|
),
|
||||||
|
// Left start
|
||||||
|
Point::new(
|
||||||
|
-CLIP_SIZE / 2 + ScreenBorder::WIDTH,
|
||||||
|
SCREEN.height() - CLIP_SIZE / 2 - 60,
|
||||||
|
),
|
||||||
|
// Left end
|
||||||
|
Point::new(-CLIP_SIZE / 2 + ScreenBorder::WIDTH, CLIP_SIZE / 2),
|
||||||
|
// Top start - duplicate to close the loop
|
||||||
|
Point::new(CLIP_SIZE / 2, -CLIP_SIZE / 2 + ScreenBorder::WIDTH),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate which segment we're in and how far along that segment
|
||||||
|
let path_length = PATH_POINTS.len() - 1; // -1 because the last point is a duplicate
|
||||||
|
let segment_position = progress_ratio * path_length as f32;
|
||||||
|
|
||||||
|
// Integer part gives us the segment
|
||||||
|
let segment = segment_position as usize % path_length;
|
||||||
|
|
||||||
|
// Fractional part gives us the position within the segment
|
||||||
|
let segment_ratio = segment_position - segment as f32;
|
||||||
|
|
||||||
|
// Get the current point and the next point
|
||||||
|
let current = PATH_POINTS[segment];
|
||||||
|
let next = PATH_POINTS[segment + 1];
|
||||||
|
|
||||||
|
// Linearly interpolate between the current and next points
|
||||||
|
let center = Point::lerp(current, next, segment_ratio);
|
||||||
|
|
||||||
|
Rect::snap(center, Offset::uniform(CLIP_SIZE), Alignment2D::CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_progress_covers(progress_ratio: f32) -> impl Iterator<Item = Rect> {
|
||||||
|
let cover_1 = {
|
||||||
|
// Top-center to top-right
|
||||||
|
const PROGRESS_PORTION: f32 = 0.11;
|
||||||
|
const PROGRESS_START: f32 = 0.0;
|
||||||
|
const FULL_WIDTH: i16 = 190;
|
||||||
|
let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0);
|
||||||
|
let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16;
|
||||||
|
Rect::snap(
|
||||||
|
SCREEN.top_right(),
|
||||||
|
Offset::new(width, theme::ICON_BORDER_TR.toif.height()),
|
||||||
|
Alignment2D::TOP_RIGHT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let cover_2 = {
|
||||||
|
// Top-right to bottom-right
|
||||||
|
const PROGRESS_PORTION: f32 = 0.3;
|
||||||
|
const PROGRESS_START: f32 = 0.11;
|
||||||
|
const FULL_HEIGHT: i16 = 502;
|
||||||
|
let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0);
|
||||||
|
let height = ((1.0 - progress) * FULL_HEIGHT as f32) as i16;
|
||||||
|
Rect::snap(
|
||||||
|
SCREEN.bottom_right(),
|
||||||
|
Offset::new(theme::ICON_BORDER_BR.toif.width(), height),
|
||||||
|
Alignment2D::BOTTOM_RIGHT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let cover_3 = {
|
||||||
|
// Bottom-right to bottom-left
|
||||||
|
const PROGRESS_PORTION: f32 = 0.18;
|
||||||
|
const PROGRESS_START: f32 = 0.41;
|
||||||
|
const FULL_WIDTH: i16 = 298;
|
||||||
|
let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0);
|
||||||
|
let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16;
|
||||||
|
Rect::snap(
|
||||||
|
SCREEN.bottom_left() + Offset::x(theme::ICON_BORDER_BL.toif.width()),
|
||||||
|
Offset::new(width, ScreenBorder::WIDTH),
|
||||||
|
Alignment2D::BOTTOM_LEFT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let cover_4 = {
|
||||||
|
// Bottom-left to top-left
|
||||||
|
const PROGRESS_PORTION: f32 = 0.3;
|
||||||
|
const PROGRESS_START: f32 = 0.59;
|
||||||
|
const FULL_HEIGHT: i16 = 502;
|
||||||
|
let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0);
|
||||||
|
let height = ((1.0 - progress) * FULL_HEIGHT as f32) as i16;
|
||||||
|
Rect::snap(
|
||||||
|
SCREEN.top_left() + Offset::y(theme::ICON_BORDER_TL.toif.height()),
|
||||||
|
Offset::new(theme::ICON_BORDER_BL.toif.width(), height),
|
||||||
|
Alignment2D::TOP_LEFT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let cover_5 = {
|
||||||
|
// Top-left to top-center
|
||||||
|
const PROGRESS_PORTION: f32 = 0.11;
|
||||||
|
const PROGRESS_START: f32 = 0.89;
|
||||||
|
const FULL_WIDTH: i16 = 190;
|
||||||
|
let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0);
|
||||||
|
let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16;
|
||||||
|
Rect::snap(
|
||||||
|
SCREEN.top_center(),
|
||||||
|
Offset::new(width, theme::ICON_BORDER_TL.toif.height()),
|
||||||
|
Alignment2D::TOP_RIGHT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
[cover_1, cover_2, cover_3, cover_4, cover_5].into_iter()
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
mod loader;
|
mod loader;
|
||||||
mod screen_border;
|
mod screen_border;
|
||||||
|
|
||||||
pub use loader::render_loader;
|
pub use loader::{render_loader, render_loader_indeterminate};
|
||||||
pub use screen_border::ScreenBorder;
|
pub use screen_border::ScreenBorder;
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
time::{Duration, Stopwatch},
|
|
||||||
ui::{
|
ui::{
|
||||||
component::{text::TextStyle, Component, Event, EventCtx, Label},
|
component::{text::TextStyle, Component, Event, EventCtx, Label},
|
||||||
display::{Color, Icon},
|
display::{Color, Icon},
|
||||||
geometry::{Alignment2D, Insets, Offset, Rect},
|
geometry::{Alignment2D, Insets, Rect},
|
||||||
lerp::Lerp,
|
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
util::animation_disabled,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,49 +13,6 @@ use super::{
|
|||||||
constant, theme,
|
constant, theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMATION_TIME_MS: u32 = 1000;
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct AttachAnimation {
|
|
||||||
pub timer: Stopwatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttachAnimation {
|
|
||||||
pub fn is_active(&self) -> bool {
|
|
||||||
if animation_disabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.timer
|
|
||||||
.is_running_within(Duration::from_millis(ANIMATION_TIME_MS))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval(&self) -> f32 {
|
|
||||||
if animation_disabled() {
|
|
||||||
return ANIMATION_TIME_MS as f32 / 1000.0;
|
|
||||||
}
|
|
||||||
self.timer.elapsed().to_millis() as f32 / 1000.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_title_offset(&self, t: f32) -> i16 {
|
|
||||||
let fnc = pareen::constant(0.0).seq_ease_in_out(
|
|
||||||
0.8,
|
|
||||||
easer::functions::Cubic,
|
|
||||||
0.2,
|
|
||||||
pareen::constant(1.0),
|
|
||||||
);
|
|
||||||
i16::lerp(0, 25, fnc.eval(t))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(&mut self) {
|
|
||||||
self.timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.timer = Stopwatch::new_stopped();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUTTON_EXPAND_BORDER: i16 = 32;
|
const BUTTON_EXPAND_BORDER: i16 = 32;
|
||||||
|
|
||||||
/// Component for the header of a screen. Eckhart UI shows the title (can be two
|
/// Component for the header of a screen. Eckhart UI shows the title (can be two
|
||||||
@ -77,8 +31,6 @@ pub struct Header {
|
|||||||
/// icon in the top-left corner (used instead of left button)
|
/// icon in the top-left corner (used instead of left button)
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
icon_color: Option<Color>,
|
icon_color: Option<Color>,
|
||||||
/// animation
|
|
||||||
anim: Option<AttachAnimation>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@ -104,7 +56,6 @@ impl Header {
|
|||||||
left_button_msg: HeaderMsg::Cancelled,
|
left_button_msg: HeaderMsg::Cancelled,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
anim: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,20 +167,6 @@ impl Component for Header {
|
|||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
self.title.event(ctx, event);
|
self.title.event(ctx, event);
|
||||||
|
|
||||||
if let Some(anim) = &mut self.anim {
|
|
||||||
if let Event::Attach(_) = event {
|
|
||||||
anim.start();
|
|
||||||
ctx.request_paint();
|
|
||||||
ctx.request_anim_frame();
|
|
||||||
}
|
|
||||||
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
|
||||||
if anim.is_active() {
|
|
||||||
ctx.request_anim_frame();
|
|
||||||
ctx.request_paint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||||
return Some(self.left_button_msg);
|
return Some(self.left_button_msg);
|
||||||
};
|
};
|
||||||
@ -241,14 +178,6 @@ impl Component for Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let offset = if let Some(anim) = &self.anim {
|
|
||||||
Offset::x(anim.get_title_offset(anim.eval()))
|
|
||||||
} else {
|
|
||||||
Offset::zero()
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: correct animation
|
|
||||||
target.with_origin(offset, &|target| {
|
|
||||||
self.right_button.render(target);
|
self.right_button.render(target);
|
||||||
self.left_button.render(target);
|
self.left_button.render(target);
|
||||||
if let Some(icon) = self.icon {
|
if let Some(icon) = self.icon {
|
||||||
@ -258,7 +187,6 @@ impl Component for Header {
|
|||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
self.title.render(target);
|
self.title.render(target);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ mod hold_to_confirm;
|
|||||||
mod homescreen;
|
mod homescreen;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
mod number_input_screen;
|
mod number_input_screen;
|
||||||
|
mod progress_screen;
|
||||||
mod qr_screen;
|
mod qr_screen;
|
||||||
mod select_word_screen;
|
mod select_word_screen;
|
||||||
mod share_words;
|
mod share_words;
|
||||||
@ -36,6 +37,7 @@ pub use keyboard::{
|
|||||||
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
|
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
|
||||||
};
|
};
|
||||||
pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg};
|
pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg};
|
||||||
|
pub use progress_screen::ProgressScreen;
|
||||||
pub use qr_screen::{QrMsg, QrScreen};
|
pub use qr_screen::{QrMsg, QrScreen};
|
||||||
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
|
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
|
||||||
pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg};
|
pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg};
|
||||||
|
@ -0,0 +1,176 @@
|
|||||||
|
use core::mem;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
strutil::TString,
|
||||||
|
translations::TR,
|
||||||
|
ui::{
|
||||||
|
component::{
|
||||||
|
text::paragraphs::{Paragraph, ParagraphSource as _, ParagraphVecShort, Paragraphs},
|
||||||
|
Component, Event, EventCtx, Label, Never,
|
||||||
|
},
|
||||||
|
geometry::{Alignment, Alignment2D, LinearPlacement, Offset, Rect},
|
||||||
|
shape::{self, Renderer},
|
||||||
|
util::animation_disabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
constant::SCREEN,
|
||||||
|
cshape::{render_loader, render_loader_indeterminate, ScreenBorder},
|
||||||
|
fonts, theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOADER_SPEED: u16 = 5;
|
||||||
|
|
||||||
|
pub struct ProgressScreen {
|
||||||
|
indeterminate: bool,
|
||||||
|
text: Paragraphs<ParagraphVecShort<'static>>,
|
||||||
|
/// Current value of the progress bar.
|
||||||
|
value: u16,
|
||||||
|
border: ScreenBorder,
|
||||||
|
/// Whether the progress is for Coinjoin BusyScreen
|
||||||
|
coinjoin_progress: bool,
|
||||||
|
coinjoin_do_not_disconnect: Option<Label<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressScreen {
|
||||||
|
pub fn new_progress(
|
||||||
|
title: TString<'static>,
|
||||||
|
indeterminate: bool,
|
||||||
|
description: TString<'static>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
indeterminate,
|
||||||
|
text: Self::create_paragraphs(title, description),
|
||||||
|
value: 0,
|
||||||
|
border: ScreenBorder::new(theme::GREEN_LIME),
|
||||||
|
coinjoin_progress: false,
|
||||||
|
coinjoin_do_not_disconnect: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_coinjoin_progress(
|
||||||
|
title: TString<'static>,
|
||||||
|
indeterminate: bool,
|
||||||
|
description: TString<'static>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
indeterminate,
|
||||||
|
text: Self::create_paragraphs(title, description),
|
||||||
|
value: 0,
|
||||||
|
border: ScreenBorder::new(theme::GREEN_LIME),
|
||||||
|
coinjoin_progress: true,
|
||||||
|
coinjoin_do_not_disconnect: Some(
|
||||||
|
Label::centered(
|
||||||
|
TR::coinjoin__title_do_not_disconnect.into(),
|
||||||
|
theme::TEXT_REGULAR,
|
||||||
|
)
|
||||||
|
.vertically_centered(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_paragraphs(
|
||||||
|
title: TString<'static>,
|
||||||
|
description: TString<'static>,
|
||||||
|
) -> Paragraphs<ParagraphVecShort<'static>> {
|
||||||
|
ParagraphVecShort::from_iter([
|
||||||
|
Paragraph::new(&theme::firmware::TEXT_MEDIUM_GREY, title).centered(),
|
||||||
|
Paragraph::new(&theme::firmware::TEXT_MEDIUM_GREY, description).centered(),
|
||||||
|
])
|
||||||
|
.into_paragraphs()
|
||||||
|
.with_placement(LinearPlacement::vertical().align_at_center())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ProgressScreen {
|
||||||
|
type Msg = Never;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
debug_assert_eq!(bounds.height(), SCREEN.height());
|
||||||
|
debug_assert_eq!(bounds.width(), SCREEN.width());
|
||||||
|
let bounds = bounds.inset(theme::SIDE_INSETS);
|
||||||
|
|
||||||
|
let max_text_area = 3 * theme::TEXT_REGULAR.text_font.text_max_height();
|
||||||
|
let middle_text_area = Rect::snap(
|
||||||
|
SCREEN.center(),
|
||||||
|
Offset::new(bounds.width(), max_text_area),
|
||||||
|
Alignment2D::CENTER,
|
||||||
|
);
|
||||||
|
let action_bar_area = bounds.split_bottom(theme::ACTION_BAR_HEIGHT).1;
|
||||||
|
|
||||||
|
self.coinjoin_do_not_disconnect.place(middle_text_area);
|
||||||
|
self.text.place(action_bar_area);
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
// ProgressScreen only reacts to Progress events
|
||||||
|
// CoinjoinProgressScreen with indeterminate reacts to ANIM_FRAME_TIMER
|
||||||
|
match event {
|
||||||
|
_ if animation_disabled() => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Event::Attach(_) if self.coinjoin_progress && self.indeterminate => {
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
}
|
||||||
|
Event::Timer(EventCtx::ANIM_FRAME_TIMER) => {
|
||||||
|
self.value = (self.value + LOADER_SPEED) % 1000;
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::Progress(new_value, new_description) => {
|
||||||
|
if mem::replace(&mut self.value, new_value) != new_value {
|
||||||
|
if !animation_disabled() {
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
if self.text.inner()[1].content() != &new_description {
|
||||||
|
self.text.mutate(|p| p[1].update(new_description));
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let progress_val = self.value.min(1000);
|
||||||
|
if self.indeterminate {
|
||||||
|
render_loader_indeterminate(progress_val, &self.border, target);
|
||||||
|
} else {
|
||||||
|
render_loader(progress_val, &self.border, target);
|
||||||
|
if !self.coinjoin_progress {
|
||||||
|
render_percentage(progress_val, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.coinjoin_progress {
|
||||||
|
self.coinjoin_do_not_disconnect.render(target);
|
||||||
|
}
|
||||||
|
self.text.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_percentage<'s>(progress: u16, target: &mut impl Renderer<'s>) {
|
||||||
|
let progress_percent = uformat!("{}%", (progress as f32 / 10.0) as i16);
|
||||||
|
shape::Text::new(
|
||||||
|
SCREEN.center(),
|
||||||
|
&progress_percent,
|
||||||
|
fonts::FONT_SATOSHI_EXTRALIGHT_72,
|
||||||
|
)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_fg(theme::GREY_LIGHT)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for ProgressScreen {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
if self.coinjoin_progress {
|
||||||
|
t.component("CoinjoinProgress");
|
||||||
|
} else {
|
||||||
|
t.component("Progress");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ use crate::{
|
|||||||
Paragraphs, VecExt,
|
Paragraphs, VecExt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Empty, FormattedText,
|
ComponentExt as _, Empty, FormattedText, Timeout,
|
||||||
},
|
},
|
||||||
geometry::{Alignment, LinearPlacement, Offset},
|
geometry::{Alignment, LinearPlacement, Offset},
|
||||||
layout::{
|
layout::{
|
||||||
@ -33,8 +33,8 @@ use super::{
|
|||||||
component::Button,
|
component::Button,
|
||||||
firmware::{
|
firmware::{
|
||||||
ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, Header, HeaderMsg, Hint,
|
ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, Header, HeaderMsg, Hint,
|
||||||
Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen,
|
Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, ProgressScreen,
|
||||||
SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen,
|
SelectWordCountScreen, SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen,
|
||||||
},
|
},
|
||||||
flow, fonts, theme, UIEckhart,
|
flow, fonts, theme, UIEckhart,
|
||||||
};
|
};
|
||||||
@ -921,7 +921,7 @@ impl FirmwareUI for UIEckhart {
|
|||||||
|
|
||||||
fn show_progress(
|
fn show_progress(
|
||||||
description: TString<'static>,
|
description: TString<'static>,
|
||||||
_indeterminate: bool,
|
indeterminate: bool,
|
||||||
title: Option<TString<'static>>,
|
title: Option<TString<'static>>,
|
||||||
) -> Result<impl LayoutMaybeTrace, Error> {
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
let (title, description) = if let Some(title) = title {
|
let (title, description) = if let Some(title) = title {
|
||||||
@ -930,23 +930,35 @@ impl FirmwareUI for UIEckhart {
|
|||||||
(description, "".into())
|
(description, "".into())
|
||||||
};
|
};
|
||||||
|
|
||||||
let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, description)
|
let layout = RootComponent::new(ProgressScreen::new_progress(
|
||||||
.into_paragraphs()
|
title,
|
||||||
.with_placement(LinearPlacement::vertical());
|
indeterminate,
|
||||||
let header = Header::new(title);
|
description,
|
||||||
let screen = TextScreen::new(paragraphs).with_header(header);
|
));
|
||||||
|
|
||||||
let layout = RootComponent::new(screen);
|
|
||||||
Ok(layout)
|
Ok(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_progress_coinjoin(
|
fn show_progress_coinjoin(
|
||||||
_title: TString<'static>,
|
description: TString<'static>,
|
||||||
_indeterminate: bool,
|
indeterminate: bool,
|
||||||
_time_ms: u32,
|
time_ms: u32,
|
||||||
_skip_first_paint: bool,
|
skip_first_paint: bool,
|
||||||
) -> Result<Gc<LayoutObj>, Error> {
|
) -> Result<Gc<LayoutObj>, Error> {
|
||||||
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
let progress = ProgressScreen::new_coinjoin_progress(
|
||||||
|
TR::coinjoin__title_progress.into(),
|
||||||
|
indeterminate,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
let obj = if time_ms > 0 && indeterminate {
|
||||||
|
let timeout = Timeout::new(time_ms);
|
||||||
|
LayoutObj::new((timeout, progress.map(|_msg| None)))?
|
||||||
|
} else {
|
||||||
|
LayoutObj::new(progress)?
|
||||||
|
};
|
||||||
|
if skip_first_paint {
|
||||||
|
obj.skip_first_paint();
|
||||||
|
}
|
||||||
|
Ok(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_share_words(
|
fn show_share_words(
|
||||||
|
Loading…
Reference in New Issue
Block a user