mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-30 18:38:27 +00:00
WIP - special share words component
This commit is contained in:
parent
2487bdf932
commit
c2f0f8f914
@ -18,6 +18,7 @@ mod pin;
|
|||||||
mod result_anim;
|
mod result_anim;
|
||||||
mod result_popup;
|
mod result_popup;
|
||||||
mod scrollbar;
|
mod scrollbar;
|
||||||
|
mod share_words;
|
||||||
mod simple_choice;
|
mod simple_choice;
|
||||||
|
|
||||||
use super::theme;
|
use super::theme;
|
||||||
@ -45,4 +46,5 @@ pub use pin::{PinEntry, PinEntryMsg};
|
|||||||
pub use result_anim::{ResultAnim, ResultAnimMsg};
|
pub use result_anim::{ResultAnim, ResultAnimMsg};
|
||||||
pub use result_popup::{ResultPopup, ResultPopupMsg};
|
pub use result_popup::{ResultPopup, ResultPopupMsg};
|
||||||
pub use scrollbar::ScrollBar;
|
pub use scrollbar::ScrollBar;
|
||||||
|
pub use share_words::ShareWords;
|
||||||
pub use simple_choice::{SimpleChoice, SimpleChoiceMsg};
|
pub use simple_choice::{SimpleChoice, SimpleChoiceMsg};
|
||||||
|
@ -20,6 +20,8 @@ pub struct ButtonPage<S, T> {
|
|||||||
back_btn_details: Option<ButtonDetails<S>>,
|
back_btn_details: Option<ButtonDetails<S>>,
|
||||||
next_btn_details: Option<ButtonDetails<S>>,
|
next_btn_details: Option<ButtonDetails<S>>,
|
||||||
buttons: Child<ButtonController<S>>,
|
buttons: Child<ButtonController<S>>,
|
||||||
|
/// Scrollbar may or may not be shown (but will be counting pages anyway).
|
||||||
|
show_scrollbar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ButtonPage<&'static str, T>
|
impl<T> ButtonPage<&'static str, T>
|
||||||
@ -32,7 +34,7 @@ where
|
|||||||
Self {
|
Self {
|
||||||
content: Child::new(content),
|
content: Child::new(content),
|
||||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||||
pad: Pad::with_background(background),
|
pad: Pad::with_background(background).with_clear(),
|
||||||
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
||||||
confirm_btn_details: Some(ButtonDetails::text("CONFIRM")),
|
confirm_btn_details: Some(ButtonDetails::text("CONFIRM")),
|
||||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||||
@ -41,6 +43,7 @@ where
|
|||||||
// Initial button layout will be set in `place()` after we can call
|
// Initial button layout will be set in `place()` after we can call
|
||||||
// `content.page_count()`.
|
// `content.page_count()`.
|
||||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||||
|
show_scrollbar: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,7 +58,7 @@ where
|
|||||||
Self {
|
Self {
|
||||||
content: Child::new(content),
|
content: Child::new(content),
|
||||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||||
pad: Pad::with_background(background),
|
pad: Pad::with_background(background).with_clear(),
|
||||||
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
||||||
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
|
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
|
||||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||||
@ -64,6 +67,7 @@ where
|
|||||||
// Initial button layout will be set in `place()` after we can call
|
// Initial button layout will be set in `place()` after we can call
|
||||||
// `content.page_count()`.
|
// `content.page_count()`.
|
||||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||||
|
show_scrollbar: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,6 +99,11 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_scrollbar(mut self, show: bool) -> Self {
|
||||||
|
self.show_scrollbar = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Basically just determining whether the right button for
|
/// Basically just determining whether the right button for
|
||||||
/// initial page should be "NEXT" or "CONFIRM".
|
/// initial page should be "NEXT" or "CONFIRM".
|
||||||
/// Can only be called when we know the final page_count.
|
/// Can only be called when we know the final page_count.
|
||||||
@ -160,8 +169,13 @@ where
|
|||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
let (content_and_scrollbar_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
let (content_and_scrollbar_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||||
let (content_area, scrollbar_area) =
|
let (content_area, scrollbar_area) = {
|
||||||
content_and_scrollbar_area.split_right(ScrollBar::WIDTH);
|
if self.show_scrollbar {
|
||||||
|
content_and_scrollbar_area.split_right(ScrollBar::WIDTH)
|
||||||
|
} else {
|
||||||
|
(content_and_scrollbar_area, Rect::zero())
|
||||||
|
}
|
||||||
|
};
|
||||||
let content_area = content_area.inset(Insets::top(1));
|
let content_area = content_area.inset(Insets::top(1));
|
||||||
// Do not pad the button area nor the scrollbar, leave it to them
|
// Do not pad the button area nor the scrollbar, leave it to them
|
||||||
self.pad.place(content_area);
|
self.pad.place(content_area);
|
||||||
@ -170,7 +184,9 @@ where
|
|||||||
// and we can calculate the page count
|
// and we can calculate the page count
|
||||||
let page_count = self.content.inner_mut().page_count();
|
let page_count = self.content.inner_mut().page_count();
|
||||||
self.scrollbar.inner_mut().set_page_count(page_count);
|
self.scrollbar.inner_mut().set_page_count(page_count);
|
||||||
self.scrollbar.place(scrollbar_area);
|
if self.show_scrollbar {
|
||||||
|
self.scrollbar.place(scrollbar_area);
|
||||||
|
}
|
||||||
self.set_buttons_for_initial_page(page_count);
|
self.set_buttons_for_initial_page(page_count);
|
||||||
self.buttons.place(button_area);
|
self.buttons.place(button_area);
|
||||||
bounds
|
bounds
|
||||||
@ -215,7 +231,9 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.pad.paint();
|
self.pad.paint();
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
self.scrollbar.paint();
|
if self.show_scrollbar {
|
||||||
|
self.scrollbar.paint();
|
||||||
|
}
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
96
core/embed/rust/src/ui/model_tr/component/share_words.rs
Normal file
96
core/embed/rust/src/ui/model_tr/component/share_words.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::{
|
||||||
|
micropython::buffer::StrBuffer,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx, Never, Paginate},
|
||||||
|
display::Font,
|
||||||
|
geometry::{Offset, Rect},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use heapless::{String, Vec};
|
||||||
|
|
||||||
|
use super::common::display_inverse;
|
||||||
|
|
||||||
|
const WORDS_PER_PAGE: usize = 3;
|
||||||
|
const EXTRA_LINE_HEIGHT: i16 = 3;
|
||||||
|
const NUMBER_X_OFFSET: i16 = 5;
|
||||||
|
const NUMBER_WORD_OFFSET: i16 = 20;
|
||||||
|
const NUMBER_FONT: Font = Font::DEMIBOLD;
|
||||||
|
const WORD_FONT: Font = Font::NORMAL;
|
||||||
|
|
||||||
|
/// Showing the given share words.
|
||||||
|
///
|
||||||
|
/// Displays them in inverse colors - black text on white background.
|
||||||
|
/// It is that because of the OLED side attack - lot of white noise makes
|
||||||
|
/// the attack much harder.
|
||||||
|
pub struct ShareWords<const N: usize> {
|
||||||
|
area: Rect,
|
||||||
|
share_words: Vec<StrBuffer, N>,
|
||||||
|
word_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> ShareWords<N> {
|
||||||
|
pub fn new(share_words: Vec<StrBuffer, N>) -> Self {
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
share_words,
|
||||||
|
word_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Component for ShareWords<N> {
|
||||||
|
type Msg = Never;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
let mut y_offset = 0;
|
||||||
|
// Showing the word index and the words itself
|
||||||
|
for i in 0..WORDS_PER_PAGE {
|
||||||
|
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
|
||||||
|
let index = self.word_index + i;
|
||||||
|
let word = self.share_words[index].clone();
|
||||||
|
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
|
||||||
|
display_inverse(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
|
||||||
|
display_inverse(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Paginate for ShareWords<N> {
|
||||||
|
fn page_count(&mut self) -> usize {
|
||||||
|
if self.share_words.len() % WORDS_PER_PAGE == 0 {
|
||||||
|
self.share_words.len() / WORDS_PER_PAGE
|
||||||
|
} else {
|
||||||
|
self.share_words.len() / WORDS_PER_PAGE + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_page(&mut self, active_page: usize) {
|
||||||
|
self.word_index = active_page * WORDS_PER_PAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl<const N: usize> crate::trace::Trace for ShareWords<N> {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.open("ShareWords");
|
||||||
|
t.content_flag();
|
||||||
|
for i in 0..WORDS_PER_PAGE {
|
||||||
|
let index = self.word_index + i;
|
||||||
|
let word = self.share_words[index].clone();
|
||||||
|
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
|
||||||
|
t.string(&content);
|
||||||
|
}
|
||||||
|
t.content_flag();
|
||||||
|
t.close();
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ use super::{
|
|||||||
component::{
|
component::{
|
||||||
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
|
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
|
||||||
FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry,
|
FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry,
|
||||||
PinEntryMsg, SimpleChoice, SimpleChoiceMsg,
|
PinEntryMsg, ShareWords, SimpleChoice, SimpleChoiceMsg,
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
@ -465,37 +465,18 @@ extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) ->
|
|||||||
|
|
||||||
extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let share_words: StrBuffer = kwargs.get(Qstr::MP_QSTR_share_words)?.try_into()?;
|
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
|
||||||
|
let share_words: Vec<StrBuffer, 24> = iter_into_vec(share_words_obj)?;
|
||||||
|
|
||||||
let get_page = move |page_index| match page_index {
|
let confirm_btn =
|
||||||
0 => Page::<10>::new(
|
Some(ButtonDetails::text("CONFIRM").with_duration(Duration::from_secs(1)));
|
||||||
ButtonLayout::cancel_and_arrow_down(),
|
|
||||||
ButtonActions::cancel_next(),
|
|
||||||
Font::BOLD,
|
|
||||||
)
|
|
||||||
.text_bold("Write all words in order on recovery seed card.".into())
|
|
||||||
.newline()
|
|
||||||
.newline_half()
|
|
||||||
.text_mono("Do NOT make digital copies.".into()),
|
|
||||||
1 => Page::<10>::new(
|
|
||||||
ButtonLayout::only_arrow_down(),
|
|
||||||
ButtonActions::only_next(),
|
|
||||||
Font::NORMAL,
|
|
||||||
)
|
|
||||||
.text_normal(share_words.clone()),
|
|
||||||
2 => Page::<10>::new(
|
|
||||||
ButtonLayout::back_and_htc_text("HOLD TO CONFIRM", Duration::from_millis(1000)),
|
|
||||||
ButtonActions::prev_confirm(),
|
|
||||||
Font::MONO,
|
|
||||||
)
|
|
||||||
.newline()
|
|
||||||
.newline()
|
|
||||||
.text_mono("I wrote down all words in order.".into()),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let pages = FlowPages::new(get_page, 1);
|
|
||||||
|
|
||||||
let obj = LayoutObj::new(Flow::new(pages).into_child())?;
|
let obj = LayoutObj::new(
|
||||||
|
ButtonPage::new_str(ShareWords::new(share_words), theme::FG)
|
||||||
|
.with_cancel_btn(None)
|
||||||
|
.with_confirm_btn(confirm_btn)
|
||||||
|
.with_scrollbar(false),
|
||||||
|
)?;
|
||||||
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) }
|
||||||
@ -521,7 +502,7 @@ extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut M
|
|||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
|
|
||||||
let choices: Vec<&str, 5> = ["12", "18", "20", "24", "33"].into_iter().collect();
|
let choices: Vec<&str, 3> = ["12", "18", "24"].into_iter().collect();
|
||||||
|
|
||||||
let obj = LayoutObj::new(Frame::new(
|
let obj = LayoutObj::new(Frame::new(
|
||||||
title,
|
title,
|
||||||
|
@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType
|
|||||||
import trezorui2
|
import trezorui2
|
||||||
|
|
||||||
from ..common import interact
|
from ..common import interact
|
||||||
from . import RustLayout, confirm_action
|
from . import RustLayout, _placeholder_confirm, confirm_action, get_bool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trezor import wire
|
from trezor import wire
|
||||||
@ -19,20 +19,56 @@ async def show_share_words(
|
|||||||
share_index: int | None = None,
|
share_index: int | None = None,
|
||||||
group_index: int | None = None,
|
group_index: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
share_word_str = ""
|
await _placeholder_confirm(
|
||||||
for i, word in enumerate(share_words):
|
ctx=ctx,
|
||||||
share_word_str += f"{i + 1} {word}\n"
|
|
||||||
await interact(
|
|
||||||
ctx,
|
|
||||||
RustLayout(
|
|
||||||
trezorui2.show_share_words(
|
|
||||||
share_words=share_word_str.rstrip(),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
br_type="backup_words",
|
br_type="backup_words",
|
||||||
|
title="SHARE WORDS",
|
||||||
|
description=f"Write all {len(share_words)} words in order on recovery seed card.",
|
||||||
|
data="Do NOT make digital copies.",
|
||||||
br_code=ButtonRequestType.ResetDevice,
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Showing words, asking for write down confirmation and preparing for check
|
||||||
|
# until user accepts everything.
|
||||||
|
while True:
|
||||||
|
await interact(
|
||||||
|
ctx,
|
||||||
|
RustLayout(
|
||||||
|
trezorui2.show_share_words(
|
||||||
|
share_words=share_words,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
br_type="backup_words",
|
||||||
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrote_down = await get_bool(
|
||||||
|
ctx=ctx,
|
||||||
|
title="SHARE WORDS",
|
||||||
|
data=f"I wrote down all {len(share_words)} words in order.",
|
||||||
|
verb="I WROTE DOWN",
|
||||||
|
hold=True,
|
||||||
|
br_type="backup_words",
|
||||||
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
if not wrote_down:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ready_to_check = await get_bool(
|
||||||
|
ctx=ctx,
|
||||||
|
title="CHECK PHRASE",
|
||||||
|
data="Select correct words in correct positions.",
|
||||||
|
verb_cancel="SEE AGAIN",
|
||||||
|
verb="BEGIN",
|
||||||
|
br_type="backup_words",
|
||||||
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
if not ready_to_check:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# All went well, we can break the loop.
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
async def select_word(
|
async def select_word(
|
||||||
ctx: wire.GenericContext,
|
ctx: wire.GenericContext,
|
||||||
@ -49,7 +85,7 @@ async def select_word(
|
|||||||
result = await ctx.wait(
|
result = await ctx.wait(
|
||||||
RustLayout(
|
RustLayout(
|
||||||
trezorui2.select_word(
|
trezorui2.select_word(
|
||||||
title=f"SELECT WORD {checked_index + 1}",
|
title=f"SELECT WORD {checked_index + 1}/{count}",
|
||||||
words=(words[0].upper(), words[1].upper(), words[2].upper()),
|
words=(words[0].upper(), words[1].upper(), words[2].upper()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user