1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-27 08:38:07 +00:00

feat(core): introdue UiFeaturesFirmware

This commit introduces a trait defining high level UI building blocks.
The trait is common for all models.

trezorui_api module exposes these functions to MicroPython world.

`show_info` is implemented as a first function.
This commit is contained in:
obrusvit 2024-10-18 17:14:21 +02:00
parent c51f51d233
commit f354a3d6ba
17 changed files with 269 additions and 136 deletions

View File

@ -1,12 +1,43 @@
use crate::micropython::map::Map;
use crate::ui::layout::obj::ATTACH_TYPE_OBJ;
use crate::ui::layout::base::LAYOUT_STATE;
use crate::ui::backlight::BACKLIGHT_LEVELS_OBJ;
use crate::{
micropython::{macros::obj_module, module::Module, qstr::Qstr},
ui::layout::result::{CANCELLED, CONFIRMED, INFO},
micropython::{
macros::{obj_fn_kw, obj_module},
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
},
strutil::TString,
ui::{
backlight::BACKLIGHT_LEVELS_OBJ,
layout::{
base::LAYOUT_STATE,
obj::{LayoutObj, ATTACH_TYPE_OBJ},
result::{CANCELLED, CONFIRMED, INFO},
},
ui_features::ModelUI,
ui_features_fw::UIFeaturesFirmware,
},
};
// free-standing functions exported to MicroPython mirror `trait
// UIFeaturesFirmware`
extern "C" fn show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let button: TString = kwargs
.get_or(Qstr::MP_QSTR_button, TString::empty())?
.try_into()?;
let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?.try_into()?;
let obj = ModelUI::show_info(title, description, button, time_ms)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
#[no_mangle]
pub static mp_module_trezorui_api: Module = obj_module! {
/// from trezor import utils
@ -103,6 +134,16 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// INFO: UiResult
Qstr::MP_QSTR_INFO => INFO.as_obj(),
/// def show_info(
/// *,
/// title: str,
/// description: str = "",
/// button: str = "",
/// time_ms: int = 0,
/// ) -> LayoutObj[UiResult]:
/// """Info screen."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, show_info).as_obj(),
/// class BacklightLevels:
/// """Backlight levels. Values dynamically update based on user settings."""
/// MAX: ClassVar[int]
@ -130,4 +171,5 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// TRANSITIONING: "ClassVar[LayoutState]"
/// DONE: "ClassVar[LayoutState]"
Qstr::MP_QSTR_LayoutState => LAYOUT_STATE.as_obj(),
};

View File

@ -94,7 +94,7 @@ pub trait ComponentMsgObj: Component {
pub trait ComponentMaybeTrace: Component + ComponentMsgObj + MaybeTrace {}
impl<T> ComponentMaybeTrace for T where T: Component + ComponentMsgObj + MaybeTrace {}
struct RootComponent<T, M>
pub struct RootComponent<T, M>
where
T: Component,
M: UIFeaturesCommon,

View File

@ -23,6 +23,7 @@ pub mod model_mercury;
pub mod model_tr;
#[cfg(feature = "model_tt")]
pub mod model_tt;
pub mod ui_features;
#[cfg(feature = "micropython")]
pub mod ui_features_fw;

View File

@ -964,21 +964,6 @@ extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let content = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description));
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(Direction::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1512,7 +1497,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
///
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(),
/// def disable_animation(disable: bool) -> None:
/// """Disable animations, debug builds only."""
Qstr::MP_QSTR_disable_animation => obj_fn_1!(upy_disable_animation).as_obj(),
@ -1716,17 +1700,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Success screen. Description is used in the footer."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
/// def show_info(
/// *,
/// title: str,
/// button: str = "CONTINUE",
/// description: str = "",
/// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> LayoutObj[UiResult]:
/// """Info modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
/// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(),

View File

@ -0,0 +1,37 @@
use crate::{
error::Error,
micropython::gc::Gc,
strutil::TString,
translations::TR,
ui::{
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, Paragraphs},
},
geometry::Direction,
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
ui_features_fw::UIFeaturesFirmware,
},
};
use super::{
component::{Frame, SwipeContent, SwipeUpScreen},
theme, ModelMercuryFeatures,
};
impl UIFeaturesFirmware for ModelMercuryFeatures {
fn show_info(
title: TString<'static>,
description: TString<'static>,
_button: TString<'static>,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
let content = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description));
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(Direction::Up, SwipeSettings::default()),
))?;
Ok(obj)
}
}

View File

