Merge remote-tracking branch 'origin/obrusvit/ui-t3t1-PIN-keyboard-and-flow' into mmilata/ui-t3t1-demo

mmilata/ui-t3t1-preview
Martin Milata 2 weeks ago
commit 996e4a5f36

@ -217,6 +217,7 @@ static void _librust_qstrs(void) {
MP_QSTR_flow_confirm_output;
MP_QSTR_flow_confirm_reset_create;
MP_QSTR_flow_confirm_reset_recover;
MP_QSTR_flow_confirm_set_new_pin;
MP_QSTR_flow_confirm_summary;
MP_QSTR_flow_get_address;
MP_QSTR_flow_prompt_backup;
@ -312,6 +313,9 @@ static void _librust_qstrs(void) {
MP_QSTR_passphrase__turn_off;
MP_QSTR_passphrase__turn_on;
MP_QSTR_path;
MP_QSTR_pin__cancel_description;
MP_QSTR_pin__cancel_info;
MP_QSTR_pin__cancel_setup;
MP_QSTR_pin__change;
MP_QSTR_pin__changed;
MP_QSTR_pin__cursor_will_change;
@ -359,6 +363,7 @@ static void _librust_qstrs(void) {
MP_QSTR_progress__x_seconds_left_template;
MP_QSTR_progress_event;
MP_QSTR_prompt;
MP_QSTR_prompt_screen;
MP_QSTR_qr_title;
MP_QSTR_reboot_to_bootloader__just_a_moment;
MP_QSTR_reboot_to_bootloader__restart;

@ -1254,9 +1254,12 @@ pub enum TranslatedString {
instructions__continue_in_app = 859, // "Continue in the app"
words__cancel_and_exit = 860, // "Cancel and exit"
address__confirmed = 861, // "Receive address confirmed"
send__cancel_sign = 862, // "Cancel sign"
send__send_from = 863, // "Send from"
instructions__hold_to_sign = 864, // "Hold to sign"
pin__cancel_description = 862, // "Continue without PIN"
pin__cancel_info = 863, // "Without a PIN, anyone can access this device."
pin__cancel_setup = 864, // "Cancel PIN setup"
send__cancel_sign = 865, // "Cancel sign"
send__send_from = 866, // "Send from"
instructions__hold_to_sign = 867, // "Hold to sign"
}
impl TranslatedString {
@ -2506,6 +2509,9 @@ impl TranslatedString {
Self::instructions__continue_in_app => "Continue in the app",
Self::words__cancel_and_exit => "Cancel and exit",
Self::address__confirmed => "Receive address confirmed",
Self::pin__cancel_description => "Continue without PIN",
Self::pin__cancel_info => "Without a PIN, anyone can access this device.",
Self::pin__cancel_setup => "Cancel PIN setup",
Self::send__cancel_sign => "Cancel sign",
Self::send__send_from => "Send from",
Self::instructions__hold_to_sign => "Hold to sign",
@ -3759,6 +3765,9 @@ impl TranslatedString {
Qstr::MP_QSTR_instructions__continue_in_app => Some(Self::instructions__continue_in_app),
Qstr::MP_QSTR_words__cancel_and_exit => Some(Self::words__cancel_and_exit),
Qstr::MP_QSTR_address__confirmed => Some(Self::address__confirmed),
Qstr::MP_QSTR_pin__cancel_description => Some(Self::pin__cancel_description),
Qstr::MP_QSTR_pin__cancel_info => Some(Self::pin__cancel_info),
Qstr::MP_QSTR_pin__cancel_setup => Some(Self::pin__cancel_setup),
Qstr::MP_QSTR_send__cancel_sign => Some(Self::send__cancel_sign),
Qstr::MP_QSTR_send__send_from => Some(Self::send__send_from),
Qstr::MP_QSTR_instructions__hold_to_sign => Some(Self::instructions__hold_to_sign),

@ -301,7 +301,7 @@ impl Bip39Input {
// Auto-complete button.
self.button.enable(ctx);
self.button
.set_stylesheet(ctx, theme::button_pin_autocomplete());
.set_stylesheet(ctx, theme::button_bip39_autocomplete());
self.button
.set_content(ctx, ButtonContent::Icon(theme::ICON_AUTOFILL));
self.button_suggestion
@ -310,7 +310,7 @@ impl Bip39Input {
} else {
// Disabled button.
self.button.disable(ctx);
self.button.set_stylesheet(ctx, theme::button_pin());
self.button.set_stylesheet(ctx, theme::button_keyboard());
self.button.set_content(ctx, ButtonContent::Text("".into()));
}
}

@ -58,7 +58,7 @@ where
keys: T::keys()
.map(|t| {
Button::with_text(t.into())
.styled(theme::button_pin())
.styled(theme::button_keyboard())
.with_text_align(Alignment::Center)
})
.map(Child::new),

@ -67,7 +67,7 @@ impl PassphraseKeyboard {
.with_long_press(theme::ERASE_HOLD_DURATION)
.into_child(),
keys: KEYBOARD[STARTING_PAGE].map(|text| {
Child::new(Button::new(Self::key_content(text)).styled(theme::button_pin()))
Child::new(Button::new(Self::key_content(text)).styled(theme::button_keyboard()))
}),
scrollbar: ScrollBar::horizontal(),
fade: Cell::new(false),

@ -10,7 +10,7 @@ use crate::{
base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe,
Never, Pad, TimerToken,
},
display::{self, Font},
display::Font,
event::TouchEvent,
geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect},
model_mercury::component::{
@ -28,15 +28,15 @@ pub enum PinKeyboardMsg {
}
const MAX_LENGTH: usize = 50;
const MAX_VISIBLE_DOTS: usize = 14;
const MAX_VISIBLE_DIGITS: usize = 16;
const MAX_VISIBLE_DOTS: usize = 18;
const MAX_VISIBLE_DIGITS: usize = 18;
const DIGIT_COUNT: usize = 10; // 0..10
const HEADER_PADDING_SIDE: i16 = 5;
const HEADER_PADDING_BOTTOM: i16 = 12;
const HEADER_PADDING_SIDE: i16 = 2;
const HEADER_PADDING_BOTTOM: i16 = 2;
const HEADER_PADDING: Insets = Insets::new(
theme::borders().top,
0,
HEADER_PADDING_SIDE,
HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE,
@ -57,10 +57,6 @@ pub struct PinKeyboard<'a> {
}
impl<'a> PinKeyboard<'a> {
// Label position fine-tuning.
const MAJOR_OFF: Offset = Offset::y(11);
const MINOR_OFF: Offset = Offset::y(11);
pub fn new(
major_prompt: TString<'a>,
minor_prompt: TString<'a>,
@ -68,17 +64,13 @@ impl<'a> PinKeyboard<'a> {
allow_cancel: bool,
) -> Self {
// Control buttons.
let erase_btn = Button::with_icon_blend(
theme::IMAGE_BG_BACK_BTN,
theme::ICON_BACK,
Offset::new(30, 12),
)
.styled(theme::button_reset())
.with_long_press(theme::ERASE_HOLD_DURATION)
.initially_enabled(false);
let erase_btn = Button::with_icon(theme::ICON_DELETE)
.styled(theme::button_pin_erase())
.with_long_press(theme::ERASE_HOLD_DURATION)
.initially_enabled(false);
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();
let cancel_btn = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel());
let cancel_btn = Button::with_icon(theme::ICON_CLOSE).styled(theme::button_pin_cancel());
let cancel_btn = Maybe::new(theme::BG, cancel_btn, allow_cancel).into_child();
Self {
@ -94,7 +86,7 @@ impl<'a> PinKeyboard<'a> {
erase_btn,
cancel_btn,
confirm_btn: Button::with_icon(theme::ICON_CONFIRM)
.styled(theme::button_confirm())
.styled(theme::button_pin_confirm())
.initially_enabled(false)
.into_child(),
digit_btns: Self::generate_digit_buttons(),
@ -108,7 +100,10 @@ impl<'a> PinKeyboard<'a> {
random::shuffle(&mut digits);
digits
.map(|c| Button::with_text(c.into()))
.map(|b| b.styled(theme::button_pin()))
.map(|b| {
b.styled(theme::button_keyboard())
.with_text_align(Alignment::Center)
})
.map(Child::new)
}
@ -150,20 +145,10 @@ impl Component for PinKeyboard<'_> {
type Msg = PinKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// Ignore the top padding for now, we need it to reliably register textbox touch
// events.
let borders_no_top = Insets {
top: 0,
..theme::borders()
};
// Prompts and PIN dots display.
let (header, keypad) = bounds
.inset(borders_no_top)
.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
let (header, keypad) =
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
let prompt = header.inset(HEADER_PADDING);
// the inset -3 is a workaround for long text in "re-enter wipe code"
let major_area = prompt.translate(Self::MAJOR_OFF).inset(Insets::right(-3));
let minor_area = prompt.translate(Self::MINOR_OFF);
// Control buttons.
let grid = Grid::new(keypad, 4, 3).with_spacing(theme::BUTTON_SPACING);
@ -171,9 +156,9 @@ impl Component for PinKeyboard<'_> {
// Prompts and PIN dots display.
self.textbox_pad.place(header);
self.textbox.place(header);
self.major_prompt.place(major_area);
self.minor_prompt.place(minor_area);
self.major_warning.as_mut().map(|c| c.place(major_area));
self.major_prompt.place(prompt);
self.minor_prompt.place(prompt);
self.major_warning.as_mut().map(|c| c.place(prompt));
// Control buttons.
let erase_cancel_area = grid.row_col(3, 0);
@ -269,6 +254,7 @@ impl Component for PinKeyboard<'_> {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.erase_btn.render(target);
self.textbox_pad.render(target);
if self.textbox.inner().is_empty() {
if let Some(ref w) = self.major_warning {
w.render(target);
@ -280,7 +266,12 @@ impl Component for PinKeyboard<'_> {
} else {
self.textbox.render(target);
}
self.confirm_btn.render(target);
// Painting the confirm only if there is already a pin
if !self.textbox.inner().is_empty() {
self.confirm_btn.render(target);
}
for btn in &self.digit_btns {
btn.render(target);
}
@ -310,7 +301,7 @@ struct PinDots {
impl PinDots {
const DOT: i16 = 6;
const PADDING: i16 = 6;
const PADDING: i16 = 7;
const TWITCH: i16 = 4;
fn new(style: TextStyle) -> Self {
@ -361,128 +352,63 @@ impl PinDots {
&self.digits
}
fn paint_digits(&self, area: Rect) {
let center = area.center() + Offset::y(Font::MONO.text_height() / 2);
let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2);
let digits = self.digits.len();
if digits <= MAX_VISIBLE_DOTS {
display::text_center(
center,
&self.digits,
Font::MONO,
self.style.text_color,
self.style.background_color,
);
} else {
let offset: usize = digits.saturating_sub(MAX_VISIBLE_DIGITS);
display::text_right(
right,
&self.digits[offset..],
Font::MONO,
self.style.text_color,
self.style.background_color,
);
}
}
fn render_digits<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let center = area.center() + Offset::y(Font::MONO.text_height() / 2);
let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2);
let left = area.left_center() + Offset::y(Font::MONO.visible_text_height("1") / 2);
let digits = self.digits.len();
if digits <= MAX_VISIBLE_DOTS {
shape::Text::new(center, &self.digits)
.with_align(Alignment::Center)
if digits <= MAX_VISIBLE_DIGITS {
shape::Text::new(left, &self.digits)
.with_align(Alignment::Start)
.with_font(Font::MONO)
.with_fg(self.style.text_color)
.render(target);
} else {
let offset: usize = digits.saturating_sub(MAX_VISIBLE_DIGITS);
shape::Text::new(right, &self.digits[offset..])
.with_align(Alignment::End)
shape::Text::new(left, &self.digits[offset..])
.with_align(Alignment::Start)
.with_font(Font::MONO)
.with_fg(self.style.text_color)
.render(target);
}
}
fn paint_dots(&self, area: Rect) {
let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER);
let digits = self.digits.len();
let dots_visible = digits.min(MAX_VISIBLE_DOTS);
let step = Self::DOT + Self::PADDING;
// Jiggle when overflowed.
if digits > dots_visible && digits % 2 == 0 {
cursor.x += Self::TWITCH
}
// Small leftmost dot.
if digits > dots_visible + 1 {
theme::DOT_SMALL.draw(
cursor - Offset::x(2 * step),
Alignment2D::TOP_LEFT,
self.style.text_color,
self.style.background_color,
);
}
// Greyed out dot.
if digits > dots_visible {
theme::DOT_ACTIVE.draw(
cursor - Offset::x(step),
Alignment2D::TOP_LEFT,
theme::GREY_LIGHT,
self.style.background_color,
);
}
// Draw a dot for each PIN digit.
for _ in 0..dots_visible {
theme::DOT_ACTIVE.draw(
cursor,
Alignment2D::TOP_LEFT,
self.style.text_color,
self.style.background_color,
);
cursor.x += step;
}
}
fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER);
let mut cursor = area.left_center();
let digits = self.digits.len();
let dots_visible = digits.min(MAX_VISIBLE_DOTS);
let step = Self::DOT + Self::PADDING;
// Jiggle when overflowed.
if digits > dots_visible && digits % 2 == 0 {
if digits >= MAX_VISIBLE_DOTS + 2 && (digits + 1) % 2 == 0 {
cursor.x += Self::TWITCH
}
let mut digit_idx = 0;
// Small leftmost dot.
if digits > dots_visible + 1 {
shape::ToifImage::new(cursor - Offset::x(2 * step), theme::DOT_SMALL.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(self.style.text_color)
if digits >= MAX_VISIBLE_DOTS + 2 {
shape::ToifImage::new(cursor, theme::DOT_SMALL.toif)
.with_align(Alignment2D::CENTER_LEFT)
.with_fg(theme::GREY_DARK)
.render(target);
cursor.x += step;
digit_idx += 1;
}
// Greyed out dot.
if digits > dots_visible {
shape::ToifImage::new(cursor - Offset::x(step), theme::DOT_ACTIVE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_fg(theme::GREY_LIGHT)
if digits >= MAX_VISIBLE_DOTS + 1 {
shape::ToifImage::new(cursor, theme::DOT_ACTIVE.toif)
.with_align(Alignment2D::CENTER_LEFT)
.with_fg(theme::GREY)
.render(target);
cursor.x += step;
digit_idx += 1;
}
// Draw a dot for each PIN digit.
for _ in 0..dots_visible {
for _ in digit_idx..dots_visible {
shape::ToifImage::new(cursor, theme::DOT_ACTIVE.toif)
.with_align(Alignment2D::TOP_LEFT)
.with_align(Alignment2D::CENTER_LEFT)
.with_fg(self.style.text_color)
.render(target);
cursor.x += step;
@ -521,13 +447,7 @@ impl Component for PinDots {
}
fn paint(&mut self) {
let dot_area = self.area.inset(HEADER_PADDING);
self.pad.paint();
if self.display_digits {
self.paint_digits(dot_area)
} else {
self.paint_dots(dot_area)
}
// TODO: remove when ui-t3t1 done
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {

@ -25,7 +25,7 @@ impl SelectWordCount {
SelectWordCount {
button: LABELS.map(|t| {
Button::with_text(t.into())
.styled(theme::button_pin())
.styled(theme::button_keyboard())
.with_text_align(Alignment::Center)
}),
}

@ -0,0 +1,210 @@
use crate::{
error,
strutil::TString,
translations::TR,
ui::{
component::{text::paragraphs::Paragraph, ComponentExt, SwipeDirection},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
};
use super::super::{
component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg},
theme,
};
// TODO: merge with code from https://github.com/trezor/trezor-firmware/pull/3805
// when ready
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum ConfirmAction {
Intro,
Menu,
Confirm,
}
/// ConfirmAction flow without a separate "Tap to confirm" or "Hold to confirm"
/// screen. Swiping up directly from the intro screen confirms action.
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum ConfirmActionSimple {
Intro,
Menu,
}
impl FlowState for ConfirmAction {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(ConfirmAction::Intro, SwipeDirection::Left) => {
Decision::Goto(ConfirmAction::Menu, direction)
}
(ConfirmAction::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmAction::Intro, direction)
}
(ConfirmAction::Intro, SwipeDirection::Up) => {
Decision::Goto(ConfirmAction::Confirm, direction)
}
(ConfirmAction::Confirm, SwipeDirection::Down) => {
Decision::Goto(ConfirmAction::Intro, direction)
}
(ConfirmAction::Confirm, SwipeDirection::Left) => {
Decision::Goto(ConfirmAction::Menu, direction)
}
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(ConfirmAction::Intro, FlowMsg::Info) => {
Decision::Goto(ConfirmAction::Menu, SwipeDirection::Left)
}
(ConfirmAction::Menu, FlowMsg::Cancelled) => {
Decision::Goto(ConfirmAction::Intro, SwipeDirection::Right)
}
(ConfirmAction::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled),
(ConfirmAction::Confirm, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Confirmed),
(ConfirmAction::Confirm, FlowMsg::Info) => {
Decision::Goto(ConfirmAction::Menu, SwipeDirection::Left)
}
_ => Decision::Nothing,
}
}
}
impl FlowState for ConfirmActionSimple {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(ConfirmActionSimple::Intro, SwipeDirection::Left) => {
Decision::Goto(ConfirmActionSimple::Menu, direction)
}
(ConfirmActionSimple::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmActionSimple::Intro, direction)
}
(ConfirmActionSimple::Intro, SwipeDirection::Up) => {
Decision::Return(FlowMsg::Confirmed)
}
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(ConfirmActionSimple::Intro, FlowMsg::Info) => {
Decision::Goto(ConfirmActionSimple::Menu, SwipeDirection::Left)
}
(ConfirmActionSimple::Menu, FlowMsg::Cancelled) => {
Decision::Goto(ConfirmActionSimple::Intro, SwipeDirection::Right)
}
(ConfirmActionSimple::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled),
_ => Decision::Nothing,
}
}
}
use crate::{
micropython::{map::Map, obj::Obj, qstr::Qstr, util},
ui::{
component::text::paragraphs::{ParagraphSource, ParagraphVecShort, VecExt},
layout::obj::LayoutObj,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, new_confirm_action_obj) }
}
fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let action: Option<TString> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
let description: Option<TString> = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
// let verb: Option<TString> = kwargs
// .get(Qstr::MP_QSTR_verb)
// .unwrap_or_else(|_| Obj::const_none())
// .try_into_option()?;
let verb_cancel: Option<TString> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
// let hold_danger: bool = kwargs.get_or(Qstr::MP_QSTR_hold_danger, false)?;
let prompt_screen: bool = kwargs.get_or(Qstr::MP_QSTR_prompt_screen, false)?;
let paragraphs = {
let action = action.unwrap_or("".into());
let description = description.unwrap_or("".into());
let mut paragraphs = ParagraphVecShort::new();
if !reverse {
paragraphs
.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, action))
.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description));
} else {
paragraphs
.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description))
.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, action));
}
paragraphs.into_paragraphs()
};
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
let content_menu = if let Some(verb_cancel) = verb_cancel {
Frame::left_aligned(
"".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, verb_cancel.into()),
)
} else {
Frame::left_aligned(
"".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()),
)
}
.with_cancel_button()
.map(move |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(_)) => Some(FlowMsg::Choice(0)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
if !prompt_screen {
let store = flow_store().add(content_intro)?.add(content_menu)?;
let res = SwipeFlow::new(ConfirmActionSimple::Intro, store)?;
return Ok(LayoutObj::new(res)?.into());
} else {
let (prompt, prompt_action) = if hold {
(
PromptScreen::new_hold_to_confirm(),
TR::instructions__hold_to_confirm.into(),
)
} else {
(
PromptScreen::new_tap_to_confirm(),
TR::instructions__tap_to_confirm.into(),
)
};
let content_confirm = Frame::left_aligned(title, prompt)
.with_footer(prompt_action, None)
.with_menu_button();
// .with_overlapping_content();
// if let Some(subtitle) = subtitle {
// content_confirm = content_confirm.with_subtitle(subtitle);
// }
let content_confirm = content_confirm.map(move |msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
});
let store = flow_store()
.add(content_intro)?
.add(content_menu)?
.add(content_confirm)?;
let res = SwipeFlow::new(ConfirmAction::Intro, store)?;
return Ok(LayoutObj::new(res)?.into());
};
}

@ -0,0 +1,148 @@
use crate::{
error,
strutil::TString,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, Paragraphs},
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
};
use super::super::{
component::{
CancelInfoConfirmMsg, Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
},
theme,
};
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum SetNewPin {
Intro,
Menu,
CancelPinIntro,
CancelPinConfirm,
}
impl FlowState for SetNewPin {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(SetNewPin::Intro, SwipeDirection::Left) => {
Decision::Goto(SetNewPin::Menu, direction)
}
(SetNewPin::CancelPinIntro, SwipeDirection::Up) => {
Decision::Goto(SetNewPin::CancelPinConfirm, direction)
}
(SetNewPin::CancelPinConfirm, SwipeDirection::Down) => {
Decision::Goto(SetNewPin::CancelPinIntro, direction)
}
(SetNewPin::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(SetNewPin::Intro, FlowMsg::Info) => {
Decision::Goto(SetNewPin::Menu, SwipeDirection::Left)
}
(SetNewPin::Menu, FlowMsg::Choice(0)) => {
Decision::Goto(SetNewPin::CancelPinIntro, SwipeDirection::Left)
}
(SetNewPin::Menu, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::Intro, SwipeDirection::Right)
}
(SetNewPin::CancelPinIntro, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::Menu, SwipeDirection::Right)
}
(SetNewPin::CancelPinConfirm, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::CancelPinIntro, SwipeDirection::Right)
}
(SetNewPin::CancelPinConfirm, FlowMsg::Confirmed) => {
Decision::Return(FlowMsg::Cancelled)
}
_ => Decision::Nothing,
}
}
}
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_set_new_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, SetNewPin::new_obj) }
}
impl SetNewPin {
fn new_obj(_args: &[Obj], _kwargs: &Map) -> Result<Obj, error::Error> {
let title: TString = TR::pin__title_settings.into();
let par_array: [Paragraph<'static>; 1] = [Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::pin__info,
)];
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| {
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
});
let content_menu = Frame::left_aligned(
"".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::pin__cancel_setup.into()),
)
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
FrameMsg::Button(_) => None,
});
let par_array_cancel_intro: [Paragraph<'static>; 2] = [
Paragraph::new(&theme::TEXT_WARNING, TR::words__not_recommended),
Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::pin__cancel_info,
),
];
let paragraphs_cancel_intro = Paragraphs::new(par_array_cancel_intro);
let content_cancel_intro = Frame::left_aligned(
TR::pin__cancel_setup.into(),
SwipePage::vertical(paragraphs_cancel_intro),
)
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::pin__cancel_description.into()),
)
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let content_cancel_confirm = Frame::left_aligned(
TR::pin__cancel_setup.into(),
PromptScreen::new_tap_to_cancel(),
)
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let store = flow_store()
.add(content_intro)?
.add(content_menu)?
.add(content_cancel_intro)?
.add(content_cancel_confirm)?;
let res = SwipeFlow::new(SetNewPin::Intro, store)?;
Ok(LayoutObj::new(res)?.into())
}
}

