1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-07 23:28:07 +00:00

WIP - implementing QR code

This commit is contained in:
grdddj 2022-11-10 11:52:20 +01:00
parent 6e9e7265c4
commit dd79a868a3
16 changed files with 325 additions and 48 deletions

View File

@ -373,7 +373,7 @@ pub struct TextRenderer;
impl LayoutSink for TextRenderer {
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) {
display::text(
display::text_left(
cursor,
text,
layout.style.text_font,
@ -383,7 +383,7 @@ impl LayoutSink for TextRenderer {
}
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
display::text(
display::text_left(
cursor,
"-",
layout.style.text_font,
@ -403,7 +403,7 @@ impl LayoutSink for TextRenderer {
layout.style.background_color,
);
} else {
display::text(
display::text_left(
cursor,
"...",
layout.style.text_font,

View File

@ -6,6 +6,8 @@ pub mod loader;
#[cfg(feature = "jpeg")]
pub mod tjpgd;
use heapless::String;
use super::{
constant,
geometry::{Offset, Point, Rect},
@ -948,11 +950,62 @@ pub fn paint_point(point: &Point, color: Color) {
display::bar(point.x, point.y, 1, 1, color.into());
}
/// Display QR code
pub fn qrcode(center: Point, data: &str, max_size: u32, case_sensitive: bool) -> Result<(), Error> {
qr::render_qrcode(center.x, center.y, data, max_size, case_sensitive)
}
pub fn text(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
/// Draws longer multiline texts inside an area.
/// Does not add any characters on the line boundaries.
///
/// If it fits, returns the rest of the area.
/// If it does not fit, returns `None`.
pub fn text_multiline(
area: Rect,
text: &str,
font: Font,
fg_color: Color,
bg_color: Color,
) -> Option<Rect> {
let line_height = font.line_height();
let characters_overall = text.chars().count();
let mut taken_from_top = 0;
let mut characters_drawn = 0;
'lines: loop {
let baseline = area.top_left() + Offset::y(line_height + taken_from_top);
if !area.contains(baseline) {
// The whole area was consumed.
return None;
}
let mut line_text: String<50> = String::new();
'characters: loop {
if let Some(character) = text.chars().nth(characters_drawn) {
unwrap!(line_text.push(character));
characters_drawn += 1;
} else {
// No more characters to draw.
break 'characters;
}
if font.text_width(&line_text) > area.width() {
// Cannot fit on the line anymore.
line_text.pop();
characters_drawn -= 1;
break 'characters;
}
}
text_left(baseline, &line_text, font, fg_color, bg_color);
if characters_drawn == characters_overall {
// No more lines to draw.
break 'lines;
}
taken_from_top += line_height;
}
// Some of the area was unused and is free to draw some further text.
Some(area.split_top(taken_from_top).1)
}
/// Display text left-alligned to a certain Point
pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text(
baseline.x,
baseline.y,
@ -963,6 +1016,7 @@ pub fn text(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color:
);
}
/// Display text centered around a certain Point
pub fn text_center(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
let w = font.text_width(text);
display::text(
@ -975,6 +1029,7 @@ pub fn text_center(baseline: Point, text: &str, font: Font, fg_color: Color, bg_
);
}
/// Display text right-alligned to a certain Point
pub fn text_right(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
let w = font.text_width(text);
display::text(

View File

@ -137,7 +137,7 @@ where
display::rect_fill(self.area, background_color)
}
display::text(
display::text_left(
self.baseline,
text.as_ref(),
style.font,

View File

@ -52,7 +52,7 @@ where
}
fn paint(&mut self) {
display::text(
display::text_left(
self.area.bottom_left() - Offset::y(2),
self.title.as_ref(),
Font::BOLD,

View File

@ -249,7 +249,7 @@ where
match &self.content {
ButtonContent::Text(text) => {
display::text(
display::text_left(
self.get_text_baseline(&style),
text.as_ref(),
style.font,

View File

@ -9,12 +9,12 @@ use super::theme;
/// Display white text on black background
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text(baseline, text.as_ref(), font, theme::FG, theme::BG);
display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display black text on white background
pub fn display_inverse<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text(baseline, text.as_ref(), font, theme::BG, theme::FG);
display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG);
}
/// Display white text on black background,

View File

@ -500,7 +500,7 @@ impl LayoutSink for TextRenderer {
match layout.style.line_alignment {
LineAlignment::Left => {
display::text(
display::text_left(
cursor,
text,
layout.style.text_font,
@ -532,7 +532,7 @@ impl LayoutSink for TextRenderer {
}
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
display::text(
display::text_left(
cursor,
"-",
layout.style.text_font,
@ -552,7 +552,7 @@ impl LayoutSink for TextRenderer {
layout.style.background_color,
);
} else {
display::text(
display::text_left(
cursor,
"...",
layout.style.text_font,

View File

@ -15,6 +15,7 @@ mod loader;
mod page;
mod passphrase;
mod pin;
mod qr_code;
mod result_anim;
mod result_popup;
mod scrollbar;
@ -43,6 +44,7 @@ pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::ButtonPage;
pub use passphrase::{PassphraseEntry, PassphraseEntryMsg};
pub use pin::{PinEntry, PinEntryMsg};
pub use qr_code::{QRCodePage, QRCodePageMessage};
pub use result_anim::{ResultAnim, ResultAnimMsg};
pub use result_popup::{ResultPopup, ResultPopupMsg};
pub use scrollbar::ScrollBar;

View File

@ -0,0 +1,113 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display::{self, Font},
geometry::{Rect},
};
use super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos};
pub enum QRCodePageMessage {
Confirmed,
Cancelled,
}
pub struct QRCodePage<F, T> {
title: T,
title_area: Rect,
qr_code: F,
buttons: Child<ButtonController<T>>,
}
impl<F, T> QRCodePage<F, T>
where
T: AsRef<str> + Clone,
{
pub fn new(title: T, qr_code: F, btn_layout: ButtonLayout<T>) -> Self {
Self {
title,
title_area: Rect::zero(),
qr_code,
buttons: Child::new(ButtonController::new(btn_layout)),
}
}
}
impl<F, T> Component for QRCodePage<F, T>
where
T: AsRef<str> + Clone,
F: Component,
{
type Msg = QRCodePageMessage;
fn place(&mut self, bounds: Rect) -> Rect {
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
let (qr_code_area, title_area) = content_area.split_left(theme::QR_SIDE_MAX);
self.title_area = title_area;
self.qr_code.place(qr_code_area);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
match pos {
ButtonPos::Left => {
return Some(QRCodePageMessage::Cancelled);
}
ButtonPos::Right => {
return Some(QRCodePageMessage::Confirmed);
}
_ => {}
}
}
None
}
fn paint(&mut self) {
self.qr_code.paint();
// TODO: add the Label from Suite
display::text_multiline(
self.title_area,
self.title.as_ref(),
Font::MONO,
theme::FG,
theme::BG,
);
self.buttons.paint();
}
}
#[cfg(feature = "ui_debug")]
use super::ButtonAction;
#[cfg(feature = "ui_debug")]
use heapless::String;
#[cfg(feature = "ui_debug")]
impl<F, T> crate::trace::Trace for QRCodePage<F, T>
where
T: AsRef<str> + Clone,
{
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => ButtonAction::Cancel.string(),
ButtonPos::Right => ButtonAction::Confirm.string(),
ButtonPos::Middle => ButtonAction::empty(),
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("QRCodePage");
t.kw_pair("active_page", "0");
t.kw_pair("page_count", "1");
self.report_btn_actions(t);
t.content_flag();
t.string("QR CODE");
t.string(self.title.as_ref());
t.content_flag();
t.field("buttons", &self.buttons);
t.close();
}
}

View File

@ -18,6 +18,7 @@ use crate::{
component::{
base::{Component, ComponentExt},
paginated::{PageMsg, Paginate},
painter,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecLong, Paragraphs, VecExt},
FormattedText,
},
@ -35,7 +36,7 @@ use super::{
component::{
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry,
PinEntryMsg, ShareWords, SimpleChoice, SimpleChoiceMsg,
PinEntryMsg, QRCodePage, QRCodePageMessage, ShareWords, SimpleChoice, SimpleChoiceMsg,
},
theme,
};
@ -68,6 +69,19 @@ where
}
}
impl<F, T> ComponentMsgObj for QRCodePage<F, T>
where
T: AsRef<str> + Clone,
F: Component,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
QRCodePageMessage::Confirmed => Ok(CONFIRMED.as_obj()),
QRCodePageMessage::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl ComponentMsgObj for PinEntry {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -132,22 +146,26 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
// TODO: could be replaced by Flow with one element after it supports pagination
let format = match (&action, &description, reverse) {
(Some(_), Some(_), false) => "{Font::bold}{action}\n\r{Font::normal}{description}",
(Some(_), Some(_), true) => "{Font::normal}{description}\n\r{Font::bold}{action}",
(Some(_), None, _) => "{Font::bold}{action}",
(None, Some(_), _) => "{Font::normal}{description}",
(Some(_), Some(_), false) => "{bold}{action}\n\r{mono}{description}",
(Some(_), Some(_), true) => "{mono}{description}\n\r{bold}{action}",
(Some(_), None, _) => "{bold}{action}",
(None, Some(_), _) => "{mono}{description}",
_ => "",
};
let verb_cancel = verb_cancel.unwrap_or_default();
let verb = verb.unwrap_or_default();
let cancel_btn = if verb_cancel.len() > 0 {
Some(ButtonDetails::cancel_icon())
// Left button - icon, text or nothing.
let cancel_btn = if let Some(verb_cancel) = verb_cancel {
if verb_cancel.len() > 0 {
Some(ButtonDetails::text(verb_cancel))
} else {
Some(ButtonDetails::cancel_icon())
}
} else {
None
};
// Right button - text or nothing.
let verb = verb.unwrap_or_default();
let mut confirm_btn = if verb.len() > 0 {
Some(ButtonDetails::text(verb))
} else {
@ -350,6 +368,29 @@ extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let verb_cancel: StrBuffer = kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let verb: StrBuffer = "CONFIRM".into();
let qr_code = painter::qrcode_painter(address, theme::QR_SIDE_MAX as u32, case_sensitive);
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::text(verb_cancel)),
None,
Some(ButtonDetails::text(verb)),
);
let obj = LayoutObj::new(QRCodePage::new(title, qr_code, btn_layout))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
/// General pattern of most tutorial screens.
/// (title, text, btn_layout, btn_actions)
fn tutorial_screen(
@ -611,6 +652,16 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm summary of a transaction. Specific for model R."""
Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, confirm_total).as_obj(),
/// def show_qr(
/// *,
/// title: str,
/// address: str,
/// verb_cancel: str,
/// case_sensitive: bool,
/// ) -> object:
/// """Show QR code."""
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, show_qr).as_obj(),
/// def tutorial() -> object:
/// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),

View File

@ -80,3 +80,6 @@ pub const ICON_WARNING: IconAndName =
pub const BUTTON_CONTENT_HEIGHT: i16 = 7;
pub const BUTTON_OUTLINE: i16 = 3;
pub const BUTTON_HEIGHT: i16 = BUTTON_CONTENT_HEIGHT + 2 * BUTTON_OUTLINE;
// Full-size QR code.
pub const QR_SIDE_MAX: i16 = 64 - BUTTON_HEIGHT;

View File

@ -189,7 +189,7 @@ impl<T> Button<T> {
let start_of_baseline = self.area.center()
+ Offset::new(-width / 2, height / 2)
+ Offset::y(Self::BASELINE_OFFSET);
display::text(
display::text_left(
start_of_baseline,
text,
style.font,

View File

@ -109,7 +109,7 @@ impl Component for Bip39Input {
// Content starts in the left-center point, offset by 16px to the right and 8px
// to the bottom.
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
display::text(
display::text_left(
text_baseline,
text,
style.font,
@ -120,7 +120,7 @@ impl Component for Bip39Input {
// Paint the rest of the suggested dictionary word.
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
let word_baseline = text_baseline + Offset::new(width, 0);
display::text(
display::text_left(
word_baseline,
word,
style.font,

View File

@ -327,7 +327,7 @@ impl Component for Input {
let ellipsis_width = style.text_font.text_width(ellipsis);
// Drawing the ellipsis and moving the baseline for the rest of the text.
display::text(
display::text_left(
text_baseline,
ellipsis,
style.text_font,
@ -346,7 +346,7 @@ impl Component for Input {
&text[text.len() - chars_from_right..]
};
display::text(
display::text_left(
text_baseline,
text_to_display,
style.text_font,

View File

@ -158,7 +158,7 @@ impl Component for Slip39Input {
.assert_if_debugging_ui("Text buffer is too small");
}
}
display::text(
display::text_left(
text_baseline,
text.as_str(),
style.font,

View File

@ -425,7 +425,7 @@ async def get_bool(
data: str,
description: str | None = None,
verb: str | None = "CONFIRM",
verb_cancel: str | None = "CANCEL",
verb_cancel: str | None = "",
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> bool:
@ -585,16 +585,27 @@ async def confirm_path_warning(
)
def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout:
content = RustLayout(
trezorui2.confirm_text(
title=title.upper(),
data=xpub,
# verb_cancel=cancel,
)
)
return content
async def show_xpub(
ctx: wire.GenericContext, xpub: str, title: str, cancel: str
) -> None:
return await _placeholder_confirm(
ctx=ctx,
br_type="show_xpub",
title=title.upper(),
data=xpub,
description="",
br_code=ButtonRequestType.PublicKey,
await raise_if_cancelled(
interact(
ctx,
_show_xpub(xpub, title, cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
)
@ -611,20 +622,62 @@ async def show_address(
address_extra: str | None = None,
title_qr: str | None = None,
) -> None:
text = ""
is_multisig = len(xpubs) > 0
# TODO: replace with confirm_blob
data = address
if network:
text += f"{network} network\n"
data += f"\n\n{network}"
if address_extra:
text += f"{address_extra}\n"
text += address
return await _placeholder_confirm(
ctx=ctx,
br_type="show_address",
title=title.upper(),
data=text,
description="",
br_code=ButtonRequestType.Address,
)
data += f"\n\n{address_extra}"
while True:
result = await interact(
ctx,
RustLayout(
trezorui2.confirm_action(
title=title.upper(),
action=data,
description=None,
verb="CONFIRM",
verb_cancel="QR",
reverse=False,
hold=False,
)
),
"show_address",
ButtonRequestType.Address,
)
if result is trezorui2.CONFIRMED:
break
result = await interact(
ctx,
RustLayout(
trezorui2.show_qr(
address=address if address_qr is None else address_qr,
case_sensitive=case_sensitive,
title=title.upper() if title_qr is None else title_qr.upper(),
verb_cancel="XPUBs" if is_multisig else "ADDRESS",
)
),
"show_qr",
ButtonRequestType.Address,
)
if result is trezorui2.CONFIRMED:
break
if is_multisig:
for i, xpub in enumerate(xpubs):
cancel = "NEXT" if i < len(xpubs) - 1 else "ADDRESS"
title_xpub = f"XPUB #{i + 1}"
title_xpub += " (yours)" if i == multisig_index else " (cosigner)"
result = await interact(
ctx,
_show_xpub(xpub, title=title_xpub, cancel=cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
if result is trezorui2.CONFIRMED:
return
def show_pubkey(