@ -1098,31 +1098,6 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?;
let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?;
let content = Frame::new(
title,
Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]),
);
let obj = if time_ms == 0 {
// No timer, used when we only want to draw the dialog once and
// then throw away the layout object.
LayoutObj::new(content)?
} else {
// Timeout.
let timeout = Timeout::new(time_ms);
LayoutObj::new((timeout, content.map(|_| None)))?
};
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_passphrase() -> Obj {
let block = move || {
let text: TString = TR::passphrase__please_enter.into();
@ -1871,15 +1846,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Warning modal with middle button and centered text."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(),
/// def show_info(
/// *,
/// title: str,
/// description: str = "",
/// time_ms: int = 0,
/// ) -> LayoutObj[UiResult]:
/// """Info modal."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_passphrase() -> LayoutObj[UiResult]:
/// """Show passphrase on host dialog."""
Qstr::MP_QSTR_show_passphrase => obj_fn_0!(new_show_passphrase).as_obj(),

View File

@ -12,6 +12,7 @@ mod screens;
pub mod theme;
pub struct ModelTRFeatures {}
pub mod ui_features_fw;
impl UIFeaturesCommon for ModelTRFeatures {
const SCREEN: Rect = constant::SCREEN;

View File

@ -0,0 +1,38 @@
use crate::{
error::Error,
micropython::gc::Gc,
strutil::TString,
ui::{
component::{
text::paragraphs::{Paragraph, Paragraphs}, ComponentExt, Timeout
},
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
ui_features_fw::UIFeaturesFirmware,
},
};
use super::{component::Frame, theme, ModelTRFeatures};
impl UIFeaturesFirmware for ModelTRFeatures {
fn show_info(
title: TString<'static>,
description: TString<'static>,
_button: TString<'static>,
time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
let content = Frame::new(
title,
Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]),
);
let obj = if time_ms == 0 {
// No timer, used when we only want to draw the dialog once and
// then throw away the layout object.
LayoutObj::new(content)?
} else {
// Timeout.
let timeout = Timeout::new(time_ms);
LayoutObj::new((timeout, content.map(|_| None)))?
};
Ok(obj)
}
}

View File

@ -1024,20 +1024,6 @@ extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let icon = BlendedImage::new(
theme::IMAGE_BG_CIRCLE,
theme::IMAGE_FG_INFO,
theme::INFO_COLOR,
theme::FG,
theme::BG,
);
new_show_modal(kwargs, icon, theme::button_info())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1653,7 +1639,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// from trezor import utils
/// from trezorui_api import *
///
/// def disable_animation(disable: bool) -> None:
/// """Disable animations, debug builds only."""
Qstr::MP_QSTR_disable_animation => obj_fn_1!(upy_disable_animation).as_obj(),
@ -1865,17 +1850,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Success modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
/// def show_info(
/// *,
/// title: str,
/// button: str = "CONTINUE",
/// description: str = "",
/// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> LayoutObj[UiResult]:
/// """Info modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
/// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(),

View File

@ -19,6 +19,7 @@ use crate::ui::{
};
pub struct ModelTTFeatures;
pub mod ui_features_fw;
impl UIFeaturesCommon for ModelTTFeatures {
#[cfg(feature = "backlight")]

View File

@ -0,0 +1,111 @@
use crate::{
error::Error,
micropython::gc::Gc,
strutil::TString,
ui::{
component::{image::BlendedImage, ComponentExt, Empty, Timeout},
layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
ui_features_fw::UIFeaturesFirmware,
},
};
use super::{
component::{Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, IconDialog},
theme, ModelTTFeatures,
};
impl UIFeaturesFirmware for ModelTTFeatures {
fn show_info(
title: TString<'static>,
description: TString<'static>,
button: TString<'static>,
time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
assert!(
!button.is_empty() || time_ms > 0,
"either button or timeout must be set"
);
let icon = BlendedImage::new(
theme::IMAGE_BG_CIRCLE,
theme::IMAGE_FG_INFO,
theme::INFO_COLOR,
theme::FG,
theme::BG,
);
let res = new_show_modal(
title,
TString::empty(),
description,
TString::empty(),
false,
time_ms,
icon,
theme::button_info(),
)?;
Ok(res)
}
}
fn new_show_modal(
title: TString<'static>,
value: TString<'static>,
description: TString<'static>,
button: TString<'static>,
allow_cancel: bool,
time_ms: u32,
icon: BlendedImage,
button_style: ButtonStyleSheet,
) -> Result<Gc<LayoutObj>, Error> {
let no_buttons = button.is_empty();
let obj = if no_buttons && time_ms == 0 {
// No buttons and no timer, used when we only want to draw the dialog once and
// then throw away the layout object.
LayoutObj::new(
IconDialog::new(icon, title, Empty)
.with_value(value)
.with_description(description),
)?
} else if no_buttons && time_ms > 0 {
// Timeout, no buttons.
LayoutObj::new(
IconDialog::new(
icon,
title,
Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)),
)
.with_value(value)
.with_description(description),
)?
} else if allow_cancel {
// Two buttons.
LayoutObj::new(
IconDialog::new(
icon,
title,
Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text(button).styled(button_style),
false,
),
)
.with_value(value)
.with_description(description),
)?
} else {
// Single button.
LayoutObj::new(
IconDialog::new(
icon,
title,
theme::button_bar(Button::with_text(button).styled(button_style).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
})),
)
.with_value(value)
.with_description(description),
)?
};
Ok(obj)
}