@ -1,6 +1,8 @@
pub mod confirm_action;
pub mod confirm_output;
pub mod confirm_reset_create;
pub mod confirm_reset_recover;
pub mod confirm_set_new_pin;
pub mod confirm_summary;
pub mod get_address;
pub mod prompt_backup;
@ -9,9 +11,11 @@ pub mod warning_hi_prio;
mod util;
pub use confirm_action::new_confirm_action;
pub use confirm_output::new_confirm_output;
pub use confirm_reset_create::ConfirmResetCreate;
pub use confirm_reset_recover::ConfirmResetRecover;
pub use confirm_set_new_pin::SetNewPin;
pub use confirm_summary::new_confirm_summary;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;

@ -331,54 +331,6 @@ impl ComponentMsgObj for super::component::bl_confirm::Confirm<'_> {
}
}
extern "C" fn new_confirm_action(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 action: Option<TString> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
let description: Option<TString> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let verb: Option<TString> = kwargs
.get(Qstr::MP_QSTR_verb)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let verb_cancel: Option<TString> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let hold_danger: bool = kwargs.get_or(Qstr::MP_QSTR_hold_danger, false)?;
let paragraphs = {
let action = action.unwrap_or("".into());
let description = description.unwrap_or("".into());
let mut paragraphs = ParagraphVecShort::new();
if !reverse {
paragraphs
.add(Paragraph::new(&theme::TEXT_DEMIBOLD, action))
.add(Paragraph::new(&theme::TEXT_NORMAL, description));
} else {
paragraphs
.add(Paragraph::new(&theme::TEXT_NORMAL, description))
.add(Paragraph::new(&theme::TEXT_DEMIBOLD, action));
}
paragraphs.into_paragraphs()
};
let mut page = if hold {
ButtonPage::new(paragraphs, theme::BG).with_hold()?
} else {
ButtonPage::new(paragraphs, theme::BG).with_cancel_confirm(verb_cancel, verb)
};
if hold && hold_danger {
page = page.with_confirm_style(theme::button_danger())
}
let obj = LayoutObj::new(Frame::left_aligned(title, page))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_emphasized(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()?;
@ -882,14 +834,26 @@ fn new_show_modal(
extern "C" fn new_show_error(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_ERROR,
theme::ERROR_COLOR,
theme::FG,
theme::BG,
);
new_show_modal(kwargs, icon, theme::button_default())
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let allow_cancel: bool = kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into()?;
let content = SwipeUpScreen::new(Paragraphs::new([Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
description,
)]));
let frame = if allow_cancel {
Frame::left_aligned(title, content)
.with_cancel_button()
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
} else {
Frame::left_aligned(title, content)
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
};
let obj = LayoutObj::new(frame)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -1598,14 +1562,16 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str,
/// action: str | None,
/// description: str | None,
/// subtitle: str | None = None,
/// verb: str | None = None,
/// verb_cancel: str | None = None,
/// hold: bool = False,
/// hold_danger: bool = False,
/// reverse: bool = False,
/// prompt_screen: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, flow::confirm_action::new_confirm_action).as_obj(),
/// def confirm_emphasized(
/// *,
@ -1670,6 +1636,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm TOS before creating a wallet and have a user hold to confirm creation."""
Qstr::MP_QSTR_flow_confirm_reset_create => obj_fn_kw!(0, flow::confirm_reset_create::new_confirm_reset_create).as_obj(),
/// def flow_confirm_set_new_pin() -> LayoutObj[UiResult]:
/// """Confirm new PIN setup with an option to cancel action."""
Qstr::MP_QSTR_flow_confirm_set_new_pin => obj_fn_kw!(0, flow::confirm_set_new_pin::new_set_new_pin).as_obj(),
/// def show_info_with_cancel(
/// *,
/// title: str,

@ -35,9 +35,11 @@ pub const GREY_DARK: Color = Color::rgb(0x46, 0x48, 0x4A);
pub const GREY: Color = Color::rgb(0x8B, 0x8F, 0x93); // secondary text, subtitle, instructions
pub const GREY_LIGHT: Color = Color::rgb(0xC7, 0xCD, 0xD3); // content
pub const GREY_EXTRA_LIGHT: Color = Color::rgb(0xF0, 0xF0, 0xF0); // primary text, header
pub const GREEN_DARK: Color = Color::rgb(0x06, 0x1E, 0x19);
pub const GREEN: Color = Color::rgb(0x08, 0x74, 0x48);
pub const GREEN_LIGHT: Color = Color::rgb(0x0B, 0xA5, 0x67);
pub const GREEN_LIME: Color = Color::rgb(0x9B, 0xE8, 0x87);
pub const ORANGE_DARK: Color = Color::rgb(0x18, 0x0C, 0x0A);
pub const ORANGE_DIMMED: Color = Color::rgb(0x9E, 0x57, 0x42);
pub const ORANGE_LIGHT: Color = Color::rgb(0xFF, 0x8D, 0x6A); // cancel button
@ -46,7 +48,6 @@ pub const RED: Color = Color::rgb(0xE7, 0x0E, 0x0E); // button
pub const RED_DARK: Color = Color::rgb(0xAE, 0x09, 0x09); // button pressed
pub const YELLOW: Color = Color::rgb(0xD9, 0x9E, 0x00); // button
pub const YELLOW_DARK: Color = Color::rgb(0x7A, 0x58, 0x00); // button pressed
pub const GREEN_DARK: Color = Color::rgb(0x00, 0x55, 0x1D); // button pressed
pub const BLUE: Color = Color::rgb(0x06, 0x1E, 0xAD); // button
pub const BLUE_DARK: Color = Color::rgb(0x04, 0x10, 0x58); // button pressed
pub const OFF_WHITE: Color = Color::rgb(0xDE, 0xDE, 0xDE); // very light grey
@ -109,6 +110,16 @@ include_icon!(
);
include_icon!(ICON_CENTRAL_CIRCLE, "model_mercury/res/central_circle.toif");
// Scrollbar/PIN dots - taken from model T
include_icon!(DOT_ACTIVE, "model_mercury/res/scroll-active.toif");
include_icon!(DOT_INACTIVE, "model_mercury/res/scroll-inactive.toif");
include_icon!(DOT_INACTIVE_HALF, "model_mercury/res/scroll-inactive-half.toif");
include_icon!(
DOT_INACTIVE_QUARTER,
"model_mercury/res/scroll-inactive-quarter.toif"
);
include_icon!(DOT_SMALL, "model_mercury/res/scroll-small.toif");
// TODO remove TT icons:
// Button icons.
@ -163,16 +174,6 @@ include_icon!(ICON_LOGO_EMPTY, "model_tt/res/lock_empty.toif");
// Default homescreen
pub const IMAGE_HOMESCREEN: &[u8] = include_res!("model_tt/res/bg.jpg");
// Scrollbar/PIN dots.
include_icon!(DOT_ACTIVE, "model_tt/res/scroll-active.toif");
include_icon!(DOT_INACTIVE, "model_tt/res/scroll-inactive.toif");
include_icon!(DOT_INACTIVE_HALF, "model_tt/res/scroll-inactive-half.toif");
include_icon!(
DOT_INACTIVE_QUARTER,
"model_tt/res/scroll-inactive-quarter.toif"
);
include_icon!(DOT_SMALL, "model_tt/res/scroll-small.toif");
pub const fn label_default() -> TextStyle {
TEXT_NORMAL
}
@ -399,6 +400,7 @@ pub const fn button_danger() -> ButtonStyleSheet {
}
}
// TODO: delete
pub const fn button_reset() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
@ -425,7 +427,8 @@ pub const fn button_reset() -> ButtonStyleSheet {
}
}
pub const fn button_pin() -> ButtonStyleSheet {
// used for PIN digit keys and passphrase/recovery letter keys
pub const fn button_keyboard() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::NORMAL,
@ -451,24 +454,77 @@ pub const fn button_pin() -> ButtonStyleSheet {
}
}
// TODO: will button_pin_xyz styles be the same for PIN and Mnemonic keyboard?
// Wait for Figma
pub const fn button_pin_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_DARK,
icon_color: GREEN_LIME,
background_color: BG,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREEN_LIGHT,
icon_color: GREEN_DARK,
background_color: BG,
},
// not used
disabled: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: BG,
icon_color: GREEN_LIGHT,
icon_color: FG,
background_color: BG,
},
}
}
pub const fn button_pin_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: BG, // TODO: gradient
icon_color: ORANGE_LIGHT,
background_color: BG,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_DARK,
button_color: ORANGE_LIGHT,
icon_color: BG,
background_color: BG,
},
// not used
disabled: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: BG,
icon_color: GREEN_LIGHT,
background_color: BG,
},
}
}
pub const fn button_pin_erase() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: BG, // TODO: gradient
icon_color: GREY,
background_color: BG,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_LIGHT,
icon_color: BG,
background_color: BG,
},
// not used
disabled: &ButtonStyle {
font: Font::MONO,
text_color: FG,
@ -479,7 +535,7 @@ pub const fn button_pin_confirm() -> ButtonStyleSheet {
}
}
pub const fn button_pin_autocomplete() -> ButtonStyleSheet {
pub const fn button_bip39_autocomplete() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
@ -691,7 +747,7 @@ pub const RECOVERY_SPACING: i16 = 18;
pub const CORNER_BUTTON_SIDE: i16 = 44;
pub const CORNER_BUTTON_SPACING: i16 = BUTTON_SPACING;
pub const INFO_BUTTON_HEIGHT: i16 = 44;
pub const PIN_BUTTON_HEIGHT: i16 = 40;
pub const PIN_BUTTON_HEIGHT: i16 = 52;
pub const MNEMONIC_BUTTON_HEIGHT: i16 = 52;
pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171;

@ -155,6 +155,11 @@ def flow_confirm_reset_create() -> LayoutObj[UiResult]:
"""Confirm TOS before creating a wallet and have a user hold to confirm creation."""
# rust/src/ui/model_mercury/layout.rs
def flow_confirm_set_new_pin() -> LayoutObj[UiResult]:
"""Confirm new PIN setup with an option to cancel action."""
# rust/src/ui/model_mercury/layout.rs
def show_info_with_cancel(
*,

@ -470,6 +470,9 @@ class TR:
passphrase__title_source: str = "PASSPHRASE SOURCE"
passphrase__turn_off: str = "Turn off passphrase protection?"
passphrase__turn_on: str = "Turn on passphrase protection?"
pin__cancel_description: str = "Continue without PIN"
pin__cancel_info: str = "Without a PIN, anyone can access this device."
pin__cancel_setup: str = "Cancel PIN setup"
pin__change: str = "Change PIN?"
pin__changed: str = "PIN changed."
pin__cursor_will_change: str = "Position of the cursor will change between entries for enhanced security."

@ -1401,8 +1401,9 @@ async def pin_mismatch_popup(
is_wipe_code: bool = False,
) -> None:
await button_request("pin_mismatch", code=BR_TYPE_OTHER)
title = TR.wipe_code__wipe_code_mismatch if is_wipe_code else TR.pin__pin_mismatch
description = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch
title = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch
description = TR.wipe_code__enter_new if is_wipe_code else TR.pin__reenter_new
return await show_error_popup(
title,
description,
@ -1428,16 +1429,7 @@ async def confirm_set_new_pin(
) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.confirm_emphasized(
title=title.upper(),
items=(
(True, description + "\n\n"),
information,
),
verb=TR.buttons__turn_on,
)
),
RustLayout(trezorui2.flow_confirm_set_new_pin()),
br_type,
br_code,
)

@ -472,8 +472,11 @@
"passphrase__title_source": "PASSPHRASE SOURCE",
"passphrase__turn_off": "Turn off passphrase protection?",
"passphrase__turn_on": "Turn on passphrase protection?",
"pin__cancel_info": "Without a PIN, anyone can access this device.",
"pin__cancel_setup": "Cancel PIN setup",
"pin__change": "Change PIN?",
"pin__changed": "PIN changed.",
"pin__cancel_description": "Continue without PIN",
"pin__cursor_will_change": "Position of the cursor will change between entries for enhanced security.",
"pin__diff_from_wipe_code": "The new PIN must be different from your wipe code.",
"pin__disabled": "PIN protection\nturned off.",

@ -861,7 +861,11 @@
"859": "instructions__continue_in_app",
"860": "words__cancel_and_exit",
"861": "address__confirmed",
"862": "send__cancel_sign",
"863": "send__send_from",
"864": "instructions__hold_to_sign"
"862": "pin__cancel_description",
"863": "pin__cancel_info",
"864": "pin__cancel_setup",
"865": "address__confirmed",
"866": "send__cancel_sign",
"867": "send__send_from",
"868": "instructions__hold_to_sign"
}

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "522ec2af48343bcf4528469e821dfc3852d14dae290239556222cbcc4356dc98",
"datetime": "2024-05-11T20:19:06.318093",
"commit": "fa8d6a4a609ae6acd972c596678cad07b99b07d8"
"datetime": "2024-05-09T13:12:46.955119",
"commit": "e3e8db19dbff80bc0281e1304f41fa8ae74d9650"
},
"history": [
{

Loading…
Cancel
Save