1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-24 07:18:09 +00:00

feat(core): T3T1 vertical menu

This commit is contained in:
obrusvit 2024-03-06 10:48:02 +01:00 committed by Martin Milata
parent 55067a6d40
commit 80462282dc
11 changed files with 675 additions and 68 deletions

View File

@ -32,10 +32,11 @@ pub struct Button {
long_timer: Option<TimerToken>, long_timer: Option<TimerToken>,
} }
impl Button { impl<T> Button<T> {
/// Offsets the baseline of the button text either up (negative) or down /// Offsets the baseline of the button text
/// (positive). /// -x/+x => left/right
pub const BASELINE_OFFSET: i16 = -2; /// -y/+y => up/down
pub const BASELINE_OFFSET: Offset = Offset::new(2, 6);
pub const fn new(content: ButtonContent) -> Self { pub const fn new(content: ButtonContent) -> Self {
Self { Self {
@ -57,8 +58,8 @@ impl Button {
Self::new(ButtonContent::Icon(icon)) Self::new(ButtonContent::Icon(icon))
} }
pub const fn with_icon_and_text(content: IconText) -> Self { pub const fn with_icon_and_text(content: IconText<T>) -> Self {
Self::new(ButtonContent::IconAndText(content)) Self::new(ButtonContent::IconAndText::<T>(content))
} }
pub const fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self { pub const fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
@ -203,20 +204,15 @@ impl Button {
match &self.content { match &self.content {
ButtonContent::Empty => {} ButtonContent::Empty => {}
ButtonContent::Text(text) => { ButtonContent::Text(text) => {
let width = text.map(|c| style.font.text_width(c)); let text = text.as_ref();
let height = style.font.text_height(); let start_of_baseline = self.area.center() + Self::BASELINE_OFFSET;
let start_of_baseline = self.area.center() display::text_left(
+ Offset::new(-width / 2, height / 2) start_of_baseline,
+ Offset::y(Self::BASELINE_OFFSET); text,
text.map(|text| { style.font,
display::text_left( style.text_color,
start_of_baseline, style.button_color,
text, );
style.font,
style.text_color,
style.button_color,
);
});
} }
ButtonContent::Icon(icon) => { ButtonContent::Icon(icon) => {
icon.draw( icon.draw(
@ -242,17 +238,12 @@ impl Button {
match &self.content { match &self.content {
ButtonContent::Empty => {} ButtonContent::Empty => {}
ButtonContent::Text(text) => { ButtonContent::Text(text) => {
let width = text.map(|c| style.font.text_width(c)); let text = text.as_ref();
let height = style.font.text_height(); let start_of_baseline = self.area.left_center() + Self::BASELINE_OFFSET;
let start_of_baseline = self.area.center() shape::Text::new(start_of_baseline, text)
+ Offset::new(-width / 2, height / 2) .with_font(style.font)
+ Offset::y(Self::BASELINE_OFFSET); .with_fg(style.text_color)
text.map(|text| { .render(target);
shape::Text::new(start_of_baseline, text)
.with_font(style.font)
.with_fg(style.text_color)
.render(target);
});
} }
ButtonContent::Icon(icon) => { ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center(), icon.toif) shape::ToifImage::new(self.area.center(), icon.toif)
@ -261,7 +252,7 @@ impl Button {
.render(target); .render(target);
} }
ButtonContent::IconAndText(child) => { ButtonContent::IconAndText(child) => {
child.render(target, self.area, self.style(), Self::BASELINE_OFFSET); child.render(target, self.area, style, Self::BASELINE_OFFSET);
} }
ButtonContent::IconBlend(bg, fg, offset) => { ButtonContent::IconBlend(bg, fg, offset) => {
shape::Bar::new(self.area) shape::Bar::new(self.area)
@ -385,7 +376,7 @@ impl crate::trace::Trace for Button {
ButtonContent::Text(text) => t.string("text", *text), ButtonContent::Text(text) => t.string("text", *text),
ButtonContent::Icon(_) => t.bool("icon", true), ButtonContent::Icon(_) => t.bool("icon", true),
ButtonContent::IconAndText(content) => { ButtonContent::IconAndText(content) => {
t.string("text", content.text.into()); t.string("text", content.text.as_ref().into());
t.bool("icon", true); t.bool("icon", true);
} }
ButtonContent::IconBlend(_, _, _) => t.bool("icon", true), ButtonContent::IconBlend(_, _, _) => t.bool("icon", true),
@ -406,7 +397,7 @@ pub enum ButtonContent {
Empty, Empty,
Text(TString<'static>), Text(TString<'static>),
Icon(Icon), Icon(Icon),
IconAndText(IconText), IconAndText(IconText<T>),
IconBlend(Icon, Icon, Offset), IconBlend(Icon, Icon, Offset),
} }
@ -428,6 +419,115 @@ pub struct ButtonStyle {
pub border_width: i16, pub border_width: i16,
} }
impl<T> Button<T> {
pub fn cancel_confirm(
left: Button<T>,
right: Button<T>,
left_is_small: bool,
) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
T: AsRef<str>,
{
let width = if left_is_small {
theme::BUTTON_WIDTH
} else {
0
};
theme::button_bar(Split::left(
width,
theme::BUTTON_SPACING,
left.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
}),
right.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
))
}
pub fn cancel_confirm_text(
left: Option<T>,
right: Option<T>,
) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
T: AsRef<str>,
{
let left_is_small: bool;
let left = if let Some(verb) = left {
left_is_small = verb.as_ref().len() <= 4;
if verb.as_ref() == "^" {
Button::with_icon(theme::ICON_UP)
} else {
Button::with_text(verb)
}
} else {
left_is_small = right.is_some();
Button::with_icon(theme::ICON_CANCEL)
};
let right = if let Some(verb) = right {
Button::with_text(verb).styled(theme::button_confirm())
} else {
Button::with_icon(theme::ICON_CONFIRM).styled(theme::button_confirm())
};
Self::cancel_confirm(left, right, left_is_small)
}
pub fn cancel_info_confirm(
confirm: T,
info: T,
) -> CancelInfoConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
>
where
T: AsRef<str>,
{
let right = Button::with_text(confirm)
.styled(theme::button_confirm())
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed)
});
let top = Button::with_text(info)
.styled(theme::button_moreinfo())
.map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info));
let left = Button::with_icon(theme::ICON_CANCEL).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled)
});
let total_height = theme::BUTTON_HEIGHT + theme::BUTTON_SPACING + theme::INFO_BUTTON_HEIGHT;
FixedHeightBar::bottom(
Split::top(
theme::INFO_BUTTON_HEIGHT,
theme::BUTTON_SPACING,
top,
Split::left(theme::BUTTON_WIDTH, theme::BUTTON_SPACING, left, right),
),
total_height,
)
}
}
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
}
type CancelInfoConfirm<T, F0, F1, F2> = FixedHeightBar<
Split<MsgMap<Button<T>, F0>, Split<MsgMap<Button<T>, F1>, MsgMap<Button<T>, F2>>>,
>;
type CancelConfirm<T, F0, F1> = FixedHeightBar<Split<MsgMap<Button<T>, F0>, MsgMap<Button<T>, F1>>>;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum CancelInfoConfirmMsg { pub enum CancelInfoConfirmMsg {
Cancelled, Cancelled,
@ -436,23 +536,25 @@ pub enum CancelInfoConfirmMsg {
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub struct IconText { pub struct IconText<T> {
text: &'static str, text: T,
icon: Icon, icon: Icon,
} }
impl IconText { impl<T> IconText<T>
where
T: AsRef<str>,
{
const ICON_SPACE: i16 = 46; const ICON_SPACE: i16 = 46;
const ICON_MARGIN: i16 = 4; const ICON_MARGIN: i16 = 4;
const TEXT_MARGIN: i16 = 6; const TEXT_MARGIN: i16 = 6;
pub fn new(text: &'static str, icon: Icon) -> Self { pub fn new(text: T, icon: Icon) -> Self {
Self { text, icon } Self { text, icon }
} }
pub fn paint(&self, area: Rect, style: &ButtonStyle, baseline_offset: i16) { pub fn paint(&self, area: Rect, style: &ButtonStyle, baseline_offset: Offset) {
let width = style.font.text_width(self.text); let width = style.font.text_width(self.text.as_ref());
let height = style.font.text_height();
let mut use_icon = false; let mut use_icon = false;
let mut use_text = false; let mut use_text = false;
@ -461,8 +563,7 @@ impl IconText {
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2), area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
area.center().y, area.center().y,
); );
let mut text_pos = let mut text_pos = area.left_center() + baseline_offset;
area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset);
if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) { if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) {
//display both icon and text //display both icon and text
@ -480,7 +581,7 @@ impl IconText {
if use_text { if use_text {
display::text_left( display::text_left(
text_pos, text_pos,
self.text, self.text.as_ref(),
style.font, style.font,
style.text_color, style.text_color,
style.button_color, style.button_color,
@ -496,16 +597,14 @@ impl IconText {
); );
} }
} }
pub fn render<'s>( pub fn render<'s>(
&self, & self,
target: &mut impl Renderer<'s>, target: &mut impl Renderer<'s>,
area: Rect, area: Rect,
style: &ButtonStyle, style: &ButtonStyle,
baseline_offset: i16, baseline_offset: Offset,
) { ) {
let width = style.font.text_width(self.text); let width = style.font.text_width(self.text.as_ref());
let height = style.font.text_height();
let mut use_icon = false; let mut use_icon = false;
let mut use_text = false; let mut use_text = false;
@ -514,8 +613,7 @@ impl IconText {
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2), area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
area.center().y, area.center().y,
); );
let mut text_pos = let mut text_pos = area.left_center() + baseline_offset;
area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset);
if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) { if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) {
//display both icon and text //display both icon and text
@ -531,12 +629,10 @@ impl IconText {
} }
if use_text { if use_text {
shape::Text::new(text_pos, self.text) shape::Text::new(text_pos, self.text.as_ref())
.with_font(style.font)
.with_fg(style.text_color) .with_fg(style.text_color)
.render(target); .render(target);
} }
if use_icon { if use_icon {
shape::ToifImage::new(icon_pos, self.icon.toif) shape::ToifImage::new(icon_pos, self.icon.toif)
.with_align(Alignment2D::CENTER) .with_align(Alignment2D::CENTER)

View File

@ -62,8 +62,8 @@ where
self self
} }
pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self { pub fn with_subtitle(mut self, subtitle: U) -> Self {
let style = theme::label_title_sub(); let style = theme::TEXT_SUB;
self.title = Child::new(self.title.into_inner().top_aligned()); self.title = Child::new(self.title.into_inner().top_aligned());
self.subtitle = Some(Child::new(Label::new( self.subtitle = Some(Child::new(Label::new(
subtitle, subtitle,
@ -127,7 +127,7 @@ where
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT); let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT);
let content_area = content_area.inset(Insets::right(TITLE_SPACE)); let content_area = content_area.inset(Insets::top(TITLE_SPACE));
if let Some(b) = &mut self.button { if let Some(b) = &mut self.button {
let (rest, button_area) = header_area.split_right(TITLE_HEIGHT); let (rest, button_area) = header_area.split_right(TITLE_HEIGHT);

View File

@ -22,7 +22,7 @@ use crate::{
ui::{ ui::{
constant::HEIGHT, constant::HEIGHT,
display::{ display::{
tjpgd::{jpeg_test, BufferInput}, tjpgd::BufferInput,
toif::{Toif, ToifFormat}, toif::{Toif, ToifFormat},
}, },
model_mercury::component::homescreen::render::{ model_mercury::component::homescreen::render::{

View File

@ -1,5 +1,12 @@
pub mod bl_confirm; pub mod bl_confirm;
mod button; mod button;
#[cfg(feature = "translations")]
mod coinjoin_progress;
mod dialog;
mod fido;
mod vertical_menu;
#[rustfmt::skip]
mod fido_icons;
mod error; mod error;
mod frame; mod frame;
mod loader; mod loader;
@ -13,6 +20,10 @@ pub use error::ErrorScreen;
pub use frame::{Frame, FrameMsg}; pub use frame::{Frame, FrameMsg};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use scroll::ScrollBar;
pub use simple_page::SimplePage;
pub use swipe::{Swipe, SwipeDirection};
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
pub use welcome_screen::WelcomeScreen; pub use welcome_screen::WelcomeScreen;
use super::{constant, theme}; use super::{constant, theme};

View File

@ -226,7 +226,7 @@ impl Component for NumberInput {
let mut buf = [0u8; 10]; let mut buf = [0u8; 10];
if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) {
let digit_font = Font::DEMIBOLD; let digit_font = Font::DEMIBOLD;
let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET; let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET.y;
display::rect_fill(self.area, theme::BG); display::rect_fill(self.area, theme::BG);
display::text_center( display::text_center(
self.area.center() + Offset::y(y_offset), self.area.center() + Offset::y(y_offset),
@ -245,7 +245,7 @@ impl Component for NumberInput {
if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) {
let digit_font = Font::DEMIBOLD; let digit_font = Font::DEMIBOLD;
let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET; let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET.y;
shape::Bar::new(self.area).with_bg(theme::BG).render(target); shape::Bar::new(self.area).with_bg(theme::BG).render(target);
shape::Text::new(self.area.center() + Offset::y(y_offset), text) shape::Text::new(self.area.center() + Offset::y(y_offset), text)

View File

@ -0,0 +1,156 @@
use heapless::Vec;
use super::theme;
use crate::ui::{
component::{base::Component, Event, EventCtx},
display::Icon,
geometry::Rect,
model_mercury::component::button::{Button, ButtonMsg, IconText},
shape::{Bar, Renderer},
};
pub enum VerticalMenuChoiceMsg {
Selected(usize),
}
/// Number of buttons.
/// Presently, VerticalMenu holds only fixed number of buttons.
/// TODO: for scrollable menu, the implementation must change.
const N_ITEMS: usize = 3;
/// Number of visual separators between buttons.
const N_SEPS: usize = N_ITEMS - 1;
/// Fixed height of each menu button.
const MENU_BUTTON_HEIGHT: i16 = 64;
/// Fixed height of a separator.
const MENU_SEP_HEIGHT: i16 = 2;
type VerticalMenuButtons<T> = Vec<Button<T>, N_ITEMS>;
type AreasForSeparators = Vec<Rect, N_SEPS>;
pub struct VerticalMenu<T> {
area: Rect,
/// buttons placed vertically from top to bottom
buttons: VerticalMenuButtons<T>,
/// areas for visual separators between buttons
areas_sep: AreasForSeparators,
}
impl<T> VerticalMenu<T>
where
T: AsRef<str>,
{
fn new(buttons: VerticalMenuButtons<T>) -> Self {
Self {
area: Rect::zero(),
buttons,
areas_sep: AreasForSeparators::new(),
}
}
pub fn select_word(words: [T; 3]) -> Self {
let mut buttons_vec = VerticalMenuButtons::new();
for word in words {
let button = Button::with_text(word).styled(theme::button_vertical_menu());
unwrap!(buttons_vec.push(button));
}
Self::new(buttons_vec)
}
pub fn context_menu(options: [(T, Icon); 3]) -> Self {
// TODO: this is just POC
let mut buttons_vec = VerticalMenuButtons::new();
for opt in options {
let button_theme;
match opt.1 {
theme::ICON_CANCEL => {
button_theme = theme::button_vertical_menu_orange();
}
_ => {
button_theme = theme::button_vertical_menu();
}
}
unwrap!(buttons_vec.push(
Button::with_icon_and_text(IconText::new(opt.0, opt.1)).styled(button_theme)
));
}
Self::new(buttons_vec)
}
}
impl<T> Component for VerticalMenu<T>
where
T: AsRef<str>,
{
type Msg = VerticalMenuChoiceMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// VerticalMenu is supposed to be used in Frame, the remaining space is just
// enought to fit 3 buttons separated by thin bars
let height_bounds_expected = 3 * MENU_BUTTON_HEIGHT + 2 * MENU_SEP_HEIGHT;
assert!(bounds.height() == height_bounds_expected);
self.area = bounds;
self.areas_sep.clear();
let mut remaining = bounds;
for i in 0..N_ITEMS {
let (area_button, new_remaining) = remaining.split_top(MENU_BUTTON_HEIGHT);
self.buttons[i].place(area_button);
remaining = new_remaining;
if i < N_SEPS {
let (area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT);
unwrap!(self.areas_sep.push(area_sep));
remaining = new_remaining;
}
}
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
for (i, button) in self.buttons.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = button.event(ctx, event) {
return Some(VerticalMenuChoiceMsg::Selected(i));
}
}
None
}
fn paint(&mut self) {
// TODO remove when ui-t3t1 done
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// render buttons separated by thin bars
for button in &self.buttons {
button.render(target);
}
for area in self.areas_sep.iter() {
Bar::new(*area)
.with_thickness(MENU_SEP_HEIGHT)
.with_fg(theme::GREY_EXTRA_DARK)
.render(target);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for VerticalMenu<T>
where
T: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("VerticalMenu");
t.in_list("buttons", &|button_list| {
for button in &self.buttons {
button_list.child(button);
}
});
}
}

View File

@ -34,7 +34,7 @@ use crate::{
}, },
Border, Component, Empty, FormattedText, Label, Never, Qr, Timeout, Border, Component, Empty, FormattedText, Label, Never, Qr, Timeout,
}, },
display::tjpgd::jpeg_info, display::{tjpgd::jpeg_info, Icon},
geometry, geometry,
layout::{ layout::{
obj::{ComponentMsgObj, LayoutObj}, obj::{ComponentMsgObj, LayoutObj},
@ -52,7 +52,8 @@ use super::{
FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput,
MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg,
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, VerticalMenu,
VerticalMenuChoiceMsg,
}, },
theme, theme,
}; };
@ -100,6 +101,16 @@ impl TryFrom<SelectWordCountMsg> for Obj {
} }
} }
impl TryFrom<VerticalMenuChoiceMsg> for Obj {
type Error = Error;
fn try_from(value: VerticalMenuChoiceMsg) -> Result<Self, Self::Error> {
match value {
VerticalMenuChoiceMsg::Selected(i) => i.try_into(),
}
}
}
impl<F, T, U> ComponentMsgObj for FidoConfirm<F, T, U> impl<F, T, U> ComponentMsgObj for FidoConfirm<F, T, U>
where where
F: Fn(usize) -> T, F: Fn(usize) -> T,
@ -195,6 +206,17 @@ where
} }
} }
impl<T> ComponentMsgObj for VerticalMenu<T>
where
T: AsRef<str>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
VerticalMenuChoiceMsg::Selected(i) => i.try_into(),
}
}
}
impl<T, U> ComponentMsgObj for ButtonPage<T, U> impl<T, U> ComponentMsgObj for ButtonPage<T, U>
where where
T: Component + Paginate, T: Component + Paginate,
@ -1299,11 +1321,30 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map)
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
let words: [StrBuffer; 3] = util::iter_into_array(words_iterable)?; let words: [StrBuffer; 3] = util::iter_into_array(words_iterable)?;
let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_DEMIBOLD, description)]); let content = VerticalMenu::select_word(words);
let obj = LayoutObj::new(Frame::left_aligned( let frame_with_menu = Frame::left_aligned(title, content).with_subtitle(description);
title, let obj = LayoutObj::new(frame_with_menu)?;
Dialog::new(paragraphs, Button::select_word(words)), Ok(obj.into())
))?; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_tx_context_menu(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], _kwargs: &Map| {
// TODO: this is just POC
let title: StrBuffer = StrBuffer::from("");
let options: [(StrBuffer, Icon); 3] = [
(StrBuffer::from("Address QR code"), theme::ICON_QR_CODE),
(
StrBuffer::from("Fee info"),
theme::ICON_CHEVRON_RIGHT,
),
(StrBuffer::from("Cancel transaction"), theme::ICON_CANCEL),
];
let content = VerticalMenu::context_menu(options);
let frame_with_menu = Frame::left_aligned(title, content).with_cancel_button();
let obj = LayoutObj::new(frame_with_menu)?;
Ok(obj.into()) Ok(obj.into())
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -2033,6 +2074,11 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// iterable must be of exact size. Returns index in range `0..3`.""" /// iterable must be of exact size. Returns index in range `0..3`."""
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
/// def show_tx_context_menu() -> LayoutObj[int]:
/// """Show transaction context menu with the options for 1) Address QR code, 2) Fee
/// information, 3) Cancel transaction"""
Qstr::MP_QSTR_show_tx_context_menu => obj_fn_kw!(0, new_show_tx_context_menu).as_obj(),
/// def show_share_words( /// def show_share_words(
/// *, /// *,
/// title: str, /// title: str,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

After

Width:  |  Height:  |  Size: 226 B

View File

@ -44,7 +44,7 @@ pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41); pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// Commonly used corner radius (i.e. for buttons). // Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2; pub const RADIUS: u8 = 0;
// UI icons (greyscale). // UI icons (greyscale).
@ -179,6 +179,298 @@ pub const fn button_moreinfo() -> ButtonStyleSheet {
} }
} }
pub const fn button_info() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: BLUE,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: BLUE_DARK,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BLUE,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_vertical_menu() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::NORMAL,
text_color: GREY_EXTRA_LIGHT,
button_color: BG,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
// TODO: change when figma done
active: &ButtonStyle {
font: Font::NORMAL,
text_color: FG,
button_color: GREEN_LIME,
background_color: GREY_EXTRA_DARK,
border_color: GREEN_LIME,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::NORMAL,
text_color: GREY_LIGHT,
button_color: GREEN_LIME,
background_color: GREY_EXTRA_DARK,
border_color: GREEN_LIME,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_vertical_menu_orange() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::NORMAL,
text_color: ORANGE_LIGHT,
button_color: BG,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::NORMAL,
text_color: FG,
button_color: GREEN_LIME,
background_color: GREY_EXTRA_DARK,
border_color: GREEN_LIME,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::NORMAL,
text_color: GREY_LIGHT,
button_color: GREEN_LIME,
background_color: GREY_EXTRA_DARK,
border_color: GREEN_LIME,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_pin() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_MEDIUM,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: BG, // so there is no "button" itself, just the text
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_pin_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_DARK,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_pin_autocomplete() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_DARK, // same as PIN buttons
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_DARK,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: BG,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: GREEN_DARK,
button_color: GREEN,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_DARK,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_suggestion_autocomplete() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: GREY_DARK, // same as PIN buttons
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_DARK,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
button_color: BG,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_counter() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: GREY_MEDIUM,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_clear() -> ButtonStyleSheet {
button_default()
}
pub const fn loader_default() -> LoaderStyleSheet { pub const fn loader_default() -> LoaderStyleSheet {
LoaderStyleSheet { LoaderStyleSheet {
normal: &LoaderStyle { normal: &LoaderStyle {

View File

@ -389,6 +389,12 @@ def select_word(
iterable must be of exact size. Returns index in range `0..3`.""" iterable must be of exact size. Returns index in range `0..3`."""
# rust/src/ui/model_mercury/layout.rs
def show_tx_context_menu() -> LayoutObj[int]:
"""Show transaction context menu with the options for 1) Address QR code, 2) Fee
information, 3) Cancel transaction"""
# rust/src/ui/model_mercury/layout.rs # rust/src/ui/model_mercury/layout.rs
def show_share_words( def show_share_words(
*, *,