1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-18 05:28:40 +00:00

feat(core/rust): add example layout for T1

[no changelog]
This commit is contained in:
Martin Milata 2021-10-29 13:06:16 +02:00
parent 4d60c10330
commit afd7cb3b01
18 changed files with 669 additions and 25 deletions

View File

@ -23,16 +23,37 @@
#include "librust.h"
#if TREZOR_MODEL == T
/// def layout_new_example(text: str) -> None:
/// """Example layout."""
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj,
ui_layout_new_example);
#elif TREZOR_MODEL == 1
/// def layout_new_confirm_action(
/// title: str,
/// action: str | None,
/// description: str | None,
/// verb: str | None,
/// verb_cancel: str | None,
/// hold: bool | None,
/// reverse: bool,
/// ) -> int:
/// """Example layout. All arguments must be passed as kwargs."""
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_action_obj,
0, ui_layout_new_confirm_action);
#endif
STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = {
#if TREZOR_MODEL == T
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorui2)},
{MP_ROM_QSTR(MP_QSTR_layout_new_example),
MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)},
#elif TREZOR_MODEL == 1
{MP_ROM_QSTR(MP_QSTR_layout_new_confirm_action),
MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_action_obj)},
#endif
};
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorui2_globals,

View File

@ -13,6 +13,8 @@ mp_obj_t protobuf_debug_msg_def_type();
#endif
mp_obj_t ui_layout_new_example(mp_obj_t);
mp_obj_t ui_layout_new_confirm_action(size_t n_args, const mp_obj_t *args,
mp_map_t *kwargs);
#ifdef TREZOR_EMULATOR
mp_obj_t ui_debug_layout_type();

View File

@ -16,4 +16,11 @@ static void _librust_qstrs(void) {
MP_QSTR_timer;
MP_QSTR_paint;
MP_QSTR_trace;
MP_QSTR_title;
MP_QSTR_action;
MP_QSTR_description;
MP_QSTR_verb;
MP_QSTR_verb_cancel;
MP_QSTR_reverse;
}

View File

