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.pull/3740/head
parent
59bde49018
commit
ef0942bacf
@ -0,0 +1,141 @@
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
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 = ();
|
||||
|
||||
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<Self::Msg> {
|
||||
let btn_msg = self.button.event(ctx, event);
|
||||
match (&self.dismiss_type, btn_msg) {
|
||||
(DismissType::Tap, Some(ButtonMsg::Clicked)) => {
|
||||
return Some(());
|
||||
}
|
||||
(DismissType::Hold, Some(ButtonMsg::LongPressed)) => {
|
||||
return Some(());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Timeout},
|
||||
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(Timeout),
|
||||
}
|
||||
|
||||
const TIMEOUT_MS: u32 = 2000;
|
||||
|
||||
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_success_timeout() -> Self {
|
||||
Self::new(
|
||||
theme::ICON_SIMPLE_CHECKMARK,
|
||||
theme::GREEN_LIME,
|
||||
theme::GREEN_LIGHT,
|
||||
DismissType::Timeout(Timeout::new(TIMEOUT_MS)),
|
||||
)
|
||||
}
|
||||
|
||||
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 = ();
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
if let DismissType::SwipeUp(swipe) = &mut self.dismiss_type {
|
||||
swipe.place(bounds);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match self.dismiss_type {
|
||||
DismissType::SwipeUp(ref mut swipe) => {
|
||||
let swipe_dir = swipe.event(ctx, event);
|
||||
match swipe_dir {
|
||||
Some(SwipeDirection::Up) => return Some(()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
DismissType::Timeout(ref mut timeout) => {
|
||||
if let Some(_) = timeout.event(ctx, event) {
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
use crate::{
|
||||
error,
|
||||
strutil::TString,
|
||||
translations::TR,
|
||||
ui::{
|
||||
component::text::paragraphs::{Paragraph, Paragraphs},
|
||||
flow::{
|
||||
base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeDirection, SwipeFlow,
|
||||
SwipePage,
|
||||
},
|
||||
},
|
||||
};
|
||||
use heapless::Vec;
|
||||
|
||||
use super::super::{
|
||||
component::{
|
||||
CancelInfoConfirmMsg, Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
|
||||
},
|
||||
theme,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
|
||||
pub enum CreateBackup {
|
||||
Intro,
|
||||
Menu,
|
||||
SkipBackupIntro,
|
||||
SkipBackupConfirm,
|
||||
}
|
||||
|
||||
impl FlowState for CreateBackup {
|
||||
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
|
||||
match (self, direction) {
|
||||
(CreateBackup::Intro, SwipeDirection::Left) => {
|
||||
Decision::Goto(CreateBackup::Menu, direction)
|
||||
}
|
||||
(CreateBackup::SkipBackupIntro, SwipeDirection::Up) => {
|
||||
Decision::Goto(CreateBackup::SkipBackupConfirm, direction)
|
||||
}
|
||||
(CreateBackup::SkipBackupConfirm, SwipeDirection::Down) => {
|
||||
Decision::Goto(CreateBackup::SkipBackupIntro, direction)
|
||||
}
|
||||
(CreateBackup::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
|
||||
_ => Decision::Nothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
|
||||
match (self, msg) {
|
||||
(CreateBackup::Intro, FlowMsg::Info) => {
|
||||
Decision::Goto(CreateBackup::Menu, SwipeDirection::Left)
|
||||
}
|
||||
(CreateBackup::Menu, FlowMsg::Choice(0)) => {
|
||||
Decision::Goto(CreateBackup::SkipBackupIntro, SwipeDirection::Left)
|
||||
}
|
||||
(CreateBackup::Menu, FlowMsg::Cancelled) => {
|
||||
Decision::Goto(CreateBackup::Intro, SwipeDirection::Right)
|
||||
}
|
||||
(CreateBackup::SkipBackupIntro, FlowMsg::Cancelled) => {
|
||||
Decision::Goto(CreateBackup::Menu, SwipeDirection::Right)
|
||||
}
|
||||
(CreateBackup::SkipBackupConfirm, FlowMsg::Cancelled) => {
|
||||
Decision::Goto(CreateBackup::SkipBackupIntro, SwipeDirection::Right)
|
||||
}
|
||||
(CreateBackup::SkipBackupConfirm, FlowMsg::Confirmed) => {
|
||||
Decision::Return(FlowMsg::Cancelled)
|
||||
}
|
||||
_ => Decision::Nothing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{
|
||||
micropython::{map::Map, obj::Obj, util},
|
||||
ui::layout::obj::LayoutObj,
|
||||
};
|
||||
|
||||
pub extern "C" fn new_create_backup(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, CreateBackup::new) }
|
||||
}
|
||||
|
||||
impl CreateBackup {
|
||||
fn new(_args: &[Obj], _kwargs: &Map) -> Result<Obj, error::Error> {
|
||||
let title: TString = TR::backup__title_backup_wallet.into();
|
||||
let par_array: [Paragraph<'static>; 1] = [Paragraph::new(
|
||||
&theme::TEXT_MAIN_GREY_LIGHT,
|
||||
TString::from_str("Your wallet backup contains X words in a specific order."),
|
||||
)];
|
||||
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);
|
||||
|
||||
let content_menu = Frame::left_aligned(
|
||||
"".into(),
|
||||
VerticalMenu::context_menu(unwrap!(Vec::from_slice(&[(
|
||||
"Skip backup", // FIXME: use TString
|
||||
theme::ICON_CANCEL
|
||||
)]))),
|
||||
)
|
||||
.with_cancel_button();
|
||||
|
||||
let par_array_skip_intro: [Paragraph<'static>; 2] = [
|
||||
Paragraph::new(&theme::TEXT_WARNING, TString::from_str("Not recommended!")),
|
||||
Paragraph::new(
|
||||
&theme::TEXT_MAIN_GREY_LIGHT,
|
||||
TString::from_str("Create a backup to avoid losing access to your funds"),
|
||||
),
|
||||
];
|
||||
let paragraphs_skip_intro = Paragraphs::new(par_array_skip_intro);
|
||||
let content_skip_intro = Frame::left_aligned(
|
||||
TR::backup__title_skip.into(),
|
||||
SwipePage::vertical(paragraphs_skip_intro),
|
||||
)
|
||||
.with_cancel_button()
|
||||
.with_footer(
|
||||
TR::instructions__swipe_up.into(),
|
||||
Some(TR::words__continue_anyway.into()),
|
||||
);
|
||||
|
||||
let content_skip_confirm = Frame::left_aligned(
|
||||
TR::backup__title_skip.into(),
|
||||
PromptScreen::new_tap_to_cancel(),
|
||||
)
|
||||
.with_footer(TR::instructions__tap_to_confirm.into(), None);
|
||||
|
||||
let store = flow_store()
|
||||
.add(content_intro, |msg| {
|
||||
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
|
||||
})?
|
||||
.add(content_menu, |msg| match msg {
|
||||
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
||||
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
|
||||
FrameMsg::Button(_) => None,
|
||||
})?
|
||||
.add(content_skip_intro, |msg| match msg {
|
||||
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
|
||||
_ => None,
|
||||
})?
|
||||
.add(content_skip_confirm, |msg| match msg {
|
||||
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
||||
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
|
||||
_ => None,
|
||||
})?;
|
||||
let res = SwipeFlow::new(CreateBackup::Intro, store)?;
|
||||
Ok(LayoutObj::new(res)?.into())
|
||||
}
|
||||
}
|
Loading…
Reference in new issue