mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-06-01 05:38:45 +00:00
feat(core): animated lockscreen for mercury UI
[no changelog]
This commit is contained in:
parent
b178c10e8b
commit
a34c2cb9dd
@ -87,11 +87,6 @@ SECTIONS {
|
|||||||
. = ALIGN(4);
|
. = ALIGN(4);
|
||||||
} >SRAM1
|
} >SRAM1
|
||||||
|
|
||||||
.buf : ALIGN(4) {
|
|
||||||
*(.buf*);
|
|
||||||
. = ALIGN(4);
|
|
||||||
} >SRAM1
|
|
||||||
|
|
||||||
.stack : ALIGN(8) {
|
.stack : ALIGN(8) {
|
||||||
. = 16K; /* Overflow causes UsageFault */
|
. = 16K; /* Overflow causes UsageFault */
|
||||||
} >SRAM2
|
} >SRAM2
|
||||||
@ -110,6 +105,11 @@ SECTIONS {
|
|||||||
. = ALIGN(4);
|
. = ALIGN(4);
|
||||||
} >SRAM3
|
} >SRAM3
|
||||||
|
|
||||||
|
.buf : ALIGN(4) {
|
||||||
|
*(.buf*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} >SRAM3
|
||||||
|
|
||||||
.heap : ALIGN(4) {
|
.heap : ALIGN(4) {
|
||||||
. = 37K; /* this acts as a build time assertion that at least this much memory is available for heap use */
|
. = 37K; /* this acts as a build time assertion that at least this much memory is available for heap use */
|
||||||
. = ABSOLUTE(sram3_end); /* this explicitly sets the end of the heap */
|
. = ABSOLUTE(sram3_end); /* this explicitly sets the end of the heap */
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
io::BinaryData,
|
io::BinaryData,
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant, Stopwatch},
|
||||||
translations::TR,
|
translations::TR,
|
||||||
trezorhal::usb::usb_configured,
|
trezorhal::usb::usb_configured,
|
||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, TimerToken},
|
component::{Component, Event, EventCtx, TimerToken},
|
||||||
display::{
|
display::{image::ImageInfo, toif::Icon, Color, Font},
|
||||||
image::{ImageInfo, ToifFormat},
|
|
||||||
toif::Icon,
|
|
||||||
Color, Font,
|
|
||||||
},
|
|
||||||
event::{TouchEvent, USBEvent},
|
event::{TouchEvent, USBEvent},
|
||||||
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
|
||||||
layout::util::get_user_custom_image,
|
layout::util::get_user_custom_image,
|
||||||
@ -18,12 +14,16 @@ use crate::{
|
|||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use core::mem;
|
||||||
|
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
constant::{screen, HEIGHT, WIDTH},
|
constant::{screen, HEIGHT, WIDTH},
|
||||||
model_mercury::theme::{
|
model_mercury::{
|
||||||
GREEN_LIGHT, GREY_LIGHT, ICON_CENTRAL_CIRCLE, ICON_KEY, ICON_LOCKSCREEN_FILTER,
|
cshape,
|
||||||
|
theme::{GREY_LIGHT, ICON_KEY},
|
||||||
},
|
},
|
||||||
|
shape::{render_on_canvas, ImageBuffer, Rgb565Canvas},
|
||||||
|
util::animation_disabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{theme, Loader, LoaderMsg};
|
use super::{theme, Loader, LoaderMsg};
|
||||||
@ -194,9 +194,6 @@ impl Component for Homescreen {
|
|||||||
ImageInfo::Jpeg(_) => shape::JpegImage::new_image(AREA.center(), self.image)
|
ImageInfo::Jpeg(_) => shape::JpegImage::new_image(AREA.center(), self.image)
|
||||||
.with_align(Alignment2D::CENTER)
|
.with_align(Alignment2D::CENTER)
|
||||||
.render(target),
|
.render(target),
|
||||||
ImageInfo::Toif(_) => shape::ToifImage::new_image(AREA.center(), self.image)
|
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
.render(target),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,20 +270,54 @@ impl crate::trace::Trace for Homescreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct LockscreenAnim {
|
||||||
|
pub timer: Stopwatch,
|
||||||
|
}
|
||||||
|
impl LockscreenAnim {
|
||||||
|
const DURATION_MS: u32 = 1500;
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval(&self) -> f32 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let anim = pareen::prop(30.0f32);
|
||||||
|
|
||||||
|
let t: f32 = self.timer.elapsed().to_millis() as f32 / 1000.0;
|
||||||
|
|
||||||
|
anim.eval(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Lockscreen<'a> {
|
pub struct Lockscreen<'a> {
|
||||||
|
anim: LockscreenAnim,
|
||||||
label: TString<'a>,
|
label: TString<'a>,
|
||||||
image: BinaryData<'static>,
|
image: BinaryData<'static>,
|
||||||
bootscreen: bool,
|
bootscreen: bool,
|
||||||
coinjoin_authorized: bool,
|
coinjoin_authorized: bool,
|
||||||
|
bg_image: ImageBuffer<Rgb565Canvas<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Lockscreen<'a> {
|
impl<'a> Lockscreen<'a> {
|
||||||
pub fn new(label: TString<'a>, bootscreen: bool, coinjoin_authorized: bool) -> Self {
|
pub fn new(label: TString<'a>, bootscreen: bool, coinjoin_authorized: bool) -> Self {
|
||||||
|
let image = get_homescreen_image();
|
||||||
|
let mut buf = unwrap!(ImageBuffer::new(AREA.size()), "no image buf");
|
||||||
|
|
||||||
|
render_on_canvas(buf.canvas(), None, |target| {
|
||||||
|
shape::JpegImage::new_image(Point::zero(), image).render(target);
|
||||||
|
});
|
||||||
|
|
||||||
Lockscreen {
|
Lockscreen {
|
||||||
|
anim: LockscreenAnim::default(),
|
||||||
label,
|
label,
|
||||||
image: get_homescreen_image(),
|
image,
|
||||||
bootscreen,
|
bootscreen,
|
||||||
coinjoin_authorized,
|
coinjoin_authorized,
|
||||||
|
bg_image: buf,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -298,10 +329,29 @@ impl Component for Lockscreen<'_> {
|
|||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
if event == Event::Attach {
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
||||||
|
if (!animation_disabled()) {
|
||||||
|
if !self.anim.timer.is_running() {
|
||||||
|
self.anim.timer.start();
|
||||||
|
}
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Event::Touch(TouchEvent::TouchEnd(_)) = event {
|
if let Event::Touch(TouchEvent::TouchEnd(_)) = event {
|
||||||
|
let bg_img = mem::replace(&mut self.bg_image, None);
|
||||||
|
if let Some(bg_img) = bg_img {
|
||||||
|
drop(bg_img);
|
||||||
|
}
|
||||||
return Some(HomescreenMsg::Dismissed);
|
return Some(HomescreenMsg::Dismissed);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,43 +360,46 @@ impl Component for Lockscreen<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
|
||||||
|
const OVERLAY_SIZE: i16 = 170;
|
||||||
|
const OVERLAY_BORDER: i16 = (AREA.height() - OVERLAY_SIZE) / 2;
|
||||||
|
const OVERLAY_OFFSET: i16 = 9;
|
||||||
|
|
||||||
let center = AREA.center();
|
let center = AREA.center();
|
||||||
|
|
||||||
match ImageInfo::parse(self.image) {
|
// shape::RawImage::new(AREA, self.bg_image.view())
|
||||||
ImageInfo::Jpeg(_) => shape::JpegImage::new_image(center, self.image)
|
// .render(target);
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
.with_blur(4)
|
|
||||||
.with_dim(102)
|
|
||||||
.render(target),
|
|
||||||
ImageInfo::Toif(_) => shape::ToifImage::new_image(center, self.image)
|
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
//.with_blur(5)
|
|
||||||
.render(target),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
shape::ToifImage::new(center, ICON_LOCKSCREEN_FILTER.toif)
|
cshape::UnlockOverlay::new(center + Offset::y(OVERLAY_OFFSET), self.anim.eval())
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
.with_fg(Color::black())
|
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
shape::ToifImage::new(center + Offset::y(12), ICON_CENTRAL_CIRCLE.toif)
|
shape::Bar::new(AREA.split_top(OVERLAY_BORDER + OVERLAY_OFFSET).0)
|
||||||
.with_align(Alignment2D::CENTER)
|
.with_bg(Color::black())
|
||||||
.with_fg(GREEN_LIGHT)
|
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
shape::ToifImage::new(center + Offset::y(12), ICON_KEY.toif)
|
shape::Bar::new(AREA.split_bottom(OVERLAY_BORDER - OVERLAY_OFFSET).1)
|
||||||
|
.with_bg(Color::black())
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Bar::new(AREA.split_left(OVERLAY_BORDER).0)
|
||||||
|
.with_bg(Color::black())
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Bar::new(AREA.split_right(OVERLAY_BORDER).1)
|
||||||
|
.with_bg(Color::black())
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(center + Offset::y(OVERLAY_OFFSET), ICON_KEY.toif)
|
||||||
.with_align(Alignment2D::CENTER)
|
.with_align(Alignment2D::CENTER)
|
||||||
.with_fg(GREY_LIGHT)
|
.with_fg(GREY_LIGHT)
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
let (locked, tap) = if self.bootscreen {
|
let (locked, tap) = if self.bootscreen {
|
||||||
(
|
(
|
||||||
TR::lockscreen__title_not_connected,
|
Some(TR::lockscreen__title_not_connected),
|
||||||
TR::lockscreen__tap_to_connect,
|
TR::lockscreen__tap_to_connect,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(TR::lockscreen__title_locked, TR::lockscreen__tap_to_unlock)
|
(None, TR::lockscreen__tap_to_unlock)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut label_style = theme::TEXT_DEMIBOLD;
|
let mut label_style = theme::TEXT_DEMIBOLD;
|
||||||
@ -367,16 +420,18 @@ impl Component for Lockscreen<'_> {
|
|||||||
|
|
||||||
offset += 6;
|
offset += 6;
|
||||||
|
|
||||||
locked.map_translated(|t| {
|
if let Some(t) = locked {
|
||||||
offset += theme::TEXT_SUB_GREY.text_font.visible_text_height(t);
|
t.map_translated(|t| {
|
||||||
|
offset += theme::TEXT_SUB_GREY.text_font.visible_text_height(t);
|
||||||
|
|
||||||
let text_pos = Point::new(0, offset);
|
let text_pos = Point::new(0, offset);
|
||||||
|
|
||||||
shape::Text::new(text_pos, t)
|
shape::Text::new(text_pos, t)
|
||||||
.with_font(theme::TEXT_SUB_GREY.text_font)
|
.with_font(theme::TEXT_SUB_GREY.text_font)
|
||||||
.with_fg(theme::TEXT_SUB_GREY.text_color)
|
.with_fg(theme::TEXT_SUB_GREY.text_color)
|
||||||
.render(target);
|
.render(target);
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
tap.map_translated(|t| {
|
tap.map_translated(|t| {
|
||||||
offset = theme::TEXT_SUB_GREY.text_font.text_baseline();
|
offset = theme::TEXT_SUB_GREY.text_font.text_baseline();
|
||||||
@ -398,26 +453,20 @@ impl Component for Lockscreen<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_homescreen_format(image: BinaryData, accept_toif: bool) -> bool {
|
pub fn check_homescreen_format(image: BinaryData) -> bool {
|
||||||
match ImageInfo::parse(image) {
|
match ImageInfo::parse(image) {
|
||||||
ImageInfo::Jpeg(info) => {
|
ImageInfo::Jpeg(info) => {
|
||||||
info.width() == HOMESCREEN_IMAGE_WIDTH
|
info.width() == HOMESCREEN_IMAGE_WIDTH
|
||||||
&& info.height() == HOMESCREEN_IMAGE_HEIGHT
|
&& info.height() == HOMESCREEN_IMAGE_HEIGHT
|
||||||
&& info.mcu_height() <= 16
|
&& info.mcu_height() <= 16
|
||||||
}
|
}
|
||||||
ImageInfo::Toif(info) => {
|
|
||||||
accept_toif
|
|
||||||
&& info.width() == HOMESCREEN_TOIF_SIZE
|
|
||||||
&& info.height() == HOMESCREEN_TOIF_SIZE
|
|
||||||
&& info.format() == ToifFormat::FullColorBE
|
|
||||||
}
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_homescreen_image() -> BinaryData<'static> {
|
fn get_homescreen_image() -> BinaryData<'static> {
|
||||||
if let Ok(image) = get_user_custom_image() {
|
if let Ok(image) = get_user_custom_image() {
|
||||||
if check_homescreen_format(image, true) {
|
if check_homescreen_format(image) {
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -592,7 +592,7 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m
|
|||||||
jpeg = theme::IMAGE_HOMESCREEN.into();
|
jpeg = theme::IMAGE_HOMESCREEN.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !check_homescreen_format(jpeg, false) {
|
if !check_homescreen_format(jpeg) {
|
||||||
return Err(value_error!("Invalid image."));
|
return Err(value_error!("Invalid image."));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1418,7 +1418,7 @@ extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut
|
|||||||
pub extern "C" fn upy_check_homescreen_format(data: Obj) -> Obj {
|
pub extern "C" fn upy_check_homescreen_format(data: Obj) -> Obj {
|
||||||
let block = || {
|
let block = || {
|
||||||
let buffer = data.try_into()?;
|
let buffer = data.try_into()?;
|
||||||
Ok(check_homescreen_format(buffer, false).into())
|
Ok(check_homescreen_format(buffer).into())
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe { util::try_or_raise(block) }
|
unsafe { util::try_or_raise(block) }
|
||||||
|
@ -7,6 +7,7 @@ pub mod component;
|
|||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
||||||
|
pub mod cshape;
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
pub mod flow;
|
pub mod flow;
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
|
@ -4,8 +4,8 @@ use crate::ui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Size of image buffer in bytes
|
/// Size of image buffer in bytes
|
||||||
/// (up to 180x180 pixel, 16-bit RGB565 image)
|
/// (up to 240x240 pixel, 16-bit RGB565 image)
|
||||||
const IMAGE_BUFFER_SIZE: usize = 64 * 1024;
|
const IMAGE_BUFFER_SIZE: usize = 240 * 240 * 2;
|
||||||
|
|
||||||
#[repr(align(16))]
|
#[repr(align(16))]
|
||||||
struct AlignedBuffer {
|
struct AlignedBuffer {
|
||||||
|
@ -49,7 +49,6 @@ async def bootscreen() -> None:
|
|||||||
Any non-PIN loaders are ignored during this function.
|
Any non-PIN loaders are ignored during this function.
|
||||||
Allowing all of them before returning.
|
Allowing all of them before returning.
|
||||||
"""
|
"""
|
||||||
lockscreen = Lockscreen(label=storage.device.get_label(), bootscreen=True)
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
@ -59,7 +58,9 @@ async def bootscreen() -> None:
|
|||||||
ui.display.orientation(storage.device.get_rotation())
|
ui.display.orientation(storage.device.get_rotation())
|
||||||
if utils.USE_HAPTIC:
|
if utils.USE_HAPTIC:
|
||||||
io.haptic.haptic_set_enabled(storage.device.get_haptic_feedback())
|
io.haptic.haptic_set_enabled(storage.device.get_haptic_feedback())
|
||||||
|
lockscreen = Lockscreen(
|
||||||
|
label=storage.device.get_label(), bootscreen=True
|
||||||
|
)
|
||||||
await lockscreen
|
await lockscreen
|
||||||
await verify_user_pin()
|
await verify_user_pin()
|
||||||
storage.init_unlocked()
|
storage.init_unlocked()
|
||||||
|
Loading…
Reference in New Issue
Block a user