@ -380,6 +380,23 @@ where
}
}
impl Obj {
/// Conversion to Rust types with typed `None`.
pub fn try_into_option<T>(self) -> Result<Option<T>, Error>
where
T: TryFrom<Obj>,
<T as TryFrom<Obj>>::Error: Into<Error>,
{
if self == Obj::const_none() {
return Ok(None);
}
match self.try_into() {
Ok(x) => Ok(Some(x)),
Err(e) => Err(e.into()),
}
}
}
impl From<TryFromIntError> for Error {
fn from(_: TryFromIntError) -> Self {
Self::OutOfRange

View File

@ -1,3 +0,0 @@
pub mod constants;
pub mod theme;

View File

@ -60,6 +60,27 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
);
}
// Used on T1 only.
pub fn rounded_rect1(r: Rect, fg_color: Color, bg_color: Color) {
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
let corners = [
r.top_left(),
r.top_right() + Offset::new(-1, 0),
r.bottom_right() + Offset::new(-1, -1),
r.bottom_left() + Offset::new(0, -1),
];
for p in corners.iter() {
display::bar(p.x, p.y, 1, 1, bg_color.into());
}
}
// Used on T1 only.
pub fn dotted_line(start: Point, width: i32, color: Color) {
for x in (start.x..width).step_by(2) {
display::bar(x, start.y, 1, 1, color.into());
}
}
pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) {
display::text(
baseline.x,
@ -138,6 +159,10 @@ impl Color {
pub fn to_u16(self) -> u16 {
self.0
}
pub fn neg(self) -> Self {
Self(!self.0)
}
}
impl From<u16> for Color {

View File

@ -180,6 +180,54 @@ impl Rect {
y1: self.y1,
}
}
pub fn hsplit(self, height: i32) -> (Self, Self) {
let height = if height.is_positive() {
height
} else {
self.height() + height
};
let top = Self {
x0: self.x0,
y0: self.y0,
x1: self.x1,
y1: self.y0 + height,
};
let bottom = Self {
x0: self.x0,
y0: top.y0 + height,
x1: self.x1,
y1: self.y1,
};
(top, bottom)
}
pub fn vsplit(self, width: i32) -> (Self, Self) {
let width = if width.is_positive() {
width
} else {
self.width() + width
};
let left = Self {
x0: self.x0,
y0: self.y0,
x1: self.x0 + width,
y1: self.y1,
};
let right = Self {
x0: left.x0 + width,
y0: self.y0,
x1: self.x1,
y1: self.y1,
};
(left, right)
}
}
#[derive(Copy, Clone, PartialEq, Eq)]

View File

@ -0,0 +1,193 @@
use crate::ui::{
component::{Component, Event, EventCtx},
display::{self, Color, Font},
geometry::{Offset, Point, Rect},
};
use super::{
event::{ButtonEvent, T1Button},
theme,
};
pub enum ButtonMsg {
Clicked,
}
#[derive(Copy, Clone)]
pub enum ButtonPos {
Left,
Right,
}
impl ButtonPos {
fn hit(&self, b: &T1Button) -> bool {
matches!(
(self, b),
(Self::Left, T1Button::Left) | (Self::Right, T1Button::Right)
)
}
}
pub struct Button<T> {
area: Rect,
pos: ButtonPos,
baseline: Point,
content: ButtonContent<T>,
styles: ButtonStyleSheet,
state: State,
}
impl<T: AsRef<[u8]>> Button<T> {
pub fn new(
area: Rect,
pos: ButtonPos,
content: ButtonContent<T>,
styles: ButtonStyleSheet,
) -> Self {
let (area, baseline) = Self::placement(area, pos, &content, &styles);
Self {
area,
pos,
baseline,
content,
styles,
state: State::Released,
}
}
pub fn with_text(area: Rect, pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self {
Self::new(area, pos, ButtonContent::Text(text), styles)
}
pub fn with_icon(
area: Rect,
pos: ButtonPos,
image: &'static [u8],
styles: ButtonStyleSheet,
) -> Self {
Self::new(area, pos, ButtonContent::Icon(image), styles)
}
pub fn content(&self) -> &ButtonContent<T> {
&self.content
}
fn style(&self) -> &ButtonStyle {
match self.state {
State::Released => self.styles.normal,
State::Pressed => self.styles.active,
}
}
fn set(&mut self, ctx: &mut EventCtx, state: State) {
if self.state != state {
self.state = state;
ctx.request_paint();
}
}
fn placement(
area: Rect,
pos: ButtonPos,
content: &ButtonContent<T>,
styles: &ButtonStyleSheet,
) -> (Rect, Point) {
let border_width = if styles.normal.border_horiz { 2 } else { 0 };
let content_width = match content {
ButtonContent::Text(text) => display::text_width(text.as_ref(), styles.normal.font) - 1,
ButtonContent::Icon(_icon) => todo!(),
};
let button_width = content_width + 2 * border_width;
let area = match pos {
ButtonPos::Left => area.vsplit(button_width).0,
ButtonPos::Right => area.vsplit(-button_width).1,
};
let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2);
return (area, start_of_baseline);
}
}
impl<T: AsRef<[u8]>> Component for Button<T> {
type Msg = ButtonMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
self.set(ctx, State::Pressed);
}
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
if matches!(self.state, State::Pressed) {
self.set(ctx, State::Released);
return Some(ButtonMsg::Clicked);
}
}
_ => {}
};
None
}
fn paint(&mut self) {
let style = self.style();
match &self.content {
ButtonContent::Text(text) => {
let background_color = style.text_color.neg();
if style.border_horiz {
display::rounded_rect1(self.area, background_color, theme::BG);
} else {
display::rect(self.area, background_color)
}
display::text(
self.baseline,
text.as_ref(),
style.font,
style.text_color,
background_color,
);
}
ButtonContent::Icon(_image) => {
todo!();
}
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Button<T>
where
T: AsRef<[u8]> + crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Button");
match &self.content {
ButtonContent::Text(text) => t.field("text", text),
ButtonContent::Icon(_) => t.symbol("icon"),
}
t.close();
}
}
#[derive(PartialEq, Eq)]
enum State {
Released,
Pressed,
}
pub enum ButtonContent<T> {
Text(T),
Icon(&'static [u8]),
}
pub struct ButtonStyleSheet {
pub normal: &'static ButtonStyle,
pub active: &'static ButtonStyle,
}
pub struct ButtonStyle {
pub font: Font,
pub text_color: Color,
pub border_horiz: bool,
}

View File

@ -0,0 +1,117 @@
use super::{
button::{Button, ButtonMsg::Clicked, ButtonPos},
theme,
};
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display,
geometry::{Offset, Rect},
};
pub enum DialogMsg<T> {
Content(T),
LeftClicked,
RightClicked,
}
pub struct Dialog<T, U> {
header: Option<(U, Rect)>,
content: Child<T>,
left_btn: Option<Child<Button<U>>>,
right_btn: Option<Child<Button<U>>>,
}
impl<T: Component, U: AsRef<[u8]>> Dialog<T, U> {
pub fn new(
area: Rect,
content: impl FnOnce(Rect) -> T,
left: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
right: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
header: Option<U>,
) -> Self {
let (header_area, content_area, button_area) = Self::areas(area, &header);
let content = Child::new(content(content_area));
let left_btn = left.map(|f| Child::new(f(button_area, ButtonPos::Left)));
let right_btn = right.map(|f| Child::new(f(button_area, ButtonPos::Right)));
Self {
header: header.zip(header_area),
content,
left_btn,
right_btn,
}
}
fn paint_header(&self) {
if let Some((title, area)) = &self.header {
display::text(
area.bottom_left() + Offset::new(0, -2),
title.as_ref(),
theme::FONT_BOLD,
theme::FG,
theme::BG,
);
display::dotted_line(area.bottom_left(), area.width(), theme::FG)
}
}
fn areas(area: Rect, header: &Option<U>) -> (Option<Rect>, Rect, Rect) {
const HEADER_SPACE: i32 = 4;
let button_height = theme::FONT_BOLD.line_height() + 2;
let header_height = theme::FONT_BOLD.line_height();
let (content_area, button_area) = area.hsplit(-button_height);
if header.is_none() {
(None, content_area, button_area)
} else {
let (header_area, content_area) = content_area.hsplit(header_height);
let (_space, content_area) = content_area.hsplit(HEADER_SPACE);
(Some(header_area), content_area, button_area)
}
}
}
impl<T: Component, U: AsRef<[u8]>> Component for Dialog<T, U> {
type Msg = DialogMsg<T::Msg>;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(msg) = self.content.event(ctx, event) {
Some(DialogMsg::Content(msg))
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
Some(DialogMsg::LeftClicked)
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
Some(DialogMsg::RightClicked)
} else {
None
}
}
fn paint(&mut self) {
self.paint_header();
self.content.paint();
if let Some(b) = self.left_btn.as_mut() {
b.paint();
}
if let Some(b) = self.right_btn.as_mut() {
b.paint();
}
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Dialog<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace + AsRef<[u8]>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Dialog");
t.field("content", &self.content);
if let Some(label) = &self.left_btn {
t.field("left", label);
}
if let Some(label) = &self.right_btn {
t.field("right", label);
}
t.close();
}
}

