1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-24 07:18:09 +00:00

feat(core/ui): GetAddress flow demo

This commit is contained in:
Martin Milata 2024-04-12 00:21:05 +02:00
parent 10234787a4
commit 1363495165
16 changed files with 245 additions and 6 deletions

View File

@ -75,6 +75,7 @@ pub trait Component {
/// dirty flag for it. Any mutation of `T` has to happen through the `mutate` /// 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 /// accessor, `T` can then request a paint call to be scheduled later by calling
/// `EventCtx::request_paint` in its `event` pass. /// `EventCtx::request_paint` in its `event` pass.
#[derive(Clone)]
pub struct Child<T> { pub struct Child<T> {
component: T, component: T,
marked_for_paint: bool, marked_for_paint: bool,

View File

@ -72,6 +72,7 @@ impl crate::trace::Trace for Image {
} }
} }
#[derive(Clone)]
pub struct BlendedImage { pub struct BlendedImage {
bg: Icon, bg: Icon,
fg: Icon, fg: Icon,

View File

@ -10,6 +10,7 @@ use crate::{
use super::{text::TextStyle, TextLayout}; use super::{text::TextStyle, TextLayout};
#[derive(Clone)]
pub struct Label<'a> { pub struct Label<'a> {
text: TString<'a>, text: TString<'a>,
layout: TextLayout, layout: TextLayout,

View File

@ -25,6 +25,7 @@ const CORNER_RADIUS: u8 = 4;
const DARK: Color = Color::rgb(0, 0, 0); const DARK: Color = Color::rgb(0, 0, 0);
const LIGHT: Color = Color::rgb(0xff, 0xff, 0xff); const LIGHT: Color = Color::rgb(0xff, 0xff, 0xff);
#[derive(Clone)]
pub struct Qr { pub struct Qr {
text: String<MAX_DATA>, text: String<MAX_DATA>,
border: i16, border: i16,

View File

@ -47,6 +47,7 @@ pub trait ParagraphSource<'a> {
} }
} }
#[derive(Clone)]
pub struct Paragraphs<T> { pub struct Paragraphs<T> {
area: Rect, area: Rect,
placement: LinearPlacement, placement: LinearPlacement,
@ -335,6 +336,7 @@ impl<'a> Paragraph<'a> {
} }
} }
#[derive(Clone)]
struct TextLayoutProxy { struct TextLayoutProxy {
offset: PageOffset, offset: PageOffset,
bounds: Rect, bounds: Rect,

View File

@ -7,6 +7,7 @@ use crate::{
}, },
}; };
#[derive(Clone)]
pub struct Timeout { pub struct Timeout {
time_ms: u32, time_ms: u32,
timer: Option<TimerToken>, timer: Option<TimerToken>,

View File

@ -95,7 +95,7 @@ impl AddressDetails {
// repaint after page change so we can use a dummy context here. // repaint after page change so we can use a dummy context here.
let mut dummy_ctx = EventCtx::new(); let mut dummy_ctx = EventCtx::new();
self.xpub_view.update_title(&mut dummy_ctx, self.xpubs[i].0); 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); p.inner_mut().update(self.xpubs[i].1);
let npages = p.page_count(); let npages = p.page_count();
p.change_page(page); p.change_page(page);

View File

@ -22,6 +22,7 @@ pub enum ButtonMsg {
LongPressed, LongPressed,
} }
#[derive(Clone)]
pub struct Button { pub struct Button {
area: Rect, area: Rect,
touch_expand: Option<Insets>, touch_expand: Option<Insets>,
@ -386,7 +387,7 @@ impl crate::trace::Trace for Button {
} }
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Clone)]
enum State { enum State {
Initial, Initial,
Pressed, Pressed,
@ -394,7 +395,7 @@ enum State {
Disabled, Disabled,
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Clone)]
pub enum ButtonContent { pub enum ButtonContent {
Empty, Empty,
Text(TString<'static>), Text(TString<'static>),
@ -510,6 +511,7 @@ impl Button {
} }
} }
#[derive(Copy, Clone)]
pub enum CancelConfirmMsg { pub enum CancelConfirmMsg {
Cancelled, Cancelled,
Confirmed, Confirmed,

View File

@ -97,6 +97,7 @@ where
} }
} }
#[derive(Clone)]
pub struct IconDialog<U> { pub struct IconDialog<U> {
image: Child<BlendedImage>, image: Child<BlendedImage>,
paragraphs: Paragraphs<ParagraphVecShort<'static>>, paragraphs: Paragraphs<ParagraphVecShort<'static>>,
@ -228,3 +229,5 @@ where
t.child("controls", &self.controls); t.child("controls", &self.controls);
} }
} }
impl<U> crate::ui::flow::Swipable for IconDialog<U> {}