View File

@ -0,0 +1,12 @@
use crate::{error::Error, micropython::gc::Gc, strutil::TString};
use super::layout::obj::{LayoutMaybeTrace, LayoutObj};
pub trait UIFeaturesFirmware {
fn show_info(
title: TString<'static>,
description: TString<'static>,
button: TString<'static>,
time_ms: u32,
) -> Result<Gc<LayoutObj>, Error>;
}

View File

@ -223,18 +223,6 @@ def show_success(
"""Success screen. Description is used in the footer."""
# rust/src/ui/model_mercury/layout.rs
def show_info(
*,
title: str,
button: str = "CONTINUE",
description: str = "",
allow_cancel: bool = False,
time_ms: int = 0,
) -> LayoutObj[UiResult]:
"""Info modal. No buttons shown when `button` is empty string."""
# rust/src/ui/model_mercury/layout.rs
def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
"""Warning modal, receiving address mismatch."""
@ -793,16 +781,6 @@ def show_warning(
"""Warning modal with middle button and centered text."""
# rust/src/ui/model_tr/layout.rs
def show_info(
*,
title: str,
description: str = "",
time_ms: int = 0,
) -> LayoutObj[UiResult]:
"""Info modal."""
# rust/src/ui/model_tr/layout.rs
def show_passphrase() -> LayoutObj[UiResult]:
"""Show passphrase on host dialog."""
@ -1251,18 +1229,6 @@ def show_success(
"""Success modal. No buttons shown when `button` is empty string."""
# rust/src/ui/model_tt/layout.rs
def show_info(
*,
title: str,
button: str = "CONTINUE",
description: str = "",
allow_cancel: bool = False,
time_ms: int = 0,
) -> LayoutObj[UiResult]:
"""Info modal. No buttons shown when `button` is empty string."""
# rust/src/ui/model_tt/layout.rs
def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
"""Warning modal, receiving address mismatch."""

View File

@ -68,6 +68,17 @@ CANCELLED: UiResult
INFO: UiResult
# rust/src/ui/api/firmware_upy.rs
def show_info(
*,
title: str,
description: str = "",
button: str = "",
time_ms: int = 0,
) -> LayoutObj[UiResult]:
"""Info screen."""
# rust/src/ui/api/firmware_upy.rs
class BacklightLevels:
"""Backlight levels. Values dynamically update based on user settings."""

View File

@ -290,8 +290,9 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non
description = TR.backup__info_multi_share_backup
await interact(
trezorui2.show_info(
title=TR.backup__title_create_wallet_backup, description=description
trezorui_api.show_info(
title=TR.backup__title_create_wallet_backup,
description=description,
),
"backup_intro",
ButtonRequestType.ResetDevice,

View File

@ -1136,7 +1136,7 @@ def error_popup(
description = description.format(description_param)
if subtitle:
description = f"{subtitle}\n{description}"
return trezorui2.show_info(
return trezorui_api.show_info(
title=title,
description=description,
time_ms=timeout_ms,

View File

@ -313,11 +313,10 @@ def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable
description = TR.backup__info_multi_share_backup
return raise_if_not_confirmed(
trezorui2.show_info(
trezorui_api.show_info(
title="",
button=TR.buttons__continue,
description=description,
allow_cancel=False,
button=TR.buttons__continue,
),
"backup_intro",
ButtonRequestType.ResetDevice,
@ -326,10 +325,10 @@ def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable
def show_warning_backup() -> Awaitable[trezorui_api.UiResult]:
return interact(
trezorui2.show_info(
trezorui_api.show_info(
title=TR.reset__never_make_digital_copy,
description="",
button=TR.buttons__ok_i_understand,
allow_cancel=False,
),
"backup_warning",
ButtonRequestType.ResetDevice,