refactor(core): introduce Notification rust type
What changed, and why it matters
This commit is a straightforward internal code cleanup in the Trezor firmware's user-interface code. It replaces a loose pair of values (a notification text string plus a separate numeric 'level') with a single structured 'Notification' type, and gives the numeric levels named constants like ALERT, WARNING, INFO, and SUCCESS. There is no change to security behavior, no bug fix, and no externally visible functional change for users.
No security action required. Treat as a normal maintainability refactor during code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors homescreen notification handling across multiple Rust UI layouts (bolt, caesar, delizia, eckhart). It introduces a new Notification struct and NotificationLevel enum in core/embed/rust/src/ui/notification.rs, exports the enum to MicroPython, and updates callers to pass a (text, level) tuple instead of separate notification and notification_level arguments. The mapping from level integers to colors/icons/LEDs is preserved exactly (0=Alert, 1=Warning, 2=Info, 3=Success). No logic, validation, or trust boundary behavior is altered.
Changed components
core/embed/rust/src/ui/notification.rscore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_bolt/component/homescreen.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/component/homescreen.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/component/homescreen.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rscore/embed/rust/src/ui/layout_eckhart/theme/firmware.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/src/apps/homescreen/__init__.pycore/src/trezor/ui/layouts/homescreen.pyInspect captured patch +210 / −141
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 600ad7e0..1afe3898 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -16,6 +16,7 @@ static void _librust_qstrs(void) {
MP_QSTR_8;
MP_QSTR_9;
MP_QSTR_;
+ MP_QSTR_ALERT;
MP_QSTR_ATTACHED;
MP_QSTR_AttachType;
MP_QSTR_BACK;
@@ -42,6 +43,7 @@ static void _librust_qstrs(void) {
MP_QSTR_MsgDef;
MP_QSTR_NONE;
MP_QSTR_NORMAL;
+ MP_QSTR_NotificationLevel;
MP_QSTR_PairDevice;
MP_QSTR_RESUME;
MP_QSTR_RX_PACKET_LEN;
@@ -51,6 +53,7 @@ static void _librust_qstrs(void) {
MP_QSTR_RemovePin;
MP_QSTR_RemoveWipeCode;
MP_QSTR_ReviewFailedBackup;
+ MP_QSTR_SUCCESS;
MP_QSTR_SWIPE_DOWN;
MP_QSTR_SWIPE_LEFT;
MP_QSTR_SWIPE_RIGHT;
@@ -71,6 +74,7 @@ static void _librust_qstrs(void) {
MP_QSTR_TurnOff;
MP_QSTR_UnpairAllDevices;
MP_QSTR_UnpairDevice;
+ MP_QSTR_WARNING;
MP_QSTR_WipeDevice;
MP_QSTR___del__;
MP_QSTR___dict__;
@@ -478,7 +482,6 @@ static void _librust_qstrs(void) {
MP_QSTR_more_info_callback;
MP_QSTR_multiple_pages_texts;
MP_QSTR_notification;
- MP_QSTR_notification_level;
MP_QSTR_page_count;
MP_QSTR_page_counter;
MP_QSTR_pages;
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 6a26b7b9..6d2b4617 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -25,6 +25,7 @@ use crate::{
result::{BACK, CANCELLED, CONFIRMED, INFO},
util::{upy_disable_animation, RecoveryType},
},
+ notification::{Notification, NotificationLevel, NOTIFICATION_LEVEL_OBJ},
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_PAIRED_DEVICES,
MAX_WORD_QUIZ_ITEMS,
@@ -927,13 +928,23 @@ extern "C" fn new_show_group_share_success(
extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let label: TString<'static> = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?;
- let notification: Option<TString<'static>> =
- kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?;
- let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?;
+ let notification: Option<Obj> = kwargs
+ .get(Qstr::MP_QSTR_notification)
+ .unwrap_or_else(|_| Obj::const_none())
+ .try_into_option()?;
let lockable: bool = kwargs.get(Qstr::MP_QSTR_lockable)?.try_into()?;
let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?;
- let layout = ModelUI::show_homescreen(label, notification, notification_level, lockable)?;
+ let notification = if let Some(notif_tuple) = notification {
+ let [text, level]: [Obj; 2] = util::iter_into_array(notif_tuple)?;
+ let text: TString<'static> = text.try_into()?;
+ let level: NotificationLevel = level.try_into()?;
+ Some(Notification { text, level })
+ } else {
+ None
+ };
+
+ let layout = ModelUI::show_homescreen(label, notification, lockable)?;
let layout_obj = LayoutObj::new_root(layout)?;
if skip_first_paint {
layout_obj.skip_first_paint();
@@ -1970,8 +1981,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// def show_homescreen(
/// *,
/// label: str,
- /// notification: str | None,
- /// notification_level: int = 0,
+ /// notification: tuple[str, int] | None = None,
/// lockable: bool,
/// skip_first_paint: bool,
/// ) -> LayoutObj[UiResult]:
@@ -2202,6 +2212,14 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// SWIPE_RIGHT: ClassVar[int]
Qstr::MP_QSTR_AttachType => ATTACH_TYPE_OBJ.as_obj(),
+ /// class NotificationLevel:
+ /// """Notification level determining the style of notification."""
+ /// ALERT: ClassVar[int]
+ /// WARNING: ClassVar[int]
+ /// INFO: ClassVar[int]
+ /// SUCCESS: ClassVar[int]
+ Qstr::MP_QSTR_NotificationLevel => NOTIFICATION_LEVEL_OBJ.as_obj(),
+
/// class LayoutState:
/// """Layout state."""
/// INITIAL: "ClassVar[LayoutState]"
diff --git a/core/embed/rust/src/ui/layout_bolt/component/homescreen.rs b/core/embed/rust/src/ui/layout_bolt/component/homescreen.rs
index 96967774..cac0b5d4 100644
--- a/core/embed/rust/src/ui/layout_bolt/component/homescreen.rs
+++ b/core/embed/rust/src/ui/layout_bolt/component/homescreen.rs
@@ -14,6 +14,7 @@ use crate::{
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
+ notification::{Notification, NotificationLevel},
shape::{self, Renderer},
},
};
@@ -47,16 +48,9 @@ pub struct HomescreenText<'a> {
pub icon: Option<Icon>,
}
-#[derive(Clone, Copy)]
-pub struct HomescreenNotification {
- pub text: TString<'static>,
- pub icon: Icon,
- pub color: Color,
-}
-
pub struct Homescreen {
label: TString<'static>,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
image: BinaryData<'static>,
hold_to_lock: bool,
loader: Loader,
@@ -73,7 +67,7 @@ pub enum HomescreenMsg {
impl Homescreen {
pub fn new(
label: TString<'static>,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
hold_to_lock: bool,
) -> Self {
Self {
@@ -88,32 +82,23 @@ impl Homescreen {
}
}
- fn level_to_style(level: u8) -> (Color, Icon) {
+ fn level_to_style(level: NotificationLevel) -> (Color, Icon) {
match level {
- 3 => (theme::YELLOW, theme::ICON_COINJOIN),
- 2 => (theme::VIOLET, theme::ICON_MAGIC),
- 1 => (theme::YELLOW, theme::ICON_WARN),
- _ => (theme::RED, theme::ICON_WARN),
+ NotificationLevel::Success => (theme::YELLOW, theme::ICON_COINJOIN),
+ NotificationLevel::Info => (theme::VIOLET, theme::ICON_MAGIC),
+ NotificationLevel::Warning => (theme::YELLOW, theme::ICON_WARN),
+ NotificationLevel::Alert => (theme::RED, theme::ICON_WARN),
}
}
- fn get_notification(&self) -> Option<HomescreenNotification> {
+ fn get_notification(&self) -> Option<Notification> {
if !usb_configured() {
- let (color, icon) = Self::level_to_style(0);
- Some(HomescreenNotification {
+ Some(Notification {
text: TR::homescreen__title_no_usb_connection.into(),
- icon,
- color,
- })
- } else if let Some((notification, level)) = self.notification {
- let (color, icon) = Self::level_to_style(level);
- Some(HomescreenNotification {
- text: notification,
- icon,
- color,
+ level: NotificationLevel::Alert,
})
} else {
- None
+ self.notification.clone()
}
}
@@ -242,6 +227,8 @@ impl Component for Homescreen {
const NOTIFICATION_BORDER: i16 = 6;
const TEXT_ICON_SPACE: i16 = 8;
+ let (color, icon) = Self::level_to_style(notif.level);
+
let banner = self
.pad
.area
@@ -251,12 +238,12 @@ impl Component for Homescreen {
shape::Bar::new(banner)
.with_radius(2)
- .with_bg(notif.color)
+ .with_bg(color)
.render(target);
notif.text.map(|t| {
let style = theme::TEXT_BOLD;
- let icon_width = notif.icon.toif.width() + TEXT_ICON_SPACE;
+ let icon_width = icon.toif.width() + TEXT_ICON_SPACE;
let text_pos = Point::new(
style
.text_font
@@ -270,7 +257,7 @@ impl Component for Homescreen {
let icon_pos = Point::new(text_pos.x - icon_width, banner.center().y);
- shape::ToifImage::new(icon_pos, notif.icon.toif)
+ shape::ToifImage::new(icon_pos, icon.toif)
.with_fg(style.text_color)
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index 291819bd..84588398 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -25,6 +25,7 @@ use crate::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::{ConfirmValueParams, PropsList, RecoveryType},
},
+ notification::Notification,
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_MENU_ITEMS,
MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS,
@@ -922,11 +923,9 @@ impl FirmwareUI for UIBolt {
fn show_homescreen(
label: TString<'static>,
- notification: Option<TString<'static>>,
- notification_level: u8,
+ notification: Option<Notification>,
lockable: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
- let notification = notification.map(|w| (w, notification_level));
let layout = RootComponent::new(Homescreen::new(label, notification, lockable));
Ok(layout)
}
diff --git a/core/embed/rust/src/ui/layout_caesar/component/homescreen.rs b/core/embed/rust/src/ui/layout_caesar/component/homescreen.rs
index 1a0cfa97..568b8d50 100644
--- a/core/embed/rust/src/ui/layout_caesar/component/homescreen.rs
+++ b/core/embed/rust/src/ui/layout_caesar/component/homescreen.rs
@@ -12,8 +12,8 @@ use crate::{
},
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
- shape,
- shape::Renderer,
+ notification::Notification,
+ shape::{self, Renderer},
},
};
@@ -59,7 +59,7 @@ pub struct Homescreen {
// TODO label should be a Child in theory, but the homescreen image is not, so it is
// always painted, so we need to always paint the label too
label: Label<'static>,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
custom_image: Option<BinaryData<'static>>,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController>,
@@ -74,7 +74,7 @@ pub struct Homescreen {
impl Homescreen {
pub fn new(
label: TString<'static>,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
loader_description: Option<TString<'static>>,
) -> Self {
// Buttons will not be visible, we only need both left and right to be existing
@@ -118,12 +118,12 @@ impl Homescreen {
.with_align(Alignment::Center)
.render(target)
});
- } else if let Some((notification, _level)) = &self.notification {
+ } else if let Some(notification) = &self.notification {
shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0)
.with_bg(theme::BG)
.render(target);
- notification.map(|c| {
+ notification.text.map(|c| {
shape::Text::new(baseline, c, NOTIFICATION_FONT)
.with_align(Alignment::Center)
.render(target)
@@ -132,7 +132,7 @@ impl Homescreen {
// Painting warning icons in top corners when the text is short enough not to
// collide with them
let icon_width = NOTIFICATION_ICON.toif.width();
- let text_width = notification.map(|c| NOTIFICATION_FONT.text_width(c));
+ let text_width = notification.text.map(|c| NOTIFICATION_FONT.text_width(c));
if AREA.width() >= text_width + (icon_width + 1) * 2 {
shape::ToifImage::new(AREA.top_left(), NOTIFICATION_ICON.toif)
.with_align(Alignment2D::TOP_LEFT)
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index ca6e64b4..12c1e9d5 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -24,6 +24,7 @@ use crate::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::{ConfirmValueParams, RecoveryType},
},
+ notification::Notification,
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_MENU_ITEMS,
MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS,
@@ -1115,11 +1116,9 @@ impl FirmwareUI for UICaesar {
fn show_homescreen(
label: TString<'static>,
- notification: Option<TString<'static>>,
- notification_level: u8,
+ notification: Option<Notification>,
lockable: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
- let notification = notification.map(|w| (w, notification_level));
let loader_description = lockable.then_some("Locking the device...".into());
let layout = RootComponent::new(Homescreen::new(label, notification, loader_description));
Ok(layout)
diff --git a/core/embed/rust/src/ui/layout_delizia/component/homescreen.rs b/core/embed/rust/src/ui/layout_delizia/component/homescreen.rs
index 8c1a221e..80199f87 100644
--- a/core/embed/rust/src/ui/layout_delizia/component/homescreen.rs
+++ b/core/embed/rust/src/ui/layout_delizia/component/homescreen.rs
@@ -14,6 +14,7 @@ use crate::{
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
+ notification::{Notification, NotificationLevel},
shape::{self, Renderer},
},
};
@@ -98,11 +99,20 @@ const fn default_hs_radii() -> [i16; 5] {
arr
}
-fn render_notif<'s>(notif: HomescreenNotification, top: i16, target: &mut impl Renderer<'s>) {
+/// Returns background color and text color
+fn level_to_style(level: NotificationLevel) -> (Color, Color) {
+ match level {
+ NotificationLevel::Success => (theme::GREEN_DARK, theme::GREEN_LIME),
+ _ => (theme::ORANGE_DARK, theme::ORANGE_LIGHT),
+ }
+}
+
+fn render_notif<'s>(notif: Notification, top: i16, target: &mut impl Renderer<'s>) {
notif.text.map(|t| {
let style = theme::TEXT_BOLD;
let text_width = style.text_font.text_width(t);
+ let (color_bg, color_text) = level_to_style(notif.level);
let banner = Rect::new(
Point::new(AREA.center().x - NOTIFICATION_BORDER - text_width / 2, top),
@@ -119,12 +129,12 @@ fn render_notif<'s>(notif: HomescreenNotification, top: i16, target: &mut impl R
shape::Bar::new(banner)
.with_radius(NOTIFICATION_BG_RADIUS)
- .with_bg(notif.color_bg)
+ .with_bg(color_bg)
.with_alpha(NOTIFICATION_BG_ALPHA)
.render(target);
shape::Text::new(text_pos, t, style.text_font)
- .with_fg(notif.color_text)
+ .with_fg(color_text)
.render(target);
});
}
@@ -488,13 +498,6 @@ impl HideLabelAnimation {
}
}
-#[derive(Clone, Copy)]
-pub struct HomescreenNotification {
- pub text: TString<'static>,
- pub color_bg: Color,
- pub color_text: Color,
-}
-
pub struct Homescreen {
/// Label for the device name, a.k.a "label"
label_device: Label<'static>,
@@ -504,7 +507,7 @@ pub struct Homescreen {
labels_width: i16,
/// Combined height of both labels
labels_height: i16,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
image: Option<BinaryData<'static>>,
bg_image: ImageBuffer<Rgb565Canvas<'static>>,
hold_to_lock: bool,
@@ -521,7 +524,7 @@ pub enum HomescreenMsg {
impl Homescreen {
pub fn new(
label: TString<'static>,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
hold_to_lock: bool,
) -> Result<Self, Error> {
let label_width = label.map(|t| theme::TEXT_DEMIBOLD.text_font.text_width(t));
@@ -567,30 +570,14 @@ impl Homescreen {
})
}
- fn level_to_style(level: u8) -> (Color, Color) {
- match level {
- 3 => (theme::GREEN_DARK, theme::GREEN_LIME),
- _ => (theme::ORANGE_DARK, theme::ORANGE_LIGHT),
- }
- }
-
- fn get_notification(&self) -> Option<HomescreenNotification> {
+ fn get_notification(&self) -> Option<Notification> {
if !usb_configured() {
- let (color_bg, color_text) = Self::level_to_style(0);
- Some(HomescreenNotification {
+ Some(Notification {
text: TR::homescreen__title_no_usb_connection.into(),
- color_bg,
- color_text,
- })
- } else if let Some((notification, level)) = self.notification {
- let (color_bg, color_text) = Self::level_to_style(level);
- Some(HomescreenNotification {
- text: notification,
- color_bg,
- color_text,
+ level: NotificationLevel::Alert,
})
} else {
- None
+ self.notification.clone()
}
}
@@ -1001,10 +988,9 @@ impl Component for Lockscreen {
render_instruction(tap.into(), target);
if self.coinjoin_authorized {
- let notif = HomescreenNotification {
+ let notif = Notification {
text: TR::homescreen__title_coinjoin_authorized.into(),
- color_bg: theme::GREEN_DARK,
- color_text: theme::GREEN_LIME,
+ level: NotificationLevel::Success,
};
render_notif(notif, NOTIFICATION_LOCKSCREEN_TOP, target);
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 564be955..95d72952 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -26,6 +26,7 @@ use crate::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::{ContentType, PropsList, RecoveryType, StrOrBytes},
},
+ notification::Notification,
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_MENU_ITEMS,
MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS,
@@ -982,11 +983,9 @@ impl FirmwareUI for UIDelizia {
fn show_homescreen(
label: TString<'static>,
- notification: Option<TString<'static>>,
- notification_level: u8,
+ notification: Option<Notification>,
lockable: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
- let notification = notification.map(|w| (w, notification_level));
let layout = RootComponent::new(Homescreen::new(label, notification, lockable)?);
Ok(layout)
}
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 565c5e16..c7651253 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs
@@ -8,6 +8,7 @@ use crate::{
display::{image::ImageInfo, Color},
geometry::{Alignment, Direction, Offset, Rect},
layout::util::get_user_custom_image,
+ notification::{Notification, NotificationLevel},
shape::{self, Renderer},
util::animation_disabled,
},
@@ -61,17 +62,15 @@ impl Homescreen {
locked: bool,
bootscreen: bool,
coinjoin_authorized: bool,
- notification: Option<(TString<'static>, u8)>,
+ notification: Option<Notification>,
) -> Result<Self, Error> {
let image = get_homescreen_image();
let shadow = image.is_some();
// Notification
- let mut notification_level = 4;
let (led_color, hint) = match notification {
- Some((text, level)) => {
- notification_level = level;
- let (led_color, hint) = Self::get_notification_display(level, text);
+ Some(ref notification) => {
+ let (led_color, hint) = Self::get_notification_display(notification);
(Some(led_color), Some(hint))
}
None if locked && coinjoin_authorized => (
@@ -85,7 +84,7 @@ impl Homescreen {
};
// Homebar
- let (style_sheet, gradient) = button_homebar_style(notification_level);
+ let (style_sheet, gradient) = button_homebar_style(notification.map(|n| n.level));
let btn = Button::new(Self::homebar_content(bootscreen, locked))
.styled(style_sheet)
.with_gradient(gradient);
@@ -110,18 +109,14 @@ impl Homescreen {
ButtonContent::HomeBar(text)
}
- fn get_notification_display(level: u8, text: TString<'static>) -> (Color, Hint<'static>) {
- match level {
- 0 => (theme::LED_RED, Hint::new_warning_danger(text)),
- 1 => (theme::LED_YELLOW, Hint::new_warning_neutral(text)),
- 2 => (theme::LED_BLUE, Hint::new_instruction(text, None)),
- 3 => (
+ fn get_notification_display(n: &Notification) -> (Color, Hint<'static>) {
+ match n.level {
+ NotificationLevel::Alert => (theme::LED_RED, Hint::new_warning_danger(n.text)),
+ NotificationLevel::Warning => (theme::LED_YELLOW, Hint::new_warning_neutral(n.text)),
+ NotificationLevel::Info => (theme::LED_BLUE, Hint::new_instruction(n.text, None)),
+ NotificationLevel::Success => (
theme::LED_GREEN_LIGHT,
- Hint::new_instruction_green(text, Some(theme::ICON_INFO)),
- ),
- _ => (
- theme::LED_WHITE,
- Hint::new_instruction(text, Some(theme::ICON_INFO)),
+ Hint::new_instruction_green(n.text, Some(theme::ICON_INFO)),
),
}
}
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 2832505e..460aa317 100644
--- a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs
@@ -1,8 +1,11 @@
use crate::{
time::ShortDuration,
- ui::component::text::{
- layout::{Chunks, LineBreaking, PageBreaking},
- TextStyle,
+ ui::{
+ component::text::{
+ layout::{Chunks, LineBreaking, PageBreaking},
+ TextStyle,
+ },
+ notification::NotificationLevel,
},
};
@@ -414,14 +417,16 @@ macro_rules! button_homebar_style {
}
};
}
-pub const fn button_homebar_style(notification_level: u8) -> (ButtonStyleSheet, Gradient) {
- // NOTE: 0 is the highest severity.
- match notification_level {
- 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),
+
+pub const fn button_homebar_style(nl: Option<NotificationLevel>) -> (ButtonStyleSheet, Gradient) {
+ match nl {
+ Some(NotificationLevel::Alert) => (button_homebar_style!(RED), Gradient::Alert),
+ Some(NotificationLevel::Warning) => (button_homebar_style!(GREY_LIGHT), Gradient::Warning),
+ Some(NotificationLevel::Info) => (button_homebar_style!(GREY_LIGHT), Gradient::DefaultGrey),
+ Some(NotificationLevel::Success) => {
+ (button_homebar_style!(GREY_LIGHT), Gradient::SignGreen)
+ }
+ None => (button_homebar_style!(GREY_LIGHT), Gradient::DefaultGrey),
}
}
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 abcaecc4..c5c0853e 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -25,6 +25,7 @@ use crate::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::{ConfirmValueParams, ContentType, PropsList, RecoveryType, StrOrBytes},
},
+ notification::Notification,
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_MENU_ITEMS,
MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS,
@@ -1203,14 +1204,12 @@ impl FirmwareUI for UIEckhart {
fn show_homescreen(
label: TString<'static>,
- notification: Option<TString<'static>>,
- notification_level: u8,
+ notification: Option<Notification>,
lockable: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
let locked = false;
let bootscreen = false;
let coinjoin_authorized = false;
- let notification = notification.map(|w| (w, notification_level));
let layout = RootComponent::new(Homescreen::new(
label,
lockable,
diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs
index 060985a4..7b054f90 100644
--- a/core/embed/rust/src/ui/mod.rs
+++ b/core/embed/rust/src/ui/mod.rs
@@ -10,6 +10,7 @@ pub mod event;
pub mod flow;
pub mod geometry;
pub mod lerp;
+pub mod notification;
pub mod shape;
pub mod util;
diff --git a/core/embed/rust/src/ui/notification.rs b/core/embed/rust/src/ui/notification.rs
new file mode 100644
index 00000000..b7c90cfb
--- /dev/null
+++ b/core/embed/rust/src/ui/notification.rs
@@ -0,0 +1,76 @@
+use crate::{error::Error, strutil::TString};
+
+#[cfg(feature = "micropython")]
+use crate::micropython::{
+ macros::{obj_dict, obj_map, obj_type},
+ obj::Obj,
+ qstr::Qstr,
+ simple_type::SimpleTypeObj,
+ typ::Type,
+};
+
+/// Homescreen notification.
+#[derive(Clone)]
+#[cfg_attr(test, derive(Debug))]
+pub struct Notification {
+ pub text: TString<'static>,
+ pub level: NotificationLevel,
+}
+
+impl Notification {
+ pub fn new(text: TString<'static>, level: NotificationLevel) -> Self {
+ Self { text, level }
+ }
+}
+
+/// Notification level determining the style of notification.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug)]
+pub enum NotificationLevel {
+ /// Strong warning, e.g. "Backup failed"
+ Alert = 0,
+ /// Warning, e.g. "PIN not set"
+ Warning = 1,
+ /// Information, e.g. "Connected" or "Experimental features"
+ Info = 2,
+ /// Successful operation, e.g. "Coinjoin authorized"
+ Success = 3,
+}
+
+impl TryFrom<u8> for NotificationLevel {
+ type Error = Error;
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(NotificationLevel::Alert),
+ 1 => Ok(NotificationLevel::Warning),
+ 2 => Ok(NotificationLevel::Info),
+ 3 => Ok(NotificationLevel::Success),
+ _ => Err(Error::OutOfRange),
+ }
+ }
+}
+
+#[cfg(feature = "micropython")]
+impl TryFrom<Obj> for NotificationLevel {
+ type Error = Error;
+
+ fn try_from(obj: Obj) -> Result<Self, Self::Error> {
+ let val = u8::try_from(obj)?;
+ let this = Self::try_from(val)?;
+ Ok(this)
+ }
+}
+
+#[cfg(feature = "micropython")]
+static NOTIFICATION_LEVEL_TYPE: Type = obj_type! {
+ name: Qstr::MP_QSTR_NotificationLevel,
+ locals: &obj_dict!(obj_map! {
+ Qstr::MP_QSTR_ALERT => Obj::small_int(0),
+ Qstr::MP_QSTR_WARNING => Obj::small_int(1),
+ Qstr::MP_QSTR_INFO => Obj::small_int(2),
+ Qstr::MP_QSTR_SUCCESS => Obj::small_int(3),
+ }),
+};
+
+#[cfg(feature = "micropython")]
+pub static NOTIFICATION_LEVEL_OBJ: SimpleTypeObj = SimpleTypeObj::new(&NOTIFICATION_LEVEL_TYPE);
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 28a5506b..34fb11f6 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -3,6 +3,7 @@ use crate::{
io::BinaryData,
micropython::{buffer::StrBuffer, gc::Gc, list::List, obj::Obj},
strutil::TString,
+ ui::notification::Notification,
};
use heapless::Vec;
@@ -351,8 +352,7 @@ pub trait FirmwareUI {
fn show_homescreen(
label: TString<'static>,
- notification: Option<TString<'static>>,
- notification_level: u8,
+ notification: Option<Notification>,
lockable: bool,
) -> Result<impl LayoutMaybeTrace, Error>;
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 3af58754..948b27ec 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -614,8 +614,7 @@ def show_group_share_success(
def show_homescreen(
*,
label: str,
- notification: str | None,
- notification_level: int = 0,
+ notification: tuple[str, int] | None = None,
lockable: bool,
skip_first_paint: bool,
) -> LayoutObj[UiResult]:
@@ -868,6 +867,15 @@ class AttachType:
SWIPE_RIGHT: ClassVar[int]
+# rust/src/ui/api/firmware_micropython.rs
+class NotificationLevel:
+ """Notification level determining the style of notification."""
+ ALERT: ClassVar[int]
+ WARNING: ClassVar[int]
+ INFO: ClassVar[int]
+ SUCCESS: ClassVar[int]
+
+
# rust/src/ui/api/firmware_micropython.rs
class LayoutState:
"""Layout state."""
diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py
index 9d785bb5..2f3fd6d9 100644
--- a/core/src/apps/homescreen/__init__.py
+++ b/core/src/apps/homescreen/__init__.py
@@ -6,6 +6,7 @@ import trezorui_api
from trezor import config, utils, wire
from trezor.enums import MessageType
from trezor.ui.layouts.homescreen import Busyscreen, Homescreen, Lockscreen
+from trezorui_api import NotificationLevel
from apps.base import busy_expiry_ms
from apps.common.authorization import is_set_any_session
@@ -31,30 +32,25 @@ async def homescreen() -> None:
# TODO: add notification that translations are out of date
notification = None
- notification_level = 1 # 0 = strong warning, 1 = warning, 2 = info, 3 = success
if is_set_any_session(MessageType.AuthorizeCoinJoin):
- notification = TR.homescreen__title_coinjoin_authorized
- notification_level = 3
+ notification = (
+ TR.homescreen__title_coinjoin_authorized,
+ NotificationLevel.SUCCESS,
+ )
elif storage.device.is_initialized() and storage.device.no_backup():
- notification = TR.homescreen__title_seedless
- notification_level = 0
+ notification = (TR.homescreen__title_seedless, NotificationLevel.ALERT)
elif storage.device.is_initialized() and storage.device.unfinished_backup():
- notification = TR.homescreen__title_backup_failed
- notification_level = 0
+ notification = (TR.homescreen__title_backup_failed, NotificationLevel.ALERT)
elif storage.device.is_initialized() and storage.device.needs_backup():
- notification = TR.homescreen__title_backup_needed
- notification_level = 1
+ notification = (TR.homescreen__title_backup_needed, NotificationLevel.WARNING)
elif storage.device.is_initialized() and not config.has_pin():
- notification = TR.homescreen__title_pin_not_set
- notification_level = 1
+ notification = (TR.homescreen__title_pin_not_set, NotificationLevel.WARNING)
elif storage.device.get_experimental_features():
- notification = TR.homescreen__title_experimental_mode
- notification_level = 2
+ notification = (TR.homescreen__title_experimental_mode, NotificationLevel.INFO)
obj = Homescreen(
label=label,
notification=notification,
- notification_level=notification_level,
lockable=config.has_pin(),
)
try:
diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py
index 08db44a0..3cf8ff93 100644
--- a/core/src/trezor/ui/layouts/homescreen.py
+++ b/core/src/trezor/ui/layouts/homescreen.py
@@ -6,7 +6,7 @@ from storage.cache_common import APP_COMMON_BUSY_DEADLINE_MS
from trezor import TR, ui, utils
if TYPE_CHECKING:
- from typing import Any, Callable, Iterator, ParamSpec, TypeVar
+ from typing import Any, Callable, Iterator, ParamSpec, Tuple, TypeVar
from trezor import loop
@@ -65,8 +65,7 @@ class Homescreen(HomescreenBase):
def __init__(
self,
label: str | None,
- notification: str | None,
- notification_level: int,
+ notification: Tuple[str, int] | None,
lockable: bool,
) -> None:
super().__init__(
@@ -74,7 +73,6 @@ class Homescreen(HomescreenBase):
trezorui_api.show_homescreen,
label=label or utils.MODEL_FULL_NAME,
notification=notification,
- notification_level=notification_level,
lockable=lockable,
skip_first_paint=self._should_resume(),
)
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.