View File

@ -0,0 +1,7 @@
mod button;
mod dialog;
use super::{event, theme};
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
pub use dialog::{Dialog, DialogMsg};

View File

@ -13,7 +13,7 @@ pub enum ButtonEvent {
}
impl ButtonEvent {
pub fn new(event: u32, button: u32, _unused: u32) -> Result<Self, error::Error> {
pub fn new(event: u32, button: u32) -> Result<Self, error::Error> {
let button = match button {
0 => T1Button::Left,
1 => T1Button::Right,

View File

@ -0,0 +1,146 @@
use core::convert::{TryFrom, TryInto};
use crate::{
error::Error,
micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr},
ui::{
component::{Child, Text},
display,
layout::obj::LayoutObj,
},
util,
};
use super::{
component::{Button, Dialog, DialogMsg},
theme,
};
impl<T> TryFrom<DialogMsg<T>> for Obj
where
Obj: TryFrom<T>,
Error: From<<T as TryInto<Obj>>::Error>,
{
type Error = Error;
fn try_from(val: DialogMsg<T>) -> Result<Self, Self::Error> {
match val {
DialogMsg::Content(c) => Ok(c.try_into()?),
DialogMsg::LeftClicked => 1.try_into(),
DialogMsg::RightClicked => 2.try_into(),
}
}
}
#[no_mangle]
extern "C" fn ui_layout_new_confirm_action(
n_args: usize,
args: *const Obj,
kwargs: *const Map,
) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let title: Buffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let action: Option<Buffer> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
let description: Option<Buffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let verb: Option<Buffer> = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?;
let verb_cancel: Option<Buffer> =
kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into_option()?;
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
let format = match (&action, &description, reverse) {
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
(Some(_), None, _) => "{bold}{action}",
(None, Some(_), _) => "{normal}{description}",
_ => "",
};
let left = verb_cancel
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_cancel()));
let right = verb
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_default()));
let obj = LayoutObj::new(Child::new(Dialog::new(
display::screen(),
|area| {
Text::new::<theme::T1DefaultText>(area, format)
.with(b"action", action.unwrap_or("".into()))
.with(b"description", description.unwrap_or("".into()))
},
left,
right,
Some(title),
)))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
#[cfg(test)]
mod tests {
use crate::trace::{Trace, Tracer};
use super::*;
impl Tracer for Vec<u8> {
fn bytes(&mut self, b: &[u8]) {
self.extend(b)
}
fn string(&mut self, s: &str) {
self.extend(s.as_bytes())
}
fn symbol(&mut self, name: &str) {
self.extend(name.as_bytes())
}
fn open(&mut self, name: &str) {
self.extend(b"<");
self.extend(name.as_bytes());
self.extend(b" ");
}
fn field(&mut self, name: &str, value: &dyn Trace) {
self.extend(name.as_bytes());
self.extend(b":");
value.trace(self);
self.extend(b" ");
}
fn close(&mut self) {
self.extend(b">")
}
}
fn trace(val: &impl Trace) -> String {
let mut t = Vec::new();
val.trace(&mut t);
String::from_utf8(t).unwrap()
}
#[test]
fn trace_example_layout() {
let layout = Child::new(Dialog::new(
display::screen(),
|area| {
Text::new(
area,
"Testing text layout, with some text, and some more text. And {param}",
)
.with(b"param", b"parameters!")
},
Some(|area, pos| Button::with_text(area, pos, "Left", theme::button_cancel())),
Some(|area, pos| Button::with_text(area, pos, "Right", theme::button_default())),
None,
));
assert_eq!(
trace(&layout),
r#"<Dialog content:<Text content:Testing text layout,
with some text, and
some more text. And p-
arameters! > left:<Button text:Left > right:<Button text:Right > >"#
)
}
}

