diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs index 4eb73eccf1..c0feb4c0d1 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs @@ -221,6 +221,11 @@ impl Button { self.area } + pub fn touch_area(&self) -> Rect { + self.touch_expand + .map_or(self.area, |expand| self.area.outset(expand)) + } + fn set(&mut self, ctx: &mut EventCtx, state: State) { if self.state != state { self.state = state; @@ -364,12 +369,7 @@ impl Component for Button { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - let touch_area = if let Some(expand) = self.touch_expand { - self.area.outset(expand) - } else { - self.area - }; - + let touch_area = self.touch_area(); match event { Event::Touch(TouchEvent::TouchStart(pos)) => { match self.state { diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 2421c0a702..06ade039d4 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -74,6 +74,7 @@ impl ComponentMsgObj for Homescreen { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()), + HomescreenMsg::Menu => Ok(INFO.as_obj()), } } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/action_bar.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/action_bar.rs index c06ec99809..7cdd78ffe2 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/action_bar.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/action_bar.rs @@ -124,6 +124,13 @@ impl ActionBar { self.right_button.set_expanded_touch_area(expand); } + pub fn touch_area(&self) -> Rect { + let right_area = self.right_button.touch_area(); + self.left_button + .as_ref() + .map_or(right_area, |left| right_area.union(left.touch_area())) + } + pub fn update(&mut self, new_pager: Pager) { // TODO: review `clone()` of `left_content`/`right_content` if let Mode::Double { pager } = &mut self.mode { @@ -347,9 +354,7 @@ impl Component for ActionBar { btn.render(target); } self.right_button.render(target); - if let Some(htc_anim) = &self.htc_anim { - htc_anim.render(target); - } + self.htc_anim.render(target); } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs index 878148d465..eeb8c4d1bf 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs @@ -5,18 +5,23 @@ use crate::{ translations::TR, ui::{ component::{text::TextStyle, Component, Event, EventCtx, Label, Never}, - display::image::ImageInfo, - geometry::{Insets, Offset, Rect}, + display::{image::ImageInfo, Color}, + geometry::{Alignment2D, Grid, Insets, Offset, Point, Rect}, layout::util::get_user_custom_image, + lerp::Lerp, shape::{self, Renderer}, + util::animation_disabled, }, }; use super::{ - super::{component::Button, fonts}, + super::{ + component::{Button, ButtonMsg}, + fonts, + }, constant::{HEIGHT, SCREEN, WIDTH}, - theme::{self, firmware::button_homebar_style, BLACK, GREEN_DARK, GREEN_EXTRA_DARK}, - ActionBar, ActionBarMsg, Hint, + theme::{self, firmware::button_homebar_style, BG, BLACK, GREY_EXTRA_DARK}, + ActionBar, ActionBarMsg, Hint, HoldToConfirmAnim, }; /// Full-screen component for the homescreen and lockscreen. @@ -29,14 +34,21 @@ pub struct Homescreen { action_bar: ActionBar, /// Background image image: Option>, + /// LED color + led_color: Option, /// Whether the PIN is set and device can be locked lockable: bool, /// Whether the homescreen is locked locked: bool, + /// Hold to lock button placed everywhere except the `action_bar` + virtual_locking_button: Button, + /// Hold to lock animation + htc_anim: Option, } pub enum HomescreenMsg { Dismissed, + Menu, } impl Homescreen { @@ -51,23 +63,33 @@ impl Homescreen { let image = get_homescreen_image(); // Notification + // TODO: better notification handling let mut notification_level = 4; - let hint = if let Some((text, level)) = notification { + let mut hint = None; + let mut led_color; + if let Some((text, level)) = notification { notification_level = level; if notification_level == 0 { - Some(Hint::new_warning_danger(text)) + led_color = Some(theme::RED); + hint = Some(Hint::new_warning_danger(text)); } else { - Some(Hint::new_instruction(text, Some(theme::ICON_INFO))) + led_color = Some(theme::YELLOW); + hint = Some(Hint::new_instruction(text, Some(theme::ICON_INFO))); } } else if locked && coinjoin_authorized { - Some(Hint::new_instruction_green( + led_color = Some(theme::GREEN_LIME); + hint = Some(Hint::new_instruction_green( TR::coinjoin__do_not_disconnect, Some(theme::ICON_INFO), - )) + )); } else { - None + led_color = Some(theme::GREY_LIGHT); }; + if locked { + led_color = None; + } + // ActionBar button let button_style = button_homebar_style(notification_level); let button = if bootscreen { @@ -81,15 +103,70 @@ impl Homescreen { Button::with_homebar_content(None).styled(button_style) }; + let lock_duration = theme::LOCK_HOLD_DURATION; + + // Locking animation + let htc_anim = if lockable && !animation_disabled() { + Some( + HoldToConfirmAnim::new() + .with_color(theme::GREY_LIGHT) + .with_duration(lock_duration), + ) + } else { + None + }; + Ok(Self { label: HomeLabel::new(label), hint, action_bar: ActionBar::new_single(button), image, + led_color, lockable, locked, + virtual_locking_button: Button::empty().with_long_press(lock_duration), + htc_anim, }) } + + fn event_hold(&mut self, ctx: &mut EventCtx, event: Event) -> bool { + self.htc_anim.event(ctx, event); + if let Some(msg) = self.virtual_locking_button.event(ctx, event) { + match msg { + ButtonMsg::Pressed => { + if let Some(htc_anim) = &mut self.htc_anim { + htc_anim.start(); + ctx.request_anim_frame(); + ctx.request_paint(); + ctx.disable_swipe(); + } + } + ButtonMsg::Clicked => { + if let Some(htc_anim) = &mut self.htc_anim { + htc_anim.stop(); + ctx.request_anim_frame(); + ctx.request_paint(); + ctx.enable_swipe(); + } else { + // Animations disabled + return true; + } + } + ButtonMsg::Released => { + if let Some(htc_anim) = &mut self.htc_anim { + htc_anim.stop(); + ctx.request_anim_frame(); + ctx.request_paint(); + ctx.enable_swipe(); + } + } + ButtonMsg::LongPressed => { + return true; + } + } + } + false + } } impl Component for Homescreen { @@ -114,6 +191,9 @@ impl Component for Homescreen { self.label.place(label_area); self.action_bar.place(bar_area); + // Locking button is placed everywhere except the action bar + let locking_area = bounds.inset(Insets::bottom(self.action_bar.touch_area().height())); + self.virtual_locking_button.place(locking_area); bounds } @@ -122,13 +202,14 @@ impl Component for Homescreen { if self.locked { return Some(HomescreenMsg::Dismissed); } else { - // TODO: Show menu and handle "lock" action differently - if self.lockable { - return Some(HomescreenMsg::Dismissed); - } + return Some(HomescreenMsg::Menu); } } - None + if self.lockable { + Self::event_hold(self, ctx, event).then_some(HomescreenMsg::Dismissed) + } else { + None + } } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { @@ -137,11 +218,12 @@ impl Component for Homescreen { shape::JpegImage::new_image(SCREEN.top_left(), image).render(target); } } else { - render_default_hs(target); + render_default_hs(target, self.led_color); } self.label.render(target); self.hint.render(target); self.action_bar.render(target); + self.htc_anim.render(target); } } @@ -202,20 +284,108 @@ pub fn check_homescreen_format(image: BinaryData) -> bool { } } -fn render_default_hs<'a>(target: &mut impl Renderer<'a>) { +fn render_default_hs<'a>(target: &mut impl Renderer<'a>, led_color: Option) { + const DEFAULT_HS_TILE_ROWS: usize = 4; + const DEFAULT_HS_TILE_COLS: usize = 4; + const DEFAULT_HS_AREA: Rect = SCREEN.inset(Insets::bottom(140)); + const DEFAULT_HS_GRID: Grid = + Grid::new(DEFAULT_HS_AREA, DEFAULT_HS_TILE_ROWS, DEFAULT_HS_TILE_COLS); + const DEFAULT_HS_TILES_2: [(usize, usize); 9] = [ + (0, 0), + (1, 0), + (1, 3), + (2, 3), + (3, 2), + (3, 3), + (4, 0), + (4, 2), + (4, 3), + ]; + + // Layer 1: Base Solid Colour shape::Bar::new(SCREEN) - .with_fg(theme::BG) - .with_bg(theme::BG) + .with_bg(GREY_EXTRA_DARK) .render(target); - shape::Circle::new(SCREEN.center(), 48) - .with_fg(GREEN_DARK) - .with_thickness(4) - .render(target); - shape::Circle::new(SCREEN.center(), 42) - .with_fg(GREEN_EXTRA_DARK) - .with_thickness(4) - .render(target); + // Layer 2: Base Gradient overlay + for y in SCREEN.y0..SCREEN.y1 { + let slice = Rect::new(Point::new(SCREEN.x0, y), Point::new(SCREEN.x1, y + 1)); + let factor = (y - SCREEN.y0) as f32 / SCREEN.height() as f32; + shape::Bar::new(slice) + .with_bg(BG) + .with_alpha(u8::lerp(u8::MIN, u8::MAX, factor)) + .render(target); + } + + // Layer 3: (Optional) LED lightning simulation + if let Some(color) = led_color { + render_led_simulation(color, target); + } + + // Layer 4: Tile pattern + // TODO: improve frame rate + for row in 0..DEFAULT_HS_TILE_ROWS { + for col in 0..DEFAULT_HS_TILE_COLS { + let tile_area = DEFAULT_HS_GRID.row_col(row, col); + let icon = if DEFAULT_HS_TILES_2.contains(&(row, col)) { + theme::ICON_HS_TILE_2.toif + } else { + theme::ICON_HS_TILE_1.toif + }; + shape::ToifImage::new(tile_area.top_left(), icon) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(BLACK) + .render(target); + } + } +} + +fn render_led_simulation<'a>(color: Color, target: &mut impl Renderer<'a>) { + const Y_MAX: i16 = SCREEN.y1 - theme::ACTION_BAR_HEIGHT; + const Y_RANGE: i16 = Y_MAX - SCREEN.y0; + + const X_MID: i16 = SCREEN.x0 + SCREEN.width() / 2; + const X_HALF_WIDTH: f32 = (SCREEN.width() / 2) as f32; + + // Vertical gradient (color intensity fading from bottom to top) + #[allow(clippy::reversed_empty_ranges)] // clippy fails here for T3B1 which has smaller screen + for y in SCREEN.y0..Y_MAX { + let factor = (y - SCREEN.y0) as f32 / Y_RANGE as f32; + let slice = Rect::new(Point::new(SCREEN.x0, y), Point::new(SCREEN.x1, y + 1)); + + // Gradient 1 (Overall intensity: 35%) + // Stops: 0%, 40% + // Opacity: 100%, 20% + let factor_grad_1 = (factor / 0.4).clamp(0.2, 1.0); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(u8::lerp(89, u8::MIN, factor_grad_1)) + .render(target); + + // Gradient 2 (Overall intensity: 70%) + // Stops: 2%, 63% + // Opacity: 100%, 0% + let factor_grad_2 = ((factor - 0.02) / (0.63 - 0.02)).clamp(0.0, 1.0); + let alpha = u8::lerp(179, u8::MIN, factor_grad_2); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(alpha) + .render(target); + } + + // Horizontal gradient (transparency increasing toward center) + for x in SCREEN.x0..SCREEN.x1 { + const WIDTH: i16 = SCREEN.width(); + let slice = Rect::new(Point::new(x, SCREEN.y0), Point::new(x + 1, Y_MAX)); + // Gradient 3 + // Calculate distance from center as a normalized factor (0 at center, 1 at + // edges) + let dist_from_mid = (x - X_MID).abs() as f32 / X_HALF_WIDTH; + shape::Bar::new(slice) + .with_bg(BG) + .with_alpha(u8::lerp(u8::MIN, u8::MAX, dist_from_mid)) + .render(target); + } } fn get_homescreen_image() -> Option> { diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs index 4e68a5f663..99ae244bec 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs @@ -19,7 +19,7 @@ type VerticalMenuButtons = Vec; pub struct VerticalMenu { /// Bounds the sliding window of the menu. bounds: Rect, - /// FUll bounds of the menu, including off-screen items. + /// Full bounds of the menu, including off-screen items. virtual_bounds: Rect, /// Menu items. buttons: VerticalMenuButtons, diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.png b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.png new file mode 100644 index 0000000000..da3303b52b Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.png differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.toif b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.toif new file mode 100644 index 0000000000..f631aead9c Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.toif differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.png b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.png new file mode 100644 index 0000000000..75214d3162 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.png differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.toif b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.toif new file mode 100644 index 0000000000..6be8fb92b0 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.toif differ diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs index 9617fbcd0e..0e8e60029f 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs @@ -14,6 +14,7 @@ use super::{ *, }; +pub const LOCK_HOLD_DURATION: Duration = Duration::from_millis(1500); pub const CONFIRM_HOLD_DURATION: Duration = Duration::from_millis(1500); pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500); diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs index 3b16f66b85..5e7f50c95b 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs @@ -103,6 +103,16 @@ include_icon!(ICON_BORDER_TR, "layout_eckhart/res/border/TR.toif"); include_icon!(ICON_PLUS, "layout_eckhart/res/plus.toif"); include_icon!(ICON_MINUS, "layout_eckhart/res/minus.toif"); +// Icon tiles for default homescreen +include_icon!( + ICON_HS_TILE_1, + "layout_eckhart/res/defaut_homescreen/hs_tile1.toif" +); +include_icon!( + ICON_HS_TILE_2, + "layout_eckhart/res/defaut_homescreen/hs_tile2.toif" +); + // Common text styles and button styles must use fonts accessible from both // bootloader and firmware diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py index 08bd648605..d0c1e21e80 100644 --- a/core/src/trezor/ui/layouts/homescreen.py +++ b/core/src/trezor/ui/layouts/homescreen.py @@ -56,8 +56,8 @@ class HomescreenBase(ui.Layout): if not self.should_resume: super()._first_paint() storage_cache.homescreen_shown = self.RENDER_INDICATOR - # else: - # self._paint() + else: + self._paint() class Homescreen(HomescreenBase):