From 6b5975ad4b3d85015c48a7a74976b2ad198f7d1d Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sun, 23 Mar 2025 23:55:05 +0100 Subject: [PATCH] feat(eckhart): default homescreen --- .../ui/layout_eckhart/firmware/homescreen.rs | 154 +++++++++++++++--- .../res/defaut_homescreen/hs_tile1.png | Bin 0 -> 1324 bytes .../res/defaut_homescreen/hs_tile1.toif | Bin 0 -> 259 bytes .../res/defaut_homescreen/hs_tile2.png | Bin 0 -> 987 bytes .../res/defaut_homescreen/hs_tile2.toif | Bin 0 -> 239 bytes .../rust/src/ui/layout_eckhart/theme/mod.rs | 10 ++ 6 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.png create mode 100644 core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile1.toif create mode 100644 core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.png create mode 100644 core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/hs_tile2.toif 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 d68315f23b..b5a53158f8 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,22 @@ 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, Pager}, + 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}, + theme::{self, firmware::button_homebar_style, BG, BLACK, GREY_EXTRA_DARK}, ActionBar, ActionBarMsg, Hint, HoldToConfirmAnim, }; @@ -30,6 +34,8 @@ 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 @@ -57,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_severe(text)) + led_color = Some(theme::RED); + hint = Some(Hint::new_warning_severe(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 { @@ -94,8 +110,7 @@ impl Homescreen { Some( HoldToConfirmAnim::new() .with_color(theme::GREY_LIGHT) - .with_duration(lock_duration) - .with_header_overlay(TR::progress__locking_device.into()), + .with_duration(lock_duration), ) } else { None @@ -106,6 +121,7 @@ impl Homescreen { hint, action_bar: ActionBar::new_single(button), image, + led_color, lockable, locked, virtual_locking_button: Button::empty().with_long_press(lock_duration), @@ -202,7 +218,7 @@ 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); @@ -268,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/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 0000000000000000000000000000000000000000..da3303b52bd9ab438112cbb9752442ca9f83a51e GIT binary patch literal 1324 zcmX|BX;70_6un=9goFkV=s++Oiii*-Dw|5CE)`Od z7zl(-R4M@?HY`D)vIs>4REAh$lT>VF3Q_ZIun_#9Hy(O)@Sedggm>LP@*Lq77RM|G!CER7ujotg@KWM1^N8j zO&g}`hne`*B5mVVQL)7+cZOx}B7aJHP+Dl{K5Zg=rS_RJb$B2n3Ys3KwyiNYH!qoQ z6390N1({!}s*=B3T4wmGCMGyzIi&7GzI%cK1Jez+)^s;FEAp;P^A#Ejh4QY(yQQs7 z`Qy@BRmwbf!C)|OV+=Z7>V+=*qC8G(-0*;{E=Q5o5W3`_{RcVbviNR-vsY9;ju$3O_ZJzwuX(R*yOw-)WC>Gyd9nRr z{aK79#7{9%hbK6msMH$z>j|iz6T+)Erf%5&ezOror0n~gE0s>!?Zt9I24CK*eybb! z`kPDg*TJ#1#aS;jZAsiDXBU{m_C$PmuAr0!411oSl!;J%K`=DxuDO?xygNBKdt8?g zTccovc4KyQ(JRwT%5)Yr;n&2S<;km0Vq5w*T7u*twDia0p@_u8w;vxMF)FrNb(}4*l-|h=jxTx2t=mI<#z*Rc?)8irG z3C`h917vbxf*YFp%l64Wy3#x44~yuDb%P^g*RA=+JtL}H?tG*FTi8;G;pM{*{qaW$ z;0B(SLd9obff}gf}@srAyiv9KJvH3@{Y#{vOEPo=h1{Qo{BQ) zSqn~H2yZQH*=I+z1vd;Dm@gY7z?RCfAUlW@#;Oq>s~4FZKL(C547BOWq#Ya<-gKGh z2hQ28xqU>)duGJwke~8*6e<1L^(r&ye`(Bq_5m@}=#~4$hynY=3YovqI*xv70Fj&N KVT~c|@BRUBojV=? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f631aead9c3e58b11440fc9688fa80878e455db1 GIT binary patch literal 259 zcmWIX_jHeEh-diDz`*eIl499m0}+>t`M>$~Jt{gYJuA5OG+4~~o4<5h470JVfXb&M zCFduqEwWJF*LN%U$aY(|KUeJDCcZB+d1O?vBg}!#o&^D-~_{2nAkqag4k?kfw_#z z%qBoJ4QCi;GG0E_@H$Gm;RVkOi3RLn<0eQNtmC<{-p!$eO@(=j0Z3g#u04lD9M6sW z9E=wwC-7W21k`B6yvZP;!stQCti~5S9&A_sGAww<&Tzc$R%5dL+XMfvzICXP_`wVS Dby#Vr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..75214d31626572f6b51c4d39f13f2d0688f957ee GIT binary patch literal 987 zcmeAS@N?(olHy`uVBq!ia0vp^@gU5>1|<7@q`v|wmUKs7M+SzC{oH>NS%G}U;vjb? z#+xT~>VX`EWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEp zc6VX;4}uH!E}sk(;VkfoEM{Qf76xHPhFNnYfP(BLp1!W^FPUZdL^O{T3Ft8}Fw1+o zIEGZ*dVBX^u9Blb>%;wAh3YI$3amyMl}B#A_Q-KhdVhA3VG-Ms2Ldup!VKRU&d$1d z&|3FviQ>m!7FA9!tb0P%v!>R}n{w&O-Ro&)Pv2L{_aFbfeEIV7sPv5yXSUsbD>dWG zNps&~UYl)l`*Y1^|9t)WwYznFX=!P8TKU@J`{U!{>Mj=U`jl3-KSuBUu1~&~Z5Z6= zZ@v9CFKl(_Z|g}xvwGYXn;QK7_T%u~=jYF#&zJofH%;w_LDeVxAEkB1MBKmK^9=unfswQ!_g#D~;$rPG|; z`(*ww#(((wkkJQd0)yTUi66@{*Tn+a2a?z4xqmpkh2j6(G8X%ozmF^v4^D4xm)|9O zkU!p{tjxM;{pBxF2y@=}q<~b+?F1X7_G3qOht{r>J-j(s|RigxaKeHF;EvH9`>=y0Atg8Lr4 zj@JVD`00a(F58uX0kLrLZNVB@pikv&Ko%cf0dgdtoE^w9?d^Z9<3BKd_}~mw+W64F z)VfJtZr^5*hKh{6Wi0Xh|2X15ls@GD(X^hQ|M&eAPCEv>r6z?3k00oFTfBJxXVDth ziiENQ;x(}qKi&Y<%gY^Lm1pKx2L&Aeeqg+=Nssu@`Y?R;)x}>qxa$}yetf$nSQGn$ z8R*W#hwDNoKVW{q2#Qb5r7OAZIPU#j0kqq-!gzMwL+*V>HgABAWIu4Y&U_J2o#ob5 zo*xb$E?<`=1oTgRm?`HzIUAXBVBB!j16@#CaY5>x0hstP<n)@6!Bt-3d z^-6N?Bc04MbBy#2Q{<-YIhJ?Qa@)+WGyl#iJYRc6ZL`Pm4!@`qreDwWf0TXv`_zw% zJ@e20FnF9;ZQ;kfN8$d#Z)ty0jKgZ>HeWAD%Y=mv@W_#=P7{=xR2 z53g4^?mO_U?Je(dLz&|RzYqO5y#8a;pSF+g#{aTVHHROYf9A){9)4rH|Lh+&*vc$t kjGM52!`{T|!-YJ@R~A(N{W*P)LOhWBt?luT+y2$e0Qv`ke*gdg literal 0 HcmV?d00001 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