View File

@ -1,3 +1,5 @@
pub mod component;
pub mod constant;
pub mod event;
pub mod layout;
pub mod theme;

View File

@ -1,4 +1,7 @@
use crate::ui::display::{Color, Font};
use crate::ui::{
component::text::DefaultTextTheme,
display::{Color, Font},
};
use super::component::{ButtonStyle, ButtonStyleSheet};
@ -19,19 +22,11 @@ pub fn button_default() -> ButtonStyleSheet {
normal: &ButtonStyle {
font: FONT_BOLD,
text_color: BG,
background_color: FG,
border_horiz: true,
},
active: &ButtonStyle {
font: FONT_BOLD,
text_color: FG,
background_color: BG,
border_horiz: true,
},
disabled: &ButtonStyle {
font: FONT_BOLD,
text_color: FG,
background_color: BG,
border_horiz: true,
},
}
@ -42,20 +37,28 @@ pub fn button_cancel() -> ButtonStyleSheet {
normal: &ButtonStyle {
font: FONT_BOLD,
text_color: FG,
background_color: BG,
border_horiz: false,
},
active: &ButtonStyle {
font: FONT_BOLD,
text_color: BG,
background_color: FG,
border_horiz: false,
},
disabled: &ButtonStyle {
font: FONT_BOLD,
text_color: BG,
background_color: FG,
border_horiz: false,
},
}
}
pub enum T1DefaultText {}
impl DefaultTextTheme for T1DefaultText {
const BACKGROUND_COLOR: Color = BG;
const TEXT_FONT: Font = FONT_NORMAL;
const TEXT_COLOR: Color = FG;
const HYPHEN_FONT: Font = FONT_NORMAL;
const HYPHEN_COLOR: Color = FG;
const ELLIPSIS_FONT: Font = FONT_NORMAL;
const ELLIPSIS_COLOR: Color = FG;
const NORMAL_FONT: Font = FONT_NORMAL;
const BOLD_FONT: Font = FONT_BOLD;
const MONO_FONT: Font = FONT_MONO;
}

