diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index 77f0ce8652..ab62262b53 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -75,6 +75,7 @@ pub trait Component { /// dirty flag for it. Any mutation of `T` has to happen through the `mutate` /// accessor, `T` can then request a paint call to be scheduled later by calling /// `EventCtx::request_paint` in its `event` pass. +#[derive(Clone)] pub struct Child { component: T, marked_for_paint: bool, diff --git a/core/embed/rust/src/ui/component/image.rs b/core/embed/rust/src/ui/component/image.rs index 300933b219..1cae0794f8 100644 --- a/core/embed/rust/src/ui/component/image.rs +++ b/core/embed/rust/src/ui/component/image.rs @@ -72,6 +72,7 @@ impl crate::trace::Trace for Image { } } +#[derive(Clone)] pub struct BlendedImage { bg: Icon, fg: Icon, diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index 2faac8a2a5..6efd6f7475 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -10,6 +10,7 @@ use crate::{ use super::{text::TextStyle, TextLayout}; +#[derive(Clone)] pub struct Label<'a> { text: TString<'a>, layout: TextLayout, diff --git a/core/embed/rust/src/ui/component/qr_code.rs b/core/embed/rust/src/ui/component/qr_code.rs index a5ea3db96c..affe9320a1 100644 --- a/core/embed/rust/src/ui/component/qr_code.rs +++ b/core/embed/rust/src/ui/component/qr_code.rs @@ -25,6 +25,7 @@ const CORNER_RADIUS: u8 = 4; const DARK: Color = Color::rgb(0, 0, 0); const LIGHT: Color = Color::rgb(0xff, 0xff, 0xff); +#[derive(Clone)] pub struct Qr { text: String, border: i16, diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index b9cc667b4b..eb33aef3f9 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -47,6 +47,7 @@ pub trait ParagraphSource<'a> { } } +#[derive(Clone)] pub struct Paragraphs { area: Rect, placement: LinearPlacement, @@ -335,6 +336,7 @@ impl<'a> Paragraph<'a> { } } +#[derive(Clone)] struct TextLayoutProxy { offset: PageOffset, bounds: Rect, diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs index aed7f508f0..708a106aa6 100644 --- a/core/embed/rust/src/ui/component/timeout.rs +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -7,6 +7,7 @@ use crate::{ }, }; +#[derive(Clone)] pub struct Timeout { time_ms: u32, timer: Option, diff --git a/core/embed/rust/src/ui/model_mercury/component/address_details.rs b/core/embed/rust/src/ui/model_mercury/component/address_details.rs index f79276dfd6..c3e7ca7dd8 100644 --- a/core/embed/rust/src/ui/model_mercury/component/address_details.rs +++ b/core/embed/rust/src/ui/model_mercury/component/address_details.rs @@ -95,7 +95,7 @@ impl AddressDetails { // repaint after page change so we can use a dummy context here. let mut dummy_ctx = EventCtx::new(); self.xpub_view.update_title(&mut dummy_ctx, self.xpubs[i].0); - self.xpub_view.update_content(&mut dummy_ctx, |p| { + self.xpub_view.update_content(&mut dummy_ctx, |_ctx, p| { p.inner_mut().update(self.xpubs[i].1); let npages = p.page_count(); p.change_page(page); diff --git a/core/embed/rust/src/ui/model_mercury/component/button.rs b/core/embed/rust/src/ui/model_mercury/component/button.rs index 037aa38294..257a3fb921 100644 --- a/core/embed/rust/src/ui/model_mercury/component/button.rs +++ b/core/embed/rust/src/ui/model_mercury/component/button.rs @@ -22,6 +22,7 @@ pub enum ButtonMsg { LongPressed, } +#[derive(Clone)] pub struct Button { area: Rect, touch_expand: Option, @@ -386,7 +387,7 @@ impl crate::trace::Trace for Button { } } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] enum State { Initial, Pressed, @@ -394,7 +395,7 @@ enum State { Disabled, } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] pub enum ButtonContent { Empty, Text(TString<'static>), @@ -510,6 +511,7 @@ impl Button { } } +#[derive(Copy, Clone)] pub enum CancelConfirmMsg { Cancelled, Confirmed, diff --git a/core/embed/rust/src/ui/model_mercury/component/dialog.rs b/core/embed/rust/src/ui/model_mercury/component/dialog.rs index c66d37a91f..3766da6c40 100644 --- a/core/embed/rust/src/ui/model_mercury/component/dialog.rs +++ b/core/embed/rust/src/ui/model_mercury/component/dialog.rs @@ -97,6 +97,7 @@ where } } +#[derive(Clone)] pub struct IconDialog { image: Child, paragraphs: Paragraphs>, @@ -228,3 +229,5 @@ where t.child("controls", &self.controls); } } + +impl crate::ui::flow::Swipable for IconDialog {} diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index bd74351e01..62a697adba 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -15,6 +15,7 @@ use super::{Button, ButtonMsg, CancelInfoConfirmMsg}; const TITLE_HEIGHT: i16 = 42; +#[derive(Clone)] pub struct Frame { border: Insets, title: Child>, @@ -109,10 +110,10 @@ where pub fn update_content(&mut self, ctx: &mut EventCtx, update_fn: F) -> R where - F: Fn(&mut T) -> R, + F: Fn(&mut EventCtx, &mut T) -> R, { self.content.mutate(ctx, |ctx, c| { - let res = update_fn(c); + let res = update_fn(ctx, c); c.request_complete_repaint(ctx); res }) @@ -195,3 +196,16 @@ where } } } + +impl crate::ui::flow::Swipable for Frame +where + T: Component + crate::ui::flow::Swipable, +{ + fn can_swipe(&self, direction: crate::ui::flow::SwipeDirection) -> bool { + self.inner().can_swipe(direction) + } + + fn swiped(&mut self, ctx: &mut EventCtx, direction: crate::ui::flow::SwipeDirection) { + self.update_content(ctx, |ctx, inner| inner.swiped(ctx, direction)) + } +} diff --git a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs index 5045f3187c..c403f6b244 100644 --- a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs +++ b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs @@ -34,6 +34,7 @@ const MENU_SEP_HEIGHT: i16 = 2; type VerticalMenuButtons = Vec; type AreasForSeparators = Vec; +#[derive(Clone)] pub struct VerticalMenu { area: Rect, /// buttons placed vertically from top to bottom @@ -150,3 +151,5 @@ impl crate::trace::Trace for VerticalMenu { }); } } + +impl crate::ui::flow::Swipable for VerticalMenu {} diff --git a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs new file mode 100644 index 0000000000..c56037f4fc --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs @@ -0,0 +1,187 @@ +use crate::{ + error, + ui::{ + component::{ + image::BlendedImage, + text::paragraphs::{Paragraph, Paragraphs}, + Qr, Timeout, + }, + flow::{ + base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeDirection, + SwipeFlow, SwipePage, + }, + }, +}; + +use super::super::{ + component::{Frame, FrameMsg, IconDialog, VerticalMenu, VerticalMenuChoiceMsg}, + theme, +}; + +const LONGSTRING: &'static str = "https://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKo"; + +#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)] +pub enum GetAddress { + Address, + Menu, + QrCode, + AccountInfo, + Cancel, + Success, +} + +impl FlowState for GetAddress { + fn handle_swipe(&self, direction: SwipeDirection) -> Decision { + match (self, direction) { + (GetAddress::Address, SwipeDirection::Left) => { + Decision::Goto(GetAddress::Menu, direction) + } + (GetAddress::Address, SwipeDirection::Up) => { + Decision::Goto(GetAddress::Success, direction) + } + (GetAddress::Menu, SwipeDirection::Right) => { + Decision::Goto(GetAddress::Address, direction) + } + (GetAddress::QrCode, SwipeDirection::Right) => { + Decision::Goto(GetAddress::Menu, direction) + } + (GetAddress::AccountInfo, SwipeDirection::Right) => { + Decision::Goto(GetAddress::Menu, direction) + } + (GetAddress::Cancel, SwipeDirection::Up) => Decision::Return(FlowMsg::Cancelled), + _ => Decision::Nothing, + } + } + + fn handle_event(&self, msg: FlowMsg) -> Decision { + match (self, msg) { + (GetAddress::Address, FlowMsg::Info) => { + Decision::Goto(GetAddress::Menu, SwipeDirection::Left) + } + + (GetAddress::Menu, FlowMsg::Choice(0)) => { + Decision::Goto(GetAddress::QrCode, SwipeDirection::Left) + } + + (GetAddress::Menu, FlowMsg::Choice(1)) => { + Decision::Goto(GetAddress::AccountInfo, SwipeDirection::Left) + } + + (GetAddress::Menu, FlowMsg::Choice(2)) => { + Decision::Goto(GetAddress::Cancel, SwipeDirection::Left) + } + + (GetAddress::Menu, FlowMsg::Cancelled) => { + Decision::Goto(GetAddress::Address, SwipeDirection::Right) + } + + (GetAddress::QrCode, FlowMsg::Cancelled) => { + Decision::Goto(GetAddress::Menu, SwipeDirection::Right) + } + + (GetAddress::AccountInfo, FlowMsg::Cancelled) => { + Decision::Goto(GetAddress::Menu, SwipeDirection::Right) + } + + (GetAddress::Cancel, FlowMsg::Cancelled) => { + Decision::Goto(GetAddress::Menu, SwipeDirection::Right) + } + + (GetAddress::Success, _) => Decision::Return(FlowMsg::Confirmed), + _ => Decision::Nothing, + } + } +} + +use crate::{ + micropython::{buffer::StrBuffer, map::Map, obj::Obj, util}, + ui::layout::obj::LayoutObj, +}; + +pub extern "C" fn new_get_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, GetAddress::new) } +} + +impl GetAddress { + fn new(_args: &[Obj], _kwargs: &Map) -> Result { + let store = flow_store() + .add( + Frame::left_aligned( + "Receive".into(), + SwipePage::vertical(Paragraphs::new(Paragraph::new( + &theme::TEXT_MONO, + StrBuffer::from(LONGSTRING), + ))), + ) + .with_subtitle("address".into()) + .with_info_button(), + |msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info), + )? + .add( + Frame::left_aligned( + "".into(), + VerticalMenu::context_menu([ + ("Address QR code", theme::ICON_QR_CODE), + ("Account info", theme::ICON_CHEVRON_RIGHT), + ("Cancel trans.", theme::ICON_CANCEL), + ]), + ) + .with_cancel_button(), + |msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => { + Some(FlowMsg::Choice(i)) + } + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + }, + )? + .add( + Frame::left_aligned( + "Receive address".into(), + IgnoreSwipe::new(Qr::new( + "https://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKo", + true, + )?), + ) + .with_cancel_button(), + |msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled), + )? + .add( + Frame::left_aligned( + "Account info".into(), + SwipePage::vertical(Paragraphs::new(Paragraph::new( + &theme::TEXT_NORMAL, + StrBuffer::from("taproot xp"), + ))), + ) + .with_cancel_button(), + |msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled), + )? + .add( + Frame::left_aligned( + "Cancel receive".into(), + SwipePage::vertical(Paragraphs::new(Paragraph::new( + &theme::TEXT_NORMAL, + StrBuffer::from("O rly?"), + ))), + ) + .with_cancel_button(), + |msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled), + )? + .add( + IconDialog::new( + BlendedImage::new( + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_WARN, + theme::SUCCESS_COLOR, + theme::FG, + theme::BG, + ), + StrBuffer::from("Confirmed"), + Timeout::new(100), + ), + |_| Some(FlowMsg::Confirmed), + )?; + let res = SwipeFlow::new(GetAddress::Address, store)?; + Ok(LayoutObj::new(res)?.into()) + } +} diff --git a/core/embed/rust/src/ui/model_mercury/flow/mod.rs b/core/embed/rust/src/ui/model_mercury/flow/mod.rs new file mode 100644 index 0000000000..be27c0c4c1 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -0,0 +1,3 @@ +pub mod get_address; + +pub use get_address::GetAddress; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 1012d24b3b..0afce326ce 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -56,7 +56,7 @@ use super::{ SelectWordCount, SelectWordCountMsg, SelectWordMsg, ShareWords, SimplePage, Slip39Input, VerticalMenu, VerticalMenuChoiceMsg, }, - theme, + flow, theme, }; impl TryFrom for Obj { @@ -2152,6 +2152,10 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_wait_text(message: str, /) -> LayoutObj[None]: /// """Show single-line text in the middle of the screen.""" Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(), + + /// def flow_get_address() -> LayoutObj[UiResult]: + /// """Get address / receive funds.""" + Qstr::MP_QSTR_flow_get_address => obj_fn_kw!(0, flow::get_address::new_get_address).as_obj(), }; #[cfg(test)] diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index efbb8e289a..f4fb27f2d6 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -523,6 +523,11 @@ def confirm_firmware_update( # rust/src/ui/model_mercury/layout.rs def show_wait_text(message: str, /) -> LayoutObj[None]: """Show single-line text in the middle of the screen.""" + + +# rust/src/ui/model_mercury/layout.rs +def flow_get_address() -> LayoutObj[UiResult]: + """Get address / receive funds.""" CONFIRMED: UiResult CANCELLED: UiResult INFO: UiResult diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index 636481ea65..2550c6b9cc 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -278,6 +278,17 @@ async def confirm_action( ) +async def flow_demo() -> None: + await raise_if_not_confirmed( + interact( + RustLayout(trezorui2.flow_get_address()), + "get_address", + BR_TYPE_OTHER, + ), + ActionCancelled, + ) + + async def confirm_single( br_type: str, title: str,