mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-30 18:38:27 +00:00
feat(eckhart): introduce gradient module
[no changelog]
This commit is contained in:
parent
0c393b3dcd
commit
b55bf90564
@ -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)
|
||||
|
@ -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());
|
||||
};
|
||||
|
@ -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<Color>) {
|
||||
// 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<BinaryData<'static>> {
|
||||
if let Ok(image) = get_user_custom_image() {
|
||||
if check_homescreen_format(image) {
|
||||
|
@ -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),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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<SwipeFlow, error::Error> {
|
||||
.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,
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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<T: AllowedTextContent + MaybeTrace + 'static>(
|
||||
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)
|
||||
|
@ -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),
|
||||
|
@ -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<SwipeFlow, error::Error> {
|
||||
)
|
||||
.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 {
|
||||
|
@ -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);
|
||||
|
@ -70,7 +70,7 @@ pub fn new_request_passphrase() -> Result<SwipeFlow, error::Error> {
|
||||
.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),
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
273
core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs
Normal file
273
core/embed/rust/src/ui/layout_eckhart/theme/gradient.rs
Normal file
@ -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<Item = (Rect, f32)> {
|
||||
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)
|
||||
})
|
||||
}
|
@ -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]
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user