Compare commits

...

4 Commits

@ -1,6 +1,7 @@
use super::{Component, Event, EventCtx};
use crate::ui::{geometry::Rect, shape::Renderer};
#[derive(Clone)]
pub struct MsgMap<T, F> {
inner: T,
func: F,
@ -50,3 +51,17 @@ where
self.inner.trace(t)
}
}
#[cfg(all(feature = "micropython", feature = "touch"))]
impl<T, F> crate::ui::flow::Swipable for MsgMap<T, F>
where
T: Component + crate::ui::flow::Swipable,
{
fn can_swipe(&self, direction: crate::ui::component::SwipeDirection) -> bool {
self.inner.can_swipe(direction)
}
fn swiped(&mut self, ctx: &mut EventCtx, direction: crate::ui::component::SwipeDirection) {
self.inner.swiped(ctx, direction)
}
}

@ -71,7 +71,7 @@ impl<Q> Decision<Q> {
/// triggered by events and swipes.
pub trait FlowState
where
Self: Sized + Copy + PartialEq + Eq + ToPrimitive,
Self: Sized + Copy + Eq + ToPrimitive,
{
/// There needs to be a mapping from states to indices of the FlowStore
/// array. Default implementation works for states that are enums, the

@ -32,8 +32,11 @@ pub struct SwipeFlow<Q, S> {
}
struct Transition<Q> {
/// State we are transitioning _from_.
prev_state: Q,
/// Animation progress.
animation: Animation<Offset>,
/// Direction of the slide animation.
direction: SwipeDirection,
}
@ -142,6 +145,7 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
// TODO: are there any events we want to send to all? timers perhaps?
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
self.handle_transition(ctx);
return None;
}
// Ignore events while transition is running.
if self.transition.is_some() {

@ -21,8 +21,7 @@ pub trait FlowStore {
/// Call `Component::place` on all elements.
fn place(&mut self, bounds: Rect) -> Rect;
/// Call `Component::event` on i-th element, if it emits a message it is
/// converted to `FlowMsg` using a function.
/// Call `Component::event` on i-th element.
fn event(&mut self, i: usize, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg>;
/// Call `Component::render` on i-th element.
@ -42,10 +41,9 @@ pub trait FlowStore {
fn render_cloned<'s>(&'s self, target: &mut impl Renderer<'s>);
/// Add a Component to the end of a `FlowStore`.
fn add<E: Component + MaybeTrace + Swipable + Clone>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
self,
elem: E,
func: fn(E::Msg) -> Option<FlowMsg>,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized;
@ -87,38 +85,33 @@ impl FlowStore for FlowEmpty {
}
fn render_cloned<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
fn add<E: Component + MaybeTrace + Swipable + Clone>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
self,
elem: E,
func: fn(E::Msg) -> Option<FlowMsg>,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized,
{
Ok(FlowComponent {
elem: Gc::new(elem)?,
func,
cloned: None,
next: Self,
})
}
}
struct FlowComponent<E: Component, P> {
struct FlowComponent<E: Component<Msg = FlowMsg>, P> {
/// Component allocated on micropython heap.
pub elem: Gc<E>,
/// Clone.
pub cloned: Option<Gc<E>>,
/// Function to convert message to `FlowMsg`.
pub func: fn(E::Msg) -> Option<FlowMsg>,
/// Nested FlowStore.
pub next: P,
}
impl<E: Component, P> FlowComponent<E, P> {
impl<E: Component<Msg = FlowMsg>, P> FlowComponent<E, P> {
fn as_ref(&self) -> &E {
&self.elem
}
@ -132,7 +125,7 @@ impl<E: Component, P> FlowComponent<E, P> {
impl<E, P> FlowStore for FlowComponent<E, P>
where
E: Component + MaybeTrace + Swipable + Clone,
E: Component<Msg = FlowMsg> + MaybeTrace + Swipable + Clone,
P: FlowStore,
{
fn place(&mut self, bounds: Rect) -> Rect {
@ -143,8 +136,7 @@ where
fn event(&mut self, i: usize, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg> {
if i == 0 {
let msg = self.as_mut().event(ctx, event);
msg.and_then(self.func)
self.as_mut().event(ctx, event)
} else {
self.next.event(i - 1, ctx, event)
}
@ -201,19 +193,17 @@ where
self.next.render_cloned(target);
}
fn add<F: Component + MaybeTrace + Swipable + Clone>(
fn add<F: Component<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
self,
elem: F,
func: fn(F::Msg) -> Option<FlowMsg>,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized,
{
Ok(FlowComponent {
elem: self.elem,
func: self.func,
cloned: None,
next: self.next.add(elem, func)?,
next: self.next.add(elem)?,
})
}
}

@ -60,12 +60,9 @@ impl AddressDetails {
)
.with_cancel_button()
.with_border(theme::borders_horizontal_scroll()),
details: Frame::left_aligned(
details_title,
para.into_paragraphs(),
)
.with_cancel_button()
.with_border(theme::borders_horizontal_scroll()),
details: Frame::left_aligned(details_title, para.into_paragraphs())
.with_cancel_button()
.with_border(theme::borders_horizontal_scroll()),
xpub_view: Frame::left_aligned(
" \n ".into(),
Paragraph::new(&theme::TEXT_MONO, "").into_paragraphs(),

@ -9,7 +9,7 @@ use crate::{
},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment2D, Insets, Offset, Point, Rect},
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
@ -30,6 +30,7 @@ pub struct Button {
touch_expand: Option<Insets>,
content: ButtonContent,
styles: ButtonStyleSheet,
text_align: Alignment,
state: State,
long_press: Option<Duration>,
long_timer: Option<TimerToken>,
@ -47,6 +48,7 @@ impl Button {
area: Rect::zero(),
touch_expand: None,
styles: theme::button_default(),
text_align: Alignment::Start,
state: State::Initial,
long_press: None,
long_timer: None,
@ -78,6 +80,11 @@ impl Button {
self
}
pub const fn with_text_align(mut self, align: Alignment) -> Self {
self.text_align = align;
self
}
pub const fn with_expanded_touch_area(mut self, expand: Insets) -> Self {
self.touch_expand = Some(expand);
self
@ -217,11 +224,19 @@ impl Button {
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let start_of_baseline = self.area.left_center() + Self::BASELINE_OFFSET;
let y_offset = Offset::y(self.style().font.allcase_text_height() / 2);
let start_of_baseline = match self.text_align {
Alignment::Start => {
self.area.left_center() + Offset::x(Self::BASELINE_OFFSET.x)
}
Alignment::Center => self.area.center(),
Alignment::End => self.area.right_center() - Offset::x(Self::BASELINE_OFFSET.x),
} + y_offset;
text.map(|text| {
shape::Text::new(start_of_baseline, text)
.with_font(style.font)
.with_fg(style.text_color)
.with_align(self.text_align)
.render(target);
});
}

@ -23,9 +23,9 @@ use heapless::String;
const MAX_LENGTH: usize = 8;
pub struct Bip39Input {
button: Button<>,
button: Button,
// used only to keep track of suggestion text color
button_suggestion: Button<>,
button_suggestion: Button,
textbox: TextBox<MAX_LENGTH>,
multi_tap: MultiTapKeyboard,
options_num: Option<usize>,

@ -1,6 +1,6 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Grid, GridCellSpan, Rect},
geometry::{Alignment, Grid, GridCellSpan, Rect},
model_mercury::{
component::button::{Button, ButtonMsg},
theme,
@ -10,7 +10,7 @@ use crate::ui::{
const NUMBERS: [u32; 5] = [12, 18, 20, 24, 33];
const LABELS: [&str; 5] = ["12", "18", "20", "24", "33"];
const CELLS: [(usize, usize); 5] = [(0, 0), (0, 2), (0, 4), (1, 0), (1, 2)];
const CELLS: [(usize, usize); 5] = [(0, 0), (0, 2), (1, 0), (1, 2), (2, 1)];
pub struct SelectWordCount {
button: [Button; NUMBERS.len()],
@ -23,7 +23,11 @@ pub enum SelectWordCountMsg {
impl SelectWordCount {
pub fn new() -> Self {
SelectWordCount {
button: LABELS.map(|t| Button::with_text(t.into()).styled(theme::button_pin())),
button: LABELS.map(|t| {
Button::with_text(t.into())
.styled(theme::button_pin())
.with_text_align(Alignment::Center)
}),
}
}
}
@ -32,8 +36,13 @@ impl Component for SelectWordCount {
type Msg = SelectWordCountMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (_, bounds) = bounds.split_bottom(2 * theme::BUTTON_HEIGHT + theme::BUTTON_SPACING);
let grid = Grid::new(bounds, 2, 6).with_spacing(theme::BUTTON_SPACING);
let n_rows: usize = 3;
let n_cols: usize = 4;
let (_, bounds) = bounds.split_bottom(
n_rows as i16 * theme::BUTTON_HEIGHT + (n_rows as i16 - 1) * theme::BUTTON_SPACING,
);
let grid = Grid::new(bounds, n_rows, n_cols).with_spacing(theme::BUTTON_SPACING);
for (btn, (x, y)) in self.button.iter_mut().zip(CELLS) {
btn.place(grid.cells(GridCellSpan {
from: (x, y),

@ -6,7 +6,7 @@ use crate::{
ui::{
component::{
text::paragraphs::{Paragraph, Paragraphs},
SwipeDirection,
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
@ -86,7 +86,8 @@ impl ConfirmResetDevice {
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);
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
let content_menu = Frame::left_aligned(
"".into(),
@ -95,29 +96,29 @@ impl ConfirmResetDevice {
theme::ICON_CANCEL
)]))),
)
.with_cancel_button();
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let content_confirm = Frame::left_aligned(
TR::reset__title_create_wallet.into(),
PromptScreen::new_hold_to_confirm(),
)
.with_footer(TR::instructions__hold_to_confirm.into(), None);
.with_footer(TR::instructions__hold_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
_ => Some(FlowMsg::Cancelled),
});
let store = flow_store()
// Intro,
.add(content_intro, |msg| {
matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)
})?
.add(content_intro)?
// Context Menu,
.add(content_menu, |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
})?
.add(content_menu)?
// Confirm prompt
.add(content_confirm, |msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
_ => Some(FlowMsg::Cancelled),
})?;
.add(content_confirm)?;
let res = SwipeFlow::new(ConfirmResetDevice::Intro, store)?;
Ok(LayoutObj::new(res)?.into())

@ -5,7 +5,7 @@ use crate::{
ui::{
component::{
text::paragraphs::{Paragraph, Paragraphs},
SwipeDirection,
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
@ -88,7 +88,10 @@ impl CreateBackup {
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);
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| {
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
});
let content_menu = Frame::left_aligned(
"".into(),
@ -97,7 +100,12 @@ impl CreateBackup {
theme::ICON_CANCEL
)]))),
)
.with_cancel_button();
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
FrameMsg::Button(_) => None,
});
let par_array_skip_intro: [Paragraph<'static>; 2] = [
Paragraph::new(&theme::TEXT_WARNING, TString::from_str("Not recommended!")),
@ -115,32 +123,28 @@ impl CreateBackup {
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::words__continue_anyway.into()),
);
)
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
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);
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => 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,
})?;
.add(content_intro)?
.add(content_menu)?
.add(content_skip_intro)?
.add(content_skip_confirm)?;
let res = SwipeFlow::new(CreateBackup::Intro, store)?;
Ok(LayoutObj::new(res)?.into())
}

@ -4,7 +4,7 @@ use crate::{
component::{
image::BlendedImage,
text::paragraphs::{Paragraph, Paragraphs},
Qr, SwipeDirection, Timeout,
ButtonRequestExt, ComponentExt, Qr, SwipeDirection, Timeout,
},
flow::{
base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeFlow,
@ -115,8 +115,8 @@ impl GetAddress {
))),
)
.with_subtitle("address".into())
.with_menu_button(),
|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info),
.with_menu_button()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)),
)?
.add(
Frame::left_aligned(
@ -127,13 +127,13 @@ impl GetAddress {
("Cancel trans.", theme::ICON_CANCEL),
]))),
)
.with_cancel_button(),
|msg| match msg {
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => {
Some(FlowMsg::Choice(i))
}
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
},
}),
)?
.add(
Frame::left_aligned(
@ -143,8 +143,8 @@ impl GetAddress {
true,
)?),
)
.with_cancel_button(),
|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled),
.with_cancel_button()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)),
)?
.add(
Frame::left_aligned(
@ -154,8 +154,8 @@ impl GetAddress {
StrBuffer::from("taproot xp"),
))),
)
.with_cancel_button(),
|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled),
.with_cancel_button()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)),
)?
.add(
Frame::left_aligned(
@ -165,8 +165,8 @@ impl GetAddress {
StrBuffer::from("O rly?"),
))),
)
.with_cancel_button(),
|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled),
.with_cancel_button()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)),
)?
.add(
IconDialog::new(
@ -179,8 +179,8 @@ impl GetAddress {
),
StrBuffer::from("Confirmed"),
Timeout::new(100),
),
|_| Some(FlowMsg::Confirmed),
)
.map(|_| Some(FlowMsg::Confirmed)),
)?;
let res = SwipeFlow::new(GetAddress::Address, store)?;
Ok(LayoutObj::new(res)?.into())

