From 36e2c98013a2bec2ea11a5fcab575ef1db7d9f5f Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sun, 21 Apr 2024 01:49:45 +0200 Subject: [PATCH] feat(core/ui): add T3T1 Status/PromptScreen StatusScreen serves to show a result of operation, typically a checkmark with a circle around dismissed by swipe up gesture. PromptScreen serves to confirm action, typically by holding a button. Designs based on Figma. So far without animation. --- core/embed/rust/librust_qstr.h | 2 + .../generated/translated_string.rs | 6 + .../src/ui/model_mercury/component/mod.rs | 4 + .../model_mercury/component/prompt_screen.rs | 141 ++++++++++++++++++ .../model_mercury/component/status_screen.rs | 114 ++++++++++++++ .../flow/confirm_reset_device.rs | 56 +++---- .../embed/rust/src/ui/model_mercury/layout.rs | 22 ++- core/mocks/trezortranslate_keys.pyi | 2 + core/translations/en.json | 2 + core/translations/order.json | 4 +- core/translations/signatures.json | 6 +- 11 files changed, 326 insertions(+), 33 deletions(-) create mode 100644 core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs create mode 100644 core/embed/rust/src/ui/model_mercury/component/status_screen.rs diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 625ecc193..e666a8450 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -231,7 +231,9 @@ static void _librust_qstrs(void) { MP_QSTR_inputs__return; MP_QSTR_inputs__show; MP_QSTR_inputs__space; + MP_QSTR_instructions__hold_to_confirm; MP_QSTR_instructions__swipe_up; + MP_QSTR_instructions__tap_to_confirm; MP_QSTR_is_type_of; MP_QSTR_items; MP_QSTR_joint__title; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index fab0fb260..36ee94f7f 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -1238,6 +1238,8 @@ pub enum TranslatedString { storage_msg__verifying_pin = 843, // "VERIFYING PIN" storage_msg__wrong_pin = 844, // "WRONG PIN" instructions__swipe_up = 845, // "Swipe up" + instructions__tap_to_confirm = 846, // "Tap to confirm" + instructions__hold_to_confirm = 847, // "Hold to confirm" } impl TranslatedString { @@ -2471,6 +2473,8 @@ impl TranslatedString { Self::storage_msg__verifying_pin => "VERIFYING PIN", Self::storage_msg__wrong_pin => "WRONG PIN", Self::instructions__swipe_up => "Swipe up", + Self::instructions__tap_to_confirm => "Tap to confirm", + Self::instructions__hold_to_confirm => "Hold to confirm", } } @@ -3705,6 +3709,8 @@ impl TranslatedString { Qstr::MP_QSTR_storage_msg__verifying_pin => Some(Self::storage_msg__verifying_pin), Qstr::MP_QSTR_storage_msg__wrong_pin => Some(Self::storage_msg__wrong_pin), Qstr::MP_QSTR_instructions__swipe_up => Some(Self::instructions__swipe_up), + Qstr::MP_QSTR_instructions__tap_to_confirm => Some(Self::instructions__tap_to_confirm), + Qstr::MP_QSTR_instructions__hold_to_confirm => Some(Self::instructions__hold_to_confirm), _ => None, } } diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index 68bd86ce9..fd9053a01 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -21,10 +21,12 @@ mod number_input; #[cfg(feature = "translations")] mod page; mod progress; +mod prompt_screen; mod result; mod scroll; mod share_words; mod simple_page; +mod status_screen; mod swipe; mod welcome_screen; @@ -57,10 +59,12 @@ pub use number_input::{NumberInputDialog, NumberInputDialogMsg}; #[cfg(feature = "translations")] pub use page::ButtonPage; pub use progress::Progress; +pub use prompt_screen::PromptScreen; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; pub use share_words::ShareWords; pub use simple_page::SimplePage; +pub use status_screen::StatusScreen; pub use swipe::{Swipe, SwipeDirection}; pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg}; pub use welcome_screen::WelcomeScreen; diff --git a/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs new file mode 100644 index 000000000..9617f38ec --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs @@ -0,0 +1,141 @@ +use crate::{ + time::Duration, + ui::{ + component::{Component, Event, EventCtx, PageMsg}, + display::Color, + geometry::{Alignment2D, Offset, Rect}, + shape, + shape::Renderer, + }, +}; + +use super::{theme, Button, ButtonContent, ButtonMsg}; + +/// Component requesting an action from a user. Most typically embedded as a +/// content of a Frame and promptin "Tap to confirm" or "Hold to XYZ". +#[derive(Clone)] +pub struct PromptScreen { + area: Rect, + button: Button, + circle_color: Color, + circle_pad_color: Color, + circle_inner_color: Color, + dismiss_type: DismissType, +} + +#[derive(Clone)] +enum DismissType { + Tap, + Hold, +} + +impl PromptScreen { + pub fn new_hold_to_confirm() -> Self { + let icon = theme::ICON_SIGN; + let button = Button::new(ButtonContent::Icon(icon)) + .styled(theme::button_default()) + .with_long_press(Duration::from_secs(2)); + Self { + area: Rect::zero(), + circle_color: theme::GREEN, + circle_pad_color: theme::GREY_EXTRA_DARK, + circle_inner_color: theme::GREEN_LIGHT, + dismiss_type: DismissType::Hold, + button, + } + } + + pub fn new_tap_to_confirm() -> Self { + let icon = theme::ICON_SIMPLE_CHECKMARK; + let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); + Self { + area: Rect::zero(), + circle_color: theme::GREEN, + circle_inner_color: theme::GREEN, + circle_pad_color: theme::GREY_EXTRA_DARK, + dismiss_type: DismissType::Tap, + button, + } + } + + pub fn new_tap_to_cancel() -> Self { + let icon = theme::ICON_SIMPLE_CHECKMARK; + let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); + Self { + area: Rect::zero(), + circle_color: theme::ORANGE_LIGHT, + circle_inner_color: theme::ORANGE_LIGHT, + circle_pad_color: theme::GREY_EXTRA_DARK, + dismiss_type: DismissType::Tap, + button, + } + } +} + +impl Component for PromptScreen { + type Msg = PageMsg<()>; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.button.place(Rect::snap( + self.area.center(), + Offset::uniform(55), + Alignment2D::CENTER, + )); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let btn_msg = self.button.event(ctx, event); + match (&self.dismiss_type, btn_msg) { + (DismissType::Tap, Some(ButtonMsg::Clicked)) => { + return Some(PageMsg::Confirmed); + } + (DismissType::Hold, Some(ButtonMsg::LongPressed)) => { + return Some(PageMsg::Confirmed); + } + _ => (), + } + None + } + + fn paint(&mut self) { + todo!() + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::Circle::new(self.area.center(), 70) + .with_fg(self.circle_pad_color) + .with_bg(theme::BLACK) + .with_thickness(20) + .render(target); + shape::Circle::new(self.area.center(), 50) + .with_fg(self.circle_color) + .with_bg(theme::BLACK) + .with_thickness(2) + .render(target); + shape::Circle::new(self.area.center(), 48) + .with_fg(self.circle_pad_color) + .with_bg(theme::BLACK) + .with_thickness(8) + .render(target); + matches!(self.dismiss_type, DismissType::Hold).then(|| { + shape::Circle::new(self.area.center(), 40) + .with_fg(self.circle_inner_color) + .with_bg(theme::BLACK) + .with_thickness(2) + .render(target); + }); + self.button.render(target); + } +} + +impl crate::ui::flow::Swipable for PromptScreen {} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for PromptScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("StatusScreen"); + t.child("button", &self.button); + } +} diff --git a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs new file mode 100644 index 000000000..5f2208ab2 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs @@ -0,0 +1,114 @@ +use crate::{ + time::Duration, + ui::{ + component::{Component, Event, EventCtx, PageMsg}, + display::{Color, Icon}, + geometry::{Alignment2D, Rect}, + shape, + shape::Renderer, + }, +}; + +use super::{theme, Swipe, SwipeDirection}; + +/// Component showing status of an operation. Most typically embedded as a +/// content of a Frame and showing success (checkmark with a circle around). +pub struct StatusScreen { + area: Rect, + icon: Icon, + icon_color: Color, + circle_color: Color, + dismiss_type: DismissType, +} + +enum DismissType { + SwipeUp(Swipe), + Timeout(Duration), // TODO: handle disappearing StatusScreen +} + +impl StatusScreen { + fn new(icon: Icon, icon_color: Color, circle_color: Color, dismiss_style: DismissType) -> Self { + Self { + area: Rect::zero(), + icon, + icon_color, + circle_color, + dismiss_type: dismiss_style, + } + } + + pub fn new_success() -> Self { + Self::new( + theme::ICON_SIMPLE_CHECKMARK, + theme::GREEN_LIME, + theme::GREEN_LIGHT, + DismissType::SwipeUp(Swipe::new().up()), + ) + } + + pub fn new_neutral() -> Self { + Self::new( + theme::ICON_SIMPLE_CHECKMARK, + theme::GREY_EXTRA_LIGHT, + theme::GREY_DARK, + DismissType::SwipeUp(Swipe::new().up()), + ) + } +} + +impl Component for StatusScreen { + type Msg = PageMsg<()>; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + match self.dismiss_type { + DismissType::SwipeUp(ref mut swipe) => { + swipe.place(bounds); + } + DismissType::Timeout(_) => { + // TODO: handle timeout or scrub the idea? + (); + } + } + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match self.dismiss_type { + DismissType::SwipeUp(ref mut swipe) => { + let swipe_dir = swipe.event(ctx, event); + match swipe_dir { + Some(SwipeDirection::Up) => return Some(PageMsg::Confirmed), + _ => (), + } + } + // TODO: handle timeout or scrub the idea? + _ => (), + } + + None + } + + fn paint(&mut self) { + todo!() + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::Circle::new(self.area.center(), 40) + .with_fg(self.circle_color) + .with_bg(theme::BLACK) + .with_thickness(2) + .render(target); + shape::ToifImage::new(self.area.center(), self.icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(self.icon_color) + .render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for StatusScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("StatusScreen"); + } +} diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_device.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_device.rs index 32475935a..f08a51e53 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_device.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_device.rs @@ -4,7 +4,10 @@ use crate::{ strutil::TString, translations::TR, ui::{ - component::text::paragraphs::{Paragraph, Paragraphs}, + component::{ + text::paragraphs::{Paragraph, Paragraphs}, + PageMsg, + }, flow::{ base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeDirection, SwipeFlow, SwipePage, @@ -14,7 +17,7 @@ use crate::{ use heapless::Vec; use super::super::{ - component::{Frame, FrameMsg, VerticalMenu, VerticalMenuChoiceMsg}, + component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg}, theme, }; @@ -22,6 +25,7 @@ use super::super::{ pub enum ConfirmResetDevice { Intro, Menu, + Confirm, } impl FlowState for ConfirmResetDevice { @@ -33,7 +37,12 @@ impl FlowState for ConfirmResetDevice { (ConfirmResetDevice::Menu, SwipeDirection::Right) => { Decision::Goto(ConfirmResetDevice::Intro, direction) } - (ConfirmResetDevice::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed), + (ConfirmResetDevice::Intro, SwipeDirection::Up) => { + Decision::Goto(ConfirmResetDevice::Confirm, direction) + } + (ConfirmResetDevice::Confirm, SwipeDirection::Down) => { + Decision::Goto(ConfirmResetDevice::Intro, direction) + } _ => Decision::Nothing, } } @@ -47,13 +56,16 @@ impl FlowState for ConfirmResetDevice { Decision::Goto(ConfirmResetDevice::Intro, SwipeDirection::Right) } (ConfirmResetDevice::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled), + (ConfirmResetDevice::Confirm, FlowMsg::Confirmed) => { + Decision::Return(FlowMsg::Confirmed) + } _ => Decision::Nothing, } } } use crate::{ - micropython::{buffer::StrBuffer, map::Map, obj::Obj, util}, + micropython::{map::Map, obj::Obj, util}, ui::layout::obj::LayoutObj, }; @@ -96,29 +108,19 @@ impl ConfirmResetDevice { let store = flow_store() // Intro, - .add( - Frame::left_aligned(title, SwipePage::vertical(paragraphs)) - .with_info_button() - .with_footer(TR::instructions__swipe_up.into(), None), - |msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info), - )? - // Menu, - .add( - Frame::left_aligned( - "".into(), - VerticalMenu::context_menu(unwrap!(Vec::from_slice(&[( - "Cancel", // FIXME: use TString - theme::ICON_CANCEL - )]))), - ) - .with_cancel_button(), - |msg| match msg { - FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => { - Some(FlowMsg::Choice(i)) - } - FrameMsg::Button(_) => Some(FlowMsg::Cancelled), - }, - )?; + .add(content_intro, |msg| { + matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info) + })? + // Context Menu, + .add(content_menu, |msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + })? + // Confirm prompt + .add(content_confirm, |msg| match msg { + FrameMsg::Content(PageMsg::Confirmed) => Some(FlowMsg::Confirmed), + _ => Some(FlowMsg::Cancelled), + })?; let res = SwipeFlow::new(ConfirmResetDevice::Intro, store)?; Ok(LayoutObj::new(res)?.into()) diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 7b0eb7bf6..ed9ad9471 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -53,8 +53,8 @@ use super::{ FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, - SelectWordCount, SelectWordCountMsg, ShareWords, SimplePage, Slip39Input, VerticalMenu, - VerticalMenuChoiceMsg, + PromptScreen, SelectWordCount, SelectWordCountMsg, ShareWords, SimplePage, Slip39Input, + StatusScreen, VerticalMenu, VerticalMenuChoiceMsg, }, flow, theme, }; @@ -207,6 +207,24 @@ impl ComponentMsgObj for VerticalMenu { } } +impl ComponentMsgObj for StatusScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + PageMsg::Confirmed => Ok(CONFIRMED.as_obj()), + _ => Err(Error::TypeError), + } + } +} + +impl ComponentMsgObj for PromptScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + PageMsg::Confirmed => Ok(CONFIRMED.as_obj()), + _ => Err(Error::TypeError), + } + } +} + impl ComponentMsgObj for ButtonPage where T: Component + Paginate, diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 12b57f4d6..d900bde92 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -349,7 +349,9 @@ class TR: inputs__return: str = "RETURN" inputs__show: str = "SHOW" inputs__space: str = "SPACE" + instructions__hold_to_confirm: str = "Hold to confirm" instructions__swipe_up: str = "Swipe up" + instructions__tap_to_confirm: str = "Tap to confirm" joint__title: str = "JOINT TRANSACTION" joint__to_the_total_amount: str = "To the total amount:" joint__you_are_contributing: str = "You are contributing:" diff --git a/core/translations/en.json b/core/translations/en.json index 27a3f3cc4..dd7c81da5 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -352,6 +352,8 @@ "inputs__show": "SHOW", "inputs__space": "SPACE", "instructions__swipe_up": "Swipe up", + "instructions__tap_to_confirm": "Tap to confirm", + "instructions__hold_to_confirm": "Hold to confirm", "joint__title": "JOINT TRANSACTION", "joint__to_the_total_amount": "To the total amount:", "joint__you_are_contributing": "You are contributing:", diff --git a/core/translations/order.json b/core/translations/order.json index e33f4cca0..7142fdc0f 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -844,5 +844,7 @@ "842": "storage_msg__starting", "843": "storage_msg__verifying_pin", "844": "storage_msg__wrong_pin", - "845": "instructions__swipe_up" + "845": "instructions__swipe_up", + "846": "instructions__tap_to_confirm", + "847": "instructions__hold_to_confirm", } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index a008f8cda..5a6bb8c2a 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "2f1c1b3de98b6be084e1c1d51f64d7d8d11ecce193563db41065da56ba8d6fba", - "datetime": "2024-04-16T08:55:40.825962", - "commit": "dbba304f5cfd54a0a08e2fa339149dec71ab2299" + "merkle_root": "5a83288c2b984cb98dfed78216ab2b24a9f01bc6d49b1e9484d7140241d7be0c", + "datetime": "2024-04-18T10:27:35.547426", + "commit": "c09789f9dd2390b79e25be884e96ede021ab8151" }, "history": [ {