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:
parent
10234787a4
commit
1363495165
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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>,
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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> {}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {}
|
||||||
|
187
core/embed/rust/src/ui/model_mercury/flow/get_address.rs
Normal file
187
core/embed/rust/src/ui/model_mercury/flow/get_address.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
3
core/embed/rust/src/ui/model_mercury/flow/mod.rs
Normal file
3
core/embed/rust/src/ui/model_mercury/flow/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod get_address;
|
||||||
|
|
||||||
|
pub use get_address::GetAddress;
|
@ -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)]
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user