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.
obrusvit/ui-t3t1-reset-device-apple-hotfix
obrusvit 3 weeks ago
parent 85c1c92918
commit 36e2c98013

@ -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;

@ -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,
}
}

@ -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;

@ -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<Self::Msg> {
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);
}
}

@ -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<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(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");
}
}

@ -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())

@ -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<Obj, Error> {
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<Obj, Error> {
match msg {
PageMsg::Confirmed => Ok(CONFIRMED.as_obj()),
_ => Err(Error::TypeError),
}
}
}
impl<T> ComponentMsgObj for ButtonPage<T>
where
T: Component + Paginate,

@ -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:"

@ -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:",

@ -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",
}

@ -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": [
{

Loading…
Cancel
Save