From b55bf905646d205251952d565ac5ce01ab1c891d Mon Sep 17 00:00:00 2001 From: obrusvit Date: Fri, 11 Jul 2025 14:53:24 +0200 Subject: [PATCH] feat(eckhart): introduce gradient module [no changelog] --- .../src/ui/layout_eckhart/component/button.rs | 55 +--- .../ui/layout_eckhart/firmware/action_bar.rs | 13 +- .../ui/layout_eckhart/firmware/homescreen.rs | 134 +++------ .../ui/layout_eckhart/flow/confirm_fido.rs | 6 +- .../ui/layout_eckhart/flow/confirm_output.rs | 9 +- .../ui/layout_eckhart/flow/confirm_reset.rs | 6 +- .../flow/confirm_set_new_pin.rs | 7 +- .../ui/layout_eckhart/flow/confirm_summary.rs | 9 +- .../layout_eckhart/flow/confirm_with_menu.rs | 7 +- .../flow/continue_recovery_homepage.rs | 6 +- .../ui/layout_eckhart/flow/prompt_backup.rs | 6 +- .../src/ui/layout_eckhart/flow/receive.rs | 14 +- .../layout_eckhart/flow/request_passphrase.rs | 2 +- .../layout_eckhart/flow/show_share_words.rs | 3 +- .../src/ui/layout_eckhart/theme/firmware.rs | 16 +- .../src/ui/layout_eckhart/theme/gradient.rs | 273 ++++++++++++++++++ .../rust/src/ui/layout_eckhart/theme/mod.rs | 9 +- .../rust/src/ui/layout_eckhart/ui_firmware.rs | 49 ++-- 18 files changed, 425 insertions(+), 199 deletions(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs 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 5bd0e8938e..749d3e752a 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs @@ -9,13 +9,12 @@ use crate::{ display::{toif::Icon, Color, Font}, event::TouchEvent, geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, - lerp::Lerp, shape::{self, Renderer}, util::split_two_lines, }, }; -use super::super::theme; +use super::super::theme::{self, Gradient}; pub enum ButtonMsg { Pressed, @@ -26,7 +25,7 @@ pub enum ButtonMsg { enum RadiusOrGradient { Radius(u8), - Gradient, + Gradient(Gradient), None, } @@ -170,11 +169,15 @@ impl Button { self } - pub fn with_gradient(mut self) -> Self { - self.radius_or_gradient = RadiusOrGradient::Gradient; + pub fn with_gradient(mut self, gradient: Gradient) -> Self { + self.radius_or_gradient = RadiusOrGradient::Gradient(gradient); self } + pub fn has_gradient(&self) -> bool { + matches!(self.radius_or_gradient, RadiusOrGradient::Gradient(_)) + } + pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) { if enabled { self.enable(ctx); @@ -322,44 +325,6 @@ impl Button { } } - fn render_gradient_bar<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) { - let height = self.area.height(); - let half_width = (self.area.width() / 2) as f32; - let x_mid = self.area.center().x; - - // Layer 1: Horizontal Gradient (Overall intensity: 100%) - // Stops: 21%, 100% - // Opacity: 100%, 20% - for y in self.area.y0..self.area.y1 { - let factor = (y - self.area.y0) as f32 / height as f32; - let slice = Rect::new(Point::new(self.area.x0, y), Point::new(self.area.x1, y + 1)); - let factor_grad = ((factor - 0.21) / (1.00 - 0.21)).clamp(0.0, 1.0); - let alpha = u8::lerp(u8::MAX, 51, factor_grad); - shape::Bar::new(slice) - .with_bg(style.button_color) - .with_alpha(alpha) - .render(target); - } - - // Layer 2: Vertical Gradient (Overall intensity: 100%) - // distance from mid - for x in self.area.x0..self.area.x1 { - let slice = Rect::new(Point::new(x, self.area.y0), Point::new(x + 1, self.area.y1)); - let dist_from_mid = (x - x_mid).abs() as f32 / half_width; - let alpha = u8::lerp(u8::MIN, u8::MAX, dist_from_mid); - shape::Bar::new(slice) - .with_bg(theme::BG) - .with_alpha(alpha) - .render(target); - } - - // Layer 3: Black overlay (Overall intensity: 20%) - shape::Bar::new(self.area) - .with_bg(theme::BG) - .with_alpha(51) - .render(target); - } - pub fn render_background<'s>( &self, target: &mut impl Renderer<'s>, @@ -377,8 +342,8 @@ impl Button { .render(target); } // Gradient bar is rendered only in `normal` state, not `active` or `disabled` - RadiusOrGradient::Gradient if self.state.is_normal() => { - self.render_gradient_bar(target, style); + RadiusOrGradient::Gradient(gradient) if self.state.is_normal() => { + gradient.render(target, self.area, 1); } _ => { shape::Bar::new(self.area) 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 a746c2ba7f..e6616f6346 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 @@ -72,12 +72,13 @@ impl ActionBar { /// component automatically shows navigation up/down buttons for /// paginated content. pub fn new_single(button: Button) -> Self { - let mut right_button = button - .with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH) - .with_gradient(); + let mut right_button = button.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH); if right_button.stylesheet() == &theme::button_default() { right_button = right_button.styled(theme::firmware::button_actionbar_right_default()); }; + if !right_button.has_gradient() { + right_button = right_button.with_gradient(theme::Gradient::DefaultGrey); + } Self::new(Mode::Single, None, Some(right_button), None) } @@ -104,8 +105,10 @@ impl ActionBar { .with_content_offset(Self::BUTTON_CONTENT_OFFSET); let mut right_button = right .with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH) - .with_content_offset(Self::BUTTON_CONTENT_OFFSET.neg()) - .with_gradient(); + .with_content_offset(Self::BUTTON_CONTENT_OFFSET.neg()); + if !right_button.has_gradient() { + right_button = right_button.with_gradient(theme::Gradient::DefaultGrey); + } if right_button.stylesheet() == &theme::button_default() { right_button = right_button.styled(theme::firmware::button_actionbar_right_default()); }; 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 2c70b9f1df..ae264bdc03 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs @@ -10,9 +10,8 @@ use crate::{ ui::{ component::{text::TextStyle, Component, Event, EventCtx, Label, Never}, display::{image::ImageInfo, Color}, - geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, + geometry::{Alignment, Alignment2D, Insets, Offset, Rect}, layout::util::get_user_custom_image, - lerp::Lerp, shape::{self, Renderer}, util::animation_disabled, }, @@ -24,7 +23,7 @@ use super::{ fonts, }, constant::{HEIGHT, SCREEN, WIDTH}, - theme::{self, firmware::button_homebar_style, TILES_GRID}, + theme::{self, firmware::button_homebar_style, Gradient, TILES_GRID}, ActionBar, ActionBarMsg, Hint, }; @@ -89,13 +88,16 @@ impl Homescreen { None => (None, None), }; + // Homebar + let (style_sheet, gradient) = button_homebar_style(notification_level); + let btn = Button::new(Self::homebar_content(bootscreen, locked)) + .styled(style_sheet) + .with_gradient(gradient); + Ok(Self { label: HomeLabel::new(label, shadow), hint, - action_bar: ActionBar::new_single( - Button::new(Self::homebar_content(bootscreen, locked)) - .styled(button_homebar_style(notification_level)), - ), + action_bar: ActionBar::new_single(btn), image, led_color, lockable, @@ -158,6 +160,37 @@ impl Homescreen { } false } + + fn render_default_hs<'s>(&self, target: &mut impl Renderer<'s>) { + // Layer 1: Base Solid Colour + shape::Bar::new(SCREEN) + .with_bg(theme::GREY_EXTRA_DARK) + .render(target); + + // Layer 2: Base Gradient overlay + Gradient::HomescreenBase.render(target, SCREEN, 1); + + // Layer 3: (Optional) LED lightning simulation + if let Some(led_color) = self.led_color { + let gradient_area = SCREEN.inset(Insets::bottom(theme::ACTION_BAR_HEIGHT)); + Gradient::HomescreenLEDSim(led_color).render(target, gradient_area, 1); + } + + // Layer 4: Tile pattern + // TODO: improve frame rate + for idx in 0..TILES_GRID.cell_count() { + let tile_area = TILES_GRID.cell(idx); + let icon = if theme::TILES_SLASH_INDICES.contains(&idx) { + theme::ICON_TILE_STRIPES_SLASH.toif + } else { + theme::ICON_TILE_STRIPES_BACKSLASH.toif + }; + shape::ToifImage::new(tile_area.top_left(), icon) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::BLACK) + .render(target); + } + } } impl Drop for Homescreen { @@ -219,7 +252,7 @@ impl Component for Homescreen { shape::JpegImage::new_image(SCREEN.top_left(), image).render(target); } } else { - render_default_hs(target, self.led_color); + self.render_default_hs(target); } self.label.render(target); self.hint.render(target); @@ -296,91 +329,6 @@ pub fn check_homescreen_format(image: BinaryData) -> bool { } } -fn render_default_hs<'a>(target: &mut impl Renderer<'a>, led_color: Option) { - // Layer 1: Base Solid Colour - shape::Bar::new(SCREEN) - .with_bg(theme::GREY_EXTRA_DARK) - .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(theme::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 idx in 0..TILES_GRID.cell_count() { - let tile_area = TILES_GRID.cell(idx); - let icon = if theme::TILES_SLASH_INDICES.contains(&idx) { - theme::ICON_TILE_STRIPES_SLASH.toif - } else { - theme::ICON_TILE_STRIPES_BACKSLASH.toif - }; - shape::ToifImage::new(tile_area.top_left(), icon) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(theme::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(theme::BG) - .with_alpha(u8::lerp(u8::MIN, u8::MAX, dist_from_mid)) - .render(target); - } -} - fn get_homescreen_image() -> Option> { if let Ok(image) = get_user_custom_image() { if check_homescreen_format(image) { diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_fido.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_fido.rs index cc87d356ca..48ebe334a8 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_fido.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_fido.rs @@ -22,7 +22,7 @@ use super::super::{ ActionBar, FidoCredential, Header, LongMenuGc, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -146,7 +146,9 @@ pub fn new_confirm_fido( ActionBar::new_cancel_confirm() } else { ActionBar::new_single( - Button::with_text(TR::words__authenticate.into()).styled(theme::button_confirm()), + Button::with_text(TR::words__authenticate.into()) + .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen), ) }; diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs index d09620781d..e1a87f83b9 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs @@ -26,7 +26,7 @@ use super::super::{ ActionBar, Header, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; const MENU_ITEM_CANCEL: usize = 0; @@ -200,7 +200,9 @@ fn content_cancel( .with_header(Header::new(TR::words__send.into())) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__cancel.into()).styled(theme::button_actionbar_danger()), + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert), )) .map(|msg| match msg { TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), @@ -409,7 +411,8 @@ pub fn new_confirm_output( Button::with_icon(theme::ICON_CHEVRON_UP), Button::with_text(TR::instructions__hold_to_sign.into()) .with_long_press(theme::CONFIRM_HOLD_DURATION) - .styled(theme::button_confirm()), + .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen), )) .map(|msg| match msg { TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_reset.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_reset.rs index 781ca70a57..4034f2d545 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_reset.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_reset.rs @@ -18,7 +18,8 @@ use super::super::{ ActionBar, Header, HeaderMsg, Hint, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - fonts, theme, + fonts, + theme::{self, gradient::Gradient}, }; #[derive(Copy, Clone, PartialEq, Eq)] @@ -70,7 +71,8 @@ pub fn new_confirm_reset(recovery: bool) -> Result { .with_action_bar(ActionBar::new_single( Button::with_text(TR::instructions__hold_to_continue.into()) .with_long_press(theme::CONFIRM_HOLD_DURATION) - .styled(theme::button_confirm()), + .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen), )) .with_hint(Hint::new_instruction( TR::reset__tos_link, diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_set_new_pin.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_set_new_pin.rs index c629598daa..ba859d8924 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_set_new_pin.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_set_new_pin.rs @@ -21,7 +21,7 @@ use crate::{ use super::super::{ component::Button, firmware::{ActionBar, Header, TextScreen}, - theme, + theme::{self, gradient::Gradient}, }; #[derive(Copy, Clone, PartialEq, Eq)] @@ -97,8 +97,9 @@ pub fn new_set_new_pin( ) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__continue.into()) - .styled(theme::button_actionbar_danger()), + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert), )) .map(|msg| match msg { TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs index 9b67f7af59..108dd670f5 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs @@ -23,7 +23,7 @@ use super::super::{ ActionBar, Header, Hint, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; const MENU_ITEM_CANCEL: usize = 0; @@ -121,7 +121,8 @@ pub fn new_confirm_summary( .with_action_bar(ActionBar::new_single( Button::with_text(TR::instructions__hold_to_sign.into()) .with_long_press(theme::CONFIRM_HOLD_DURATION) - .styled(theme::button_confirm()), + .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen), )) .with_hint(Hint::new_instruction( TR::send__send_in_the_app, @@ -190,7 +191,9 @@ pub fn new_confirm_summary( .with_header(Header::new(TR::words__send.into())) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__cancel.into()).styled(theme::button_actionbar_danger()), + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert), )) .map(|msg| match msg { TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_with_menu.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_with_menu.rs index 6194894544..c3338764e0 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_with_menu.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_with_menu.rs @@ -19,7 +19,7 @@ use super::super::{ ActionBar, AllowedTextContent, Header, Hint, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; const TIMEOUT_MS: u32 = 2000; @@ -71,10 +71,13 @@ pub fn new_confirm_with_menu( Button::with_text(verb) .with_long_press(theme::CONFIRM_HOLD_DURATION) .styled(theme::firmware::button_confirm()) + .with_gradient(Gradient::SignGreen) } else if let Some(verb) = verb { Button::with_text(verb) } else { - Button::with_text(TR::buttons__confirm.into()).styled(theme::firmware::button_confirm()) + Button::with_text(TR::buttons__confirm.into()) + .styled(theme::firmware::button_confirm()) + .with_gradient(Gradient::SignGreen) }; let mut value_screen = TextScreen::new(content) diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs b/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs index 04b3341e18..e8d808cb5c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs @@ -27,7 +27,7 @@ use super::super::{ ActionBar, Header, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; #[derive(Copy, Clone, PartialEq, Eq)] @@ -214,7 +214,9 @@ pub fn new_continue_recovery_homepage( .with_header(Header::new(cancel_title.into())) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__cancel.into()).styled(theme::button_actionbar_danger()), + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert), )) .map(|msg| match msg { TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/prompt_backup.rs b/core/embed/rust/src/ui/layout_eckhart/flow/prompt_backup.rs index bf21a7fdae..d2d0f72d3c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/prompt_backup.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/prompt_backup.rs @@ -21,7 +21,7 @@ use super::super::{ ActionBar, Header, HeaderMsg, Hint, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; #[derive(Copy, Clone, PartialEq, Eq)] @@ -103,7 +103,9 @@ pub fn new_prompt_backup() -> Result { ) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__skip.into()).styled(theme::button_cancel()), + Button::with_text(TR::buttons__skip.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert), )) .with_hint(Hint::new_instruction(TR::backup__not_recommend, None)) .map(|msg| match msg { diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs b/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs index 310f367b3c..f7b884fc12 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs @@ -27,7 +27,7 @@ use super::super::{ ActionBar, Header, HeaderMsg, Hint, QrScreen, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; const ITEM_PADDING: i16 = 16; @@ -115,9 +115,13 @@ pub fn new_receive( paragraphs.add(Paragraph::new(text_style, content)); let button = if hint.is_some() { - Button::with_text(TR::buttons__confirm.into()).styled(theme::button_actionbar_danger()) + Button::with_text(TR::buttons__confirm.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert) } else { - Button::with_text(TR::buttons__confirm.into()).styled(theme::button_confirm()) + Button::with_text(TR::buttons__confirm.into()) + .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen) }; let mut address_screen = TextScreen::new( @@ -229,7 +233,9 @@ pub fn new_receive( .with_header(Header::new(title)) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__cancel.into()).styled(theme::button_actionbar_danger()), + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::button_actionbar_danger()) + .with_gradient(theme::Gradient::Alert), )); if let Some(hint) = cancel_hint { screen_cancel_info = screen_cancel_info.with_hint(hint); diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs b/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs index e25a82b508..4077d3a323 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs @@ -70,7 +70,7 @@ pub fn new_request_passphrase() -> Result { .with_header(Header::new(TR::passphrase__title_enter.into())) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CHEVRON_LEFT), - Button::with_text(TR::buttons__confirm.into()).styled(theme::button_default()), + Button::with_text(TR::buttons__confirm.into()), )) .map(|msg| match msg { TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs b/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs index c2a9dbe04e..2c8d12e385 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs @@ -24,7 +24,7 @@ use super::super::{ ActionBar, Header, ShareWordsScreen, ShareWordsScreenMsg, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, }, - theme, + theme::{self, gradient::Gradient}, }; #[derive(Copy, Clone, PartialEq, Eq)] @@ -110,6 +110,7 @@ pub fn new_show_share_words_flow( Button::with_icon(theme::ICON_CHEVRON_UP), Button::with_text(TR::buttons__hold_to_confirm.into()) .styled(theme::button_confirm()) + .with_gradient(Gradient::SignGreen) .with_long_press(theme::CONFIRM_HOLD_DURATION), )) .map(|msg| match msg { 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 107f7c8bb3..1181603423 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs @@ -328,12 +328,12 @@ pub const fn menu_item_title_red() -> ButtonStyleSheet { } macro_rules! button_homebar_style { - ($button_color:expr, $icon_color:expr) => { + ($icon_color:expr) => { ButtonStyleSheet { normal: &ButtonStyle { font: fonts::FONT_SATOSHI_MEDIUM_26, text_color: GREY_LIGHT, - button_color: $button_color, + button_color: GREY_SUPER_DARK, icon_color: $icon_color, }, active: &ButtonStyle { @@ -352,14 +352,14 @@ macro_rules! button_homebar_style { } }; } -pub const fn button_homebar_style(notification_level: u8) -> ButtonStyleSheet { +pub const fn button_homebar_style(notification_level: u8) -> (ButtonStyleSheet, Gradient) { // NOTE: 0 is the highest severity. match notification_level { - 0 => button_homebar_style!(ORANGE_SUPER_DARK, RED), - 1 => button_homebar_style!(YELLOW_DARK, GREY_LIGHT), - 2 => button_homebar_style!(GREY_SUPER_DARK, GREY_LIGHT), - 3 => button_homebar_style!(GREEN_DARK, GREY_LIGHT), - _ => button_homebar_style!(GREY_EXTRA_DARK, GREY_LIGHT), + 0 => (button_homebar_style!(RED), Gradient::Alert), + 1 => (button_homebar_style!(GREY_LIGHT), Gradient::Warning), + 2 => (button_homebar_style!(GREY_LIGHT), Gradient::DefaultGrey), + 3 => (button_homebar_style!(GREY_LIGHT), Gradient::SignGreen), + _ => (button_homebar_style!(GREY_LIGHT), Gradient::DefaultGrey), } } diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs b/core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs new file mode 100644 index 0000000000..ff1763c267 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs @@ -0,0 +1,273 @@ +use crate::ui::{ + display::Color, + geometry::{Axis, Point, Rect}, + lerp::Lerp, + shape::{self, Renderer}, +}; + +use super::super::theme; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Gradient { + DefaultGrey, + #[cfg(feature = "micropython")] + SignGreen, + #[cfg(feature = "micropython")] + Warning, + #[cfg(feature = "micropython")] + Alert, + #[cfg(feature = "micropython")] + HomescreenBase, + #[cfg(feature = "micropython")] + HomescreenLEDSim(Color), +} + +impl Gradient { + /// Renders a gradient within the given rectangle + /// + /// # Arguments + /// * `target` - The renderer to draw to + /// * `area` - The rectangle to fill with gradient + /// * `step_size` - Step size for speed tuning (1 = full quality, higher = + /// faster but lower quality) + pub fn render<'s>(&self, target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + render_gradient(target, area, *self, step_size); + } +} + +fn render_gradient<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + gradient_type: Gradient, + step_size: u16, +) { + match gradient_type { + Gradient::DefaultGrey => render_default_grey(target, area, step_size), + #[cfg(feature = "micropython")] + Gradient::SignGreen => render_sign_green(target, area, step_size), + #[cfg(feature = "micropython")] + Gradient::Warning => render_warning(target, area, step_size), + #[cfg(feature = "micropython")] + Gradient::Alert => render_alert(target, area, step_size), + #[cfg(feature = "micropython")] + Gradient::HomescreenBase => render_homescreen_base(target, area, step_size), + #[cfg(feature = "micropython")] + Gradient::HomescreenLEDSim(color) => render_led_simulation(target, area, step_size, color), + } +} + +fn render_default_grey<'s>(target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + // Layer 1: Vertical Gradient (Overall intensity: 100%) + // Stops: 21%, 100% + // Opacity: 100%, 20% + // Color: color1, color2 + let color1 = theme::GREY_EXTRA_DARK; + let color2 = theme::GREEN_DARK; + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + let factor = normalize_factor(factor, 0.21, 1.00); + let color = Color::lerp(color1, color2, factor); + let alpha = u8::lerp(u8::MAX, 51, factor); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(alpha) + .render(target); + } + render_edge_fade(target, area, theme::BLACK, step_size); + render_black_overlay(target, area); +} + +#[cfg(feature = "micropython")] +fn render_sign_green<'s>(target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + // Layer 1: Vertical Gradient (Overall intensity: 100%) + // Stops: 21%, 100% + // Opacity: 100%, 20% + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + let factor = normalize_factor(factor, 0.21, 1.00); + let alpha = u8::lerp(u8::MAX, 51, factor); + shape::Bar::new(slice) + .with_bg(theme::GREEN_DARK) + .with_alpha(alpha) + .render(target); + } + + render_edge_fade(target, area, theme::BLACK, step_size); + render_black_overlay(target, area); +} + +#[cfg(feature = "micropython")] +fn render_warning<'s>(target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + // Layer 1: Vertical Gradient (Overall intensity: 100%) + // Stops: 21%, 100% + // Opacity: 100%, 20% + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + let factor = normalize_factor(factor, 0.21, 1.00); + let alpha = u8::lerp(u8::MAX, 51, factor); + shape::Bar::new(slice) + .with_bg(theme::YELLOW_DARK) + .with_alpha(alpha) + .render(target); + } + + render_edge_fade(target, area, theme::YELLOW_DARK, step_size); + render_black_overlay(target, area); +} + +#[cfg(feature = "micropython")] +fn render_alert<'s>(target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + render_alert_horizontal(target, area, theme::ORANGE_SUPER_DARK, step_size); + // Layer 2 Vertical Gradient (Overall intensity: 100%) + // Stops: 85%, 100% + // Opacity: 20%, 100% + let color1 = theme::ORANGE_SUPER_DARK; + let color2 = theme::BLACK; + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + let factor = normalize_factor(factor, 0.80, 1.00); + let alpha = u8::lerp(51, u8::MAX, factor); + let color = Color::lerp(color1, color2, factor); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(alpha) + .render(target); + } + render_black_overlay(target, area); +} + +#[cfg(feature = "micropython")] +fn render_homescreen_base<'s>(target: &mut impl Renderer<'s>, area: Rect, step_size: u16) { + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + shape::Bar::new(slice) + .with_bg(theme::BG) + .with_alpha(u8::lerp(u8::MIN, u8::MAX, factor)) + .render(target); + } +} + +#[cfg(feature = "micropython")] +fn render_led_simulation<'a>( + target: &mut impl Renderer<'a>, + area: Rect, + step_size: u16, + color: Color, +) { + // Vertical gradient (color intensity fading from bottom to top) + for (slice, factor) in iter_slices(area, Axis::Vertical, step_size) { + // 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 = normalize_factor(factor, 0.02, 0.63); + 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 (slice, _) in iter_slices(area, Axis::Horizontal, step_size) { + // Calculate distance from center as a normalized factor (0 at center, 1 at + // edges) + let x_mid = area.center().x; + let x_half_width = (area.width() / 2) as f32; + let dist_from_mid = (slice.x0 - x_mid).abs() as f32 / x_half_width; + + shape::Bar::new(slice) + .with_bg(theme::BG) + .with_alpha(u8::lerp(u8::MIN, u8::MAX, dist_from_mid)) + .render(target); + } +} + +fn render_edge_fade<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + color_mid: Color, + step_size: u16, +) { + // Render horizontal distance-from-mid gradient + // Black at edges, color_mid at center with minimal opacity + let x_mid = area.center().x; + let half_width = (area.width() / 2) as f32; + for (slice, _) in iter_slices(area, Axis::Horizontal, step_size) { + let dist_from_mid = (slice.x0 - x_mid).abs() as f32 / half_width; + let alpha = u8::lerp(u8::MIN, u8::MAX, dist_from_mid); + let color = Color::lerp(color_mid, theme::BLACK, dist_from_mid); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(alpha) + .render(target); + } +} + +fn render_alert_horizontal<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + color_mid: Color, + step_size: u16, +) { + // Render horizontal distance-from-mid gradient + // Black at edges, color_mid at center with full opacity + let x_mid = area.center().x; + let half_width = (area.width() / 2) as f32; + for (slice, _) in iter_slices(area, Axis::Horizontal, step_size) { + let dist_from_mid = (slice.x0 - x_mid).abs() as f32 / half_width; + let color = Color::lerp(color_mid, theme::BLACK, dist_from_mid); + shape::Bar::new(slice) + .with_bg(color) + .with_alpha(u8::MAX) + .render(target); + } +} + +fn render_black_overlay<'s>(target: &mut impl Renderer<'s>, area: Rect) { + // Render a black overlay (Overall intensity: 20%) + shape::Bar::new(area) + .with_bg(theme::BG) + .with_alpha(51) // 20% opacity + .render(target); +} + +// Helper functions + +/// Normalizes a factor to the given range and clamps it to [0.0, 1.0] +fn normalize_factor(factor: f32, start: f32, end: f32) -> f32 { + ((factor - start) / (end - start)).clamp(0.0, 1.0) +} + +fn iter_slices(area: Rect, axis: Axis, step_size: u16) -> impl Iterator { + let (start, end) = match axis { + Axis::Horizontal => (area.x0, area.x1), + Axis::Vertical => (area.y0, area.y1), + }; + + let total_length = end - start; + + (start..end).step_by(step_size as usize).map(move |pos| { + let remaining = end - pos; + let slice_size = (step_size as i16).min(remaining); + let slice = match axis { + Axis::Horizontal => Rect::new( + Point::new(pos, area.y0), + Point::new(pos + slice_size, area.y1), + ), + Axis::Vertical => Rect::new( + Point::new(area.x0, pos), + Point::new(area.x1, pos + slice_size), + ), + }; + + // Calculate factor based on the center of the slice for better visual accuracy + let slice_center = pos + slice_size / 2; + let factor = (slice_center - start) as f32 / total_length as f32; + (slice, factor) + }) +} 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 6155d81a32..46dbc6e2f9 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs @@ -3,6 +3,7 @@ pub mod backlight; pub mod bootloader; #[cfg(feature = "micropython")] pub mod firmware; +pub mod gradient; #[cfg(feature = "micropython")] pub use firmware::*; @@ -18,6 +19,8 @@ use super::{ fonts, }; +pub use gradient::Gradient; + // Color palette. pub const WHITE: Color = Color::rgb(0xFF, 0xFF, 0xFF); pub const BLACK: Color = Color::rgb(0, 0, 0); @@ -40,16 +43,14 @@ pub const ORANGE: Color = Color::rgb(0xFF, 0x63, 0x30); pub const ORANGE_DIMMED: Color = Color::rgb(0x9E, 0x57, 0x42); pub const ORANGE_DARK: Color = Color::rgb(0x18, 0x0C, 0x0A); pub const ORANGE_EXTRA_DARK: Color = Color::rgb(0x12, 0x07, 0x04); -pub const ORANGE_SUPER_DARK: Color = Color::rgb(0x2A, 0x0A, 0x00); // Homescreen gradient +pub const ORANGE_SUPER_DARK: Color = Color::rgb(0x2A, 0x0A, 0x00); // used in gradient pub const YELLOW: Color = Color::rgb(0xFF, 0xE4, 0x58); -pub const YELLOW_DARK: Color = Color::rgb(0x21, 0x1E, 0x0C); // Homescreen gradient +pub const YELLOW_DARK: Color = Color::rgb(0x21, 0x1E, 0x0C); // used in gradient pub const BLUE: Color = Color::rgb(0x00, 0x46, 0xFF); pub const RED: Color = Color::rgb(0xFF, 0x30, 0x30); -pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E); -pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41); // Common constants pub const PADDING: i16 = 24; // [px] diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 4af5ce468f..1bbd4b6204 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -39,7 +39,13 @@ use super::{ SelectWordCountScreen, SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen, ValueInputScreen, }, - flow, fonts, theme, UIEckhart, + flow, fonts, + theme::{ + self, + firmware::{button_actionbar_danger, button_confirm}, + gradient::Gradient, + }, + UIEckhart, }; use heapless::Vec; @@ -77,21 +83,24 @@ impl FirmwareUI for UIEckhart { ) }; - let right_button = if hold { - let verb = verb.unwrap_or(TR::buttons__hold_to_confirm.into()); - let style = if hold_danger { - theme::firmware::button_actionbar_danger() - } else { - theme::firmware::button_confirm() - }; - Button::with_text(verb) - .with_long_press(theme::CONFIRM_HOLD_DURATION) - .with_long_press_danger(hold_danger) - .styled(style) - } else if let Some(verb) = verb { - Button::with_text(verb) - } else { - Button::with_text(TR::buttons__confirm.into()).styled(theme::firmware::button_confirm()) + let right_button = match (hold, verb) { + (true, verb) => { + let verb = verb.unwrap_or(TR::buttons__hold_to_confirm.into()); + let (style, gradient) = if hold_danger { + (button_actionbar_danger(), theme::Gradient::Alert) + } else { + (button_confirm(), theme::Gradient::SignGreen) + }; + Button::with_text(verb) + .with_long_press(theme::CONFIRM_HOLD_DURATION) + .with_long_press_danger(hold_danger) + .with_gradient(gradient) + .styled(style) + } + (false, Some(verb)) => Button::with_text(verb), + (false, None) => { + Button::with_text(TR::buttons__confirm.into()).styled(button_confirm()) + } }; let mut screen = TextScreen::new(paragraphs) @@ -444,14 +453,16 @@ impl FirmwareUI for UIEckhart { let verb = verb.unwrap_or(TR::buttons__hold_to_confirm.into()); Button::with_text(verb) .with_long_press(theme::CONFIRM_HOLD_DURATION) - .styled(theme::firmware::button_confirm()) + .styled(button_confirm()) } else if let Some(verb) = verb { Button::with_text(verb) } else { - Button::with_text(TR::buttons__confirm.into()).styled(theme::firmware::button_confirm()) + Button::with_text(TR::buttons__confirm.into()).styled(button_confirm()) }; if warning_footer.is_some() { - right_button = right_button.styled(theme::button_actionbar_danger()); + right_button = right_button + .styled(theme::button_actionbar_danger()) + .with_gradient(Gradient::Alert); } let header = if info { Header::new(title)