View File

@ -15,6 +15,7 @@ use super::{Button, ButtonMsg, CancelInfoConfirmMsg};
const TITLE_HEIGHT: i16 = 42; const TITLE_HEIGHT: i16 = 42;
#[derive(Clone)]
pub struct Frame<T> { pub struct Frame<T> {
border: Insets, border: Insets,
title: Child<Label<'static>>, title: Child<Label<'static>>,
@ -109,10 +110,10 @@ where
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where where
F: Fn(&mut T) -> R, F: Fn(&mut EventCtx, &mut T) -> R,
{ {
self.content.mutate(ctx, |ctx, c| { self.content.mutate(ctx, |ctx, c| {
let res = update_fn(c); let res = update_fn(ctx, c);
c.request_complete_repaint(ctx); c.request_complete_repaint(ctx);
res res
}) })
@ -195,3 +196,16 @@ where
} }
} }
} }
impl<T> crate::ui::flow::Swipable for Frame<T>
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))
}
}

View File

@ -34,6 +34,7 @@ const MENU_SEP_HEIGHT: i16 = 2;
type VerticalMenuButtons = Vec<Button, N_ITEMS>; type VerticalMenuButtons = Vec<Button, N_ITEMS>;
type AreasForSeparators = Vec<Rect, N_SEPS>; type AreasForSeparators = Vec<Rect, N_SEPS>;
#[derive(Clone)]
pub struct VerticalMenu { pub struct VerticalMenu {
area: Rect, area: Rect,
/// buttons placed vertically from top to bottom /// buttons placed vertically from top to bottom
@ -150,3 +151,5 @@ impl crate::trace::Trace for VerticalMenu {
}); });
} }
} }
impl crate::ui::flow::Swipable for VerticalMenu {}

View File

@ -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<Self> {
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<Self> {
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<Obj, error::Error> {
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())
}
}

View File

@ -0,0 +1,3 @@
pub mod get_address;
pub use get_address::GetAddress;

View File

@ -56,7 +56,7 @@ use super::{
SelectWordCount, SelectWordCountMsg, SelectWordMsg, ShareWords, SimplePage, Slip39Input, SelectWordCount, SelectWordCountMsg, SelectWordMsg, ShareWords, SimplePage, Slip39Input,
VerticalMenu, VerticalMenuChoiceMsg, VerticalMenu, VerticalMenuChoiceMsg,
}, },
theme, flow, theme,
}; };
impl TryFrom<CancelConfirmMsg> for Obj { impl TryFrom<CancelConfirmMsg> for Obj {
@ -2152,6 +2152,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def show_wait_text(message: str, /) -> LayoutObj[None]: /// def show_wait_text(message: str, /) -> LayoutObj[None]:
/// """Show single-line text in the middle of the screen.""" /// """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(), 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)] #[cfg(test)]

View File

@ -523,6 +523,11 @@ def confirm_firmware_update(
# rust/src/ui/model_mercury/layout.rs # rust/src/ui/model_mercury/layout.rs
def show_wait_text(message: str, /) -> LayoutObj[None]: def show_wait_text(message: str, /) -> LayoutObj[None]:
"""Show single-line text in the middle of the screen.""" """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 CONFIRMED: UiResult
CANCELLED: UiResult CANCELLED: UiResult
INFO: UiResult INFO: UiResult

View File

@ -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( async def confirm_single(
br_type: str, br_type: str,
title: str, title: str,