refactor(core/ui): rust UI flow simplification

mmilata/ui-t3t1-verticalmenu
Martin Milata 4 weeks ago
parent 33719cb7b4
commit 34f6964241

@ -1,6 +1,7 @@
use super::{Component, Event, EventCtx}; use super::{Component, Event, EventCtx};
use crate::ui::{geometry::Rect, shape::Renderer}; use crate::ui::{geometry::Rect, shape::Renderer};
#[derive(Clone)]
pub struct MsgMap<T, F> { pub struct MsgMap<T, F> {
inner: T, inner: T,
func: F, func: F,
@ -50,3 +51,17 @@ where
self.inner.trace(t) 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. /// triggered by events and swipes.
pub trait FlowState pub trait FlowState
where 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 /// There needs to be a mapping from states to indices of the FlowStore
/// array. Default implementation works for states that are enums, the /// array. Default implementation works for states that are enums, the

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

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

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

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

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

Loading…
Cancel
Save