@ -388,22 +388,22 @@ pub const fn button_reset() -> ButtonStyleSheet {
pub const fn button_pin() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_DARK,
font: Font::NORMAL,
text_color: GREY_LIGHT,
button_color: GREY_EXTRA_DARK,
icon_color: GREY_LIGHT,
background_color: BG,
},
active: &ButtonStyle {
font: Font::MONO,
text_color: FG,
button_color: GREY_MEDIUM,
font: Font::NORMAL,
text_color: GREY_LIGHT,
button_color: GREY_EXTRA_DARK,
icon_color: GREY_LIGHT,
background_color: BG,
},
disabled: &ButtonStyle {
font: Font::MONO,
text_color: GREY_LIGHT,
font: Font::NORMAL,
text_color: GREY_DARK,
button_color: BG, // so there is no "button" itself, just the text
icon_color: GREY_LIGHT,
background_color: BG,
@ -639,9 +639,9 @@ pub const TEXT_CHECKLIST_DONE: TextStyle =
TextStyle::new(Font::NORMAL, GREEN_DARK, BG, GREY_LIGHT, GREY_LIGHT);
pub const CONTENT_BORDER: i16 = 0;
pub const BUTTON_HEIGHT: i16 = 50;
pub const BUTTON_WIDTH: i16 = 56;
pub const BUTTON_SPACING: i16 = 6;
pub const BUTTON_HEIGHT: i16 = 62;
pub const BUTTON_WIDTH: i16 = 78;
pub const BUTTON_SPACING: i16 = 2;
pub const KEYBOARD_SPACING: i16 = BUTTON_SPACING;
pub const CHECKLIST_SPACING: i16 = 10;
pub const RECOVERY_SPACING: i16 = 18;

Loading…
Cancel
Save