From a34c2cb9ddbbbfe1f4dd2c192c04a262e03645e1 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Wed, 22 May 2024 22:19:50 +0200 Subject: [PATCH] feat(core): animated lockscreen for mercury UI [no changelog] --- core/embed/firmware/memory_T3T1.ld | 10 +- .../model_mercury/component/homescreen/mod.rs | 149 ++++++++++++------ .../embed/rust/src/ui/model_mercury/layout.rs | 4 +- core/embed/rust/src/ui/model_mercury/mod.rs | 1 + .../embed/rust/src/ui/shape/utils/imagebuf.rs | 4 +- core/src/boot.py | 5 +- 6 files changed, 112 insertions(+), 61 deletions(-) diff --git a/core/embed/firmware/memory_T3T1.ld b/core/embed/firmware/memory_T3T1.ld index d7538891bf..98b24ae4d3 100644 --- a/core/embed/firmware/memory_T3T1.ld +++ b/core/embed/firmware/memory_T3T1.ld @@ -87,11 +87,6 @@ SECTIONS { . = ALIGN(4); } >SRAM1 - .buf : ALIGN(4) { - *(.buf*); - . = ALIGN(4); - } >SRAM1 - .stack : ALIGN(8) { . = 16K; /* Overflow causes UsageFault */ } >SRAM2 @@ -110,6 +105,11 @@ SECTIONS { . = ALIGN(4); } >SRAM3 + .buf : ALIGN(4) { + *(.buf*); + . = ALIGN(4); + } >SRAM3 + .heap : ALIGN(4) { . = 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 */ diff --git a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs index 7575110641..e5d71bd87d 100644 --- a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs @@ -1,16 +1,12 @@ use crate::{ io::BinaryData, strutil::TString, - time::{Duration, Instant}, + time::{Duration, Instant, Stopwatch}, translations::TR, trezorhal::usb::usb_configured, ui::{ component::{Component, Event, EventCtx, TimerToken}, - display::{ - image::{ImageInfo, ToifFormat}, - toif::Icon, - Color, Font, - }, + display::{image::ImageInfo, toif::Icon, Color, Font}, event::{TouchEvent, USBEvent}, geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, layout::util::get_user_custom_image, @@ -18,12 +14,16 @@ use crate::{ shape::{self, Renderer}, }, }; +use core::mem; use crate::ui::{ constant::{screen, HEIGHT, WIDTH}, - model_mercury::theme::{ - GREEN_LIGHT, GREY_LIGHT, ICON_CENTRAL_CIRCLE, ICON_KEY, ICON_LOCKSCREEN_FILTER, + model_mercury::{ + cshape, + theme::{GREY_LIGHT, ICON_KEY}, }, + shape::{render_on_canvas, ImageBuffer, Rgb565Canvas}, + util::animation_disabled, }; use super::{theme, Loader, LoaderMsg}; @@ -194,9 +194,6 @@ impl Component for Homescreen { ImageInfo::Jpeg(_) => shape::JpegImage::new_image(AREA.center(), self.image) .with_align(Alignment2D::CENTER) .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> { + anim: LockscreenAnim, label: TString<'a>, image: BinaryData<'static>, bootscreen: bool, coinjoin_authorized: bool, + bg_image: ImageBuffer>, } impl<'a> Lockscreen<'a> { 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 { + anim: LockscreenAnim::default(), label, - image: get_homescreen_image(), + image, bootscreen, coinjoin_authorized, + bg_image: buf, } } } @@ -298,10 +329,29 @@ impl Component for Lockscreen<'_> { bounds } - fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + 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 { + let bg_img = mem::replace(&mut self.bg_image, None); + if let Some(bg_img) = bg_img { + drop(bg_img); + } return Some(HomescreenMsg::Dismissed); } + None } @@ -310,43 +360,46 @@ impl Component for Lockscreen<'_> { } 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(); - match ImageInfo::parse(self.image) { - ImageInfo::Jpeg(_) => shape::JpegImage::new_image(center, self.image) - .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::RawImage::new(AREA, self.bg_image.view()) + // .render(target); - shape::ToifImage::new(center, ICON_LOCKSCREEN_FILTER.toif) - .with_align(Alignment2D::CENTER) - .with_fg(Color::black()) + cshape::UnlockOverlay::new(center + Offset::y(OVERLAY_OFFSET), self.anim.eval()) .render(target); - shape::ToifImage::new(center + Offset::y(12), ICON_CENTRAL_CIRCLE.toif) - .with_align(Alignment2D::CENTER) - .with_fg(GREEN_LIGHT) + shape::Bar::new(AREA.split_top(OVERLAY_BORDER + OVERLAY_OFFSET).0) + .with_bg(Color::black()) .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_fg(GREY_LIGHT) .render(target); let (locked, tap) = if self.bootscreen { ( - TR::lockscreen__title_not_connected, + Some(TR::lockscreen__title_not_connected), TR::lockscreen__tap_to_connect, ) } else { - (TR::lockscreen__title_locked, TR::lockscreen__tap_to_unlock) + (None, TR::lockscreen__tap_to_unlock) }; let mut label_style = theme::TEXT_DEMIBOLD; @@ -367,16 +420,18 @@ impl Component for Lockscreen<'_> { offset += 6; - locked.map_translated(|t| { - offset += theme::TEXT_SUB_GREY.text_font.visible_text_height(t); + if let Some(t) = locked { + 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) - .with_font(theme::TEXT_SUB_GREY.text_font) - .with_fg(theme::TEXT_SUB_GREY.text_color) - .render(target); - }); + shape::Text::new(text_pos, t) + .with_font(theme::TEXT_SUB_GREY.text_font) + .with_fg(theme::TEXT_SUB_GREY.text_color) + .render(target); + }) + }; tap.map_translated(|t| { 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) { ImageInfo::Jpeg(info) => { info.width() == HOMESCREEN_IMAGE_WIDTH && info.height() == HOMESCREEN_IMAGE_HEIGHT && info.mcu_height() <= 16 } - ImageInfo::Toif(info) => { - accept_toif - && info.width() == HOMESCREEN_TOIF_SIZE - && info.height() == HOMESCREEN_TOIF_SIZE - && info.format() == ToifFormat::FullColorBE - } _ => false, } } fn get_homescreen_image() -> BinaryData<'static> { if let Ok(image) = get_user_custom_image() { - if check_homescreen_format(image, true) { + if check_homescreen_format(image) { return image; } } diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index f51e4d7268..d1306c9eff 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -592,7 +592,7 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m jpeg = theme::IMAGE_HOMESCREEN.into(); } - if !check_homescreen_format(jpeg, false) { + if !check_homescreen_format(jpeg) { 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 { let block = || { let buffer = data.try_into()?; - Ok(check_homescreen_format(buffer, false).into()) + Ok(check_homescreen_format(buffer).into()) }; unsafe { util::try_or_raise(block) } diff --git a/core/embed/rust/src/ui/model_mercury/mod.rs b/core/embed/rust/src/ui/model_mercury/mod.rs index c73ee96a77..10376795af 100644 --- a/core/embed/rust/src/ui/model_mercury/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/mod.rs @@ -7,6 +7,7 @@ pub mod component; pub mod constant; pub mod theme; +pub mod cshape; #[cfg(feature = "micropython")] pub mod flow; #[cfg(feature = "micropython")] diff --git a/core/embed/rust/src/ui/shape/utils/imagebuf.rs b/core/embed/rust/src/ui/shape/utils/imagebuf.rs index ee93865475..cfbc824c87 100644 --- a/core/embed/rust/src/ui/shape/utils/imagebuf.rs +++ b/core/embed/rust/src/ui/shape/utils/imagebuf.rs @@ -4,8 +4,8 @@ use crate::ui::{ }; /// Size of image buffer in bytes -/// (up to 180x180 pixel, 16-bit RGB565 image) -const IMAGE_BUFFER_SIZE: usize = 64 * 1024; +/// (up to 240x240 pixel, 16-bit RGB565 image) +const IMAGE_BUFFER_SIZE: usize = 240 * 240 * 2; #[repr(align(16))] struct AlignedBuffer { diff --git a/core/src/boot.py b/core/src/boot.py index cb7a8fd88e..f606da3dcd 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -49,7 +49,6 @@ async def bootscreen() -> None: Any non-PIN loaders are ignored during this function. Allowing all of them before returning. """ - lockscreen = Lockscreen(label=storage.device.get_label(), bootscreen=True) while True: try: @@ -59,7 +58,9 @@ async def bootscreen() -> None: ui.display.orientation(storage.device.get_rotation()) if utils.USE_HAPTIC: io.haptic.haptic_set_enabled(storage.device.get_haptic_feedback()) - + lockscreen = Lockscreen( + label=storage.device.get_label(), bootscreen=True + ) await lockscreen await verify_user_pin() storage.init_unlocked()