View File

@ -4,3 +4,16 @@ from typing import *
# extmod/rustmods/modtrezorui2.c
def layout_new_example(text: str) -> None:
"""Example layout."""
# extmod/rustmods/modtrezorui2.c
def layout_new_confirm_action(
title: str,
action: str | None,
description: str | None,
verb: str | None,
verb_cancel: str | None,
hold: bool | None,
reverse: bool,
) -> int:
"""Example layout. All arguments must be passed as kwargs."""

View File

@ -6,3 +6,4 @@ TEXT_LINE_HEIGHT_HALF = const(4)
TEXT_MARGIN_LEFT = const(0)
TEXT_MAX_LINES = const(4)
TEXT_MAX_LINES_NO_HEADER = const(5)
PAGINATION_MARGIN_RIGHT = const(4)

View File

@ -11,7 +11,10 @@ if False:
if __debug__:
from ..components.tt.scroll import Paginated
from trezor import utils
if utils.MODEL == "T":
from ..components.tt.scroll import Paginated
async def button_request(

View File

@ -1,6 +1,10 @@
from trezor import ui, wire
from trezor import log, ui, wire
from trezor.enums import ButtonRequestType
from trezorui2 import layout_new_confirm_action
from .common import interact
if False:
from typing import NoReturn, Type, Union
@ -16,7 +20,7 @@ async def confirm_action(
description_param: str | None = None,
description_param_font: int = ui.BOLD,
verb: str | bytes | None = "OK",
verb_cancel: str | bytes | None = "X",
verb_cancel: str | bytes | None = "cancel",
hold: bool = False,
hold_danger: bool = False,
icon: str | None = None,
@ -26,7 +30,35 @@ async def confirm_action(
exc: ExceptionType = wire.ActionCancelled,
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
raise NotImplementedError
if isinstance(verb, bytes) or isinstance(verb_cancel, bytes):
raise NotImplementedError
if description is not None and description_param is not None:
if description_param_font != ui.BOLD:
log.error(__name__, "confirm_action description_param_font not implemented")
description = description.format(description_param)
if hold:
log.error(__name__, "confirm_action hold not implemented")
result = await interact(
ctx,
ui.RustLayout(
layout_new_confirm_action(
title=title.upper(),
action=action,
description=description,
verb=verb,
verb_cancel=verb_cancel,
hold=hold,
reverse=reverse,
)
),
br_type,
br_code,
)
if result == 1:
raise exc
async def show_error_and_raise(
@ -40,3 +72,13 @@ async def show_error_and_raise(
exc: ExceptionType = wire.ActionCancelled,
) -> NoReturn:
raise NotImplementedError
async def show_popup(
title: str,
description: str,
subtitle: str | None = None,
description_param: str = "",
timeout_ms: int = 3000,
) -> None:
raise NotImplementedError