feat(core/ui): rust-based UI flows

Martin Milata 1 month ago
parent c6748d6b75
commit 0e3586c524

@ -203,6 +203,9 @@ static void _librust_qstrs(void) {
MP_QSTR_fingerprint;
MP_QSTR_firmware_update__title;
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_flow;
MP_QSTR_flow_get_address;
MP_QSTR_get_address;
MP_QSTR_get_language;
MP_QSTR_hold;
MP_QSTR_hold_danger;

@ -59,4 +59,8 @@ impl<T> Animation<T> {
panic!("offset is too large");
}
}
pub fn finished(&self, now: Instant) -> bool {
self.elapsed(now) >= self.duration
}
}

@ -73,6 +73,7 @@ pub trait Component {
/// 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
/// `EventCtx::request_paint` in its `event` pass.
#[derive(Clone)]
pub struct Child<T> {
component: T,
marked_for_paint: bool,

@ -72,6 +72,7 @@ impl crate::trace::Trace for Image {
}
}
#[derive(Clone)]
pub struct BlendedImage {
bg: Icon,
fg: Icon,

@ -7,6 +7,7 @@ use crate::ui::{
use super::{text::TextStyle, TextLayout};
#[derive(Clone)]
pub struct Label<T> {
text: T,
layout: TextLayout,

@ -25,6 +25,7 @@ const CORNER_RADIUS: u8 = 4;
const DARK: Color = Color::rgb(0, 0, 0);
const LIGHT: Color = Color::rgb(0xff, 0xff, 0xff);
#[derive(Clone)]
pub struct Qr {
text: String<MAX_DATA>,
border: i16,
@ -189,3 +190,5 @@ impl crate::trace::Trace for Qr {
t.string("text", self.text.as_str().into());
}
}
impl crate::ui::flow::Swipable for Qr {}

@ -50,6 +50,7 @@ pub trait ParagraphSource {
}
}
#[derive(Clone)]
pub struct Paragraphs<T> {
area: Rect,
placement: LinearPlacement,
@ -346,6 +347,7 @@ impl<T> Paragraph<T> {
}
}
#[derive(Clone)]
struct TextLayoutProxy {
offset: PageOffset,
bounds: Rect,

@ -7,6 +7,7 @@ use crate::{
},
};
#[derive(Clone)]
pub struct Timeout {
time_ms: u32,
timer: Option<TimerToken>,

@ -0,0 +1,95 @@
use crate::ui::{component::EventCtx, geometry::Offset};
use num_traits::ToPrimitive;
#[derive(Copy, Clone)]
pub enum SwipeDirection {
Up,
Down,
Left,
Right,
}
impl SwipeDirection {
pub fn as_offset(self, size: Offset) -> Offset {
match self {
SwipeDirection::Up => Offset::y(-size.y),
SwipeDirection::Down => Offset::y(size.y),
SwipeDirection::Left => Offset::x(-size.x),
SwipeDirection::Right => Offset::x(size.x),
}
}
}
/// Component must implement this trait in order to be part of swipe-based flow.
/// The process of receiving a swipe is two-step, because in order to render the
/// transition animation Flow makes a copy of the pre-swipe state of the
/// component to render it along with the post-swipe state.
pub trait Swipable {
/// Return true if component can handle swipe in a given direction.
fn can_swipe(&self, _direction: SwipeDirection) -> bool {
false
}
/// Make component react to swipe event. Only called if component returned
/// true in the previous function.
fn swiped(&mut self, _ctx: &mut EventCtx, _direction: SwipeDirection) {}
}
/// Component::Msg for component parts of a flow. Converting results of
/// different screens to a shared type makes things easier to work with.
///
/// Also currently the type for message emitted by Flow::event to
/// micropython. They don't need to be the same.
#[derive(Copy, Clone)]
pub enum FlowMsg {
Confirmed,
Cancelled,
Info,
Choice(usize),
}
/// Composable event handler result.
#[derive(Copy, Clone)]
pub enum Decision<Q> {
/// Do nothing, continue with processing next handler.
Nothing,
/// Initiate transition to another state, end event processing.
/// NOTE: it might make sense to include Option<ButtonRequest> here
Goto(Q, SwipeDirection),
/// Yield a message to the caller of the flow (i.e. micropython), end event
/// processing.
Return(FlowMsg),
}
impl<Q> Decision<Q> {
pub fn or_else(self, func: impl FnOnce() -> Self) -> Self {
match self {
Decision::Nothing => func(),
_ => self,
}
}
}
/// Encodes the flow logic as a set of states, and transitions between them
/// triggered by events and swipes.
pub trait FlowState
where
Self: Sized + Copy + PartialEq + 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
/// FlowStore has to have number of elements equal to number of states.
fn index(&self) -> usize {
unwrap!(self.to_usize())
}
/// What to do when user swipes on screen and current component doesn't
/// respond to swipe of that direction.
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self>;
/// What to do when the current component emits a message in response to an
/// event.
fn handle_event(&self, msg: FlowMsg) -> Decision<Self>;
}

@ -0,0 +1,191 @@
use crate::{
error,
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx},
flow::{base::Decision, swipe::Swipe, FlowMsg, FlowState, FlowStore, SwipeDirection},
geometry::{Offset, Rect},
shape::Renderer,
},
};
const ANIMATION_DURATION: Duration = Duration::from_millis(333);
struct Transition<Q> {
prev_state: Q,
animation: Animation<Offset>,
direction: SwipeDirection,
}
pub struct Flow<Q, S> {
state: Q,
store: S,
transition: Option<Transition<Q>>,
swipe: Swipe,
anim_offset: Offset,
}
impl<Q: FlowState, S: FlowStore> Flow<Q, S> {
pub fn new(init: Q, store: S) -> Result<Self, error::Error> {
Ok(Self {
state: init,
store,
transition: None,
swipe: Swipe::new().down().up().left().right(),
anim_offset: Offset::zero(),
})
}
fn goto(&mut self, ctx: &mut EventCtx, direction: SwipeDirection, state: Q) {
self.transition = Some(Transition {
prev_state: self.state,
animation: Animation::new(
Offset::zero(),
direction.as_offset(self.anim_offset),
ANIMATION_DURATION,
Instant::now(),
),
direction,
});
self.state = state;
ctx.request_anim_frame();
ctx.request_paint()
}
fn render_state<'s>(&'s self, state: Q, target: &mut impl Renderer<'s>) {
self.store.render(state.index(), target)
}
fn render_transition<'s>(&'s self, transition: &Transition<Q>, target: &mut impl Renderer<'s>) {
let off = transition.animation.value(Instant::now());
if self.state == transition.prev_state {
target.with_origin(off, &|target| {
self.store.render_cloned(target);
});
} else {
target.with_origin(off, &|target| {
self.render_state(transition.prev_state, target);
});
}
target.with_origin(
off - transition.direction.as_offset(self.anim_offset),
&|target| {
self.render_state(self.state, target);
},
);
}
fn handle_transition(&mut self, ctx: &mut EventCtx) {
if let Some(transition) = &self.transition {
if transition.animation.finished(Instant::now()) {
self.transition = None;
unwrap!(self.store.clone(None)); // Free the clone.
let msg = self.store.event(self.state.index(), ctx, Event::Attach);
assert!(msg.is_none())
} else {
ctx.request_anim_frame();
}
ctx.request_paint();
}
}
fn handle_swipe_child(&mut self, ctx: &mut EventCtx, direction: SwipeDirection) -> Decision<Q> {
let i = self.state.index();
if self.store.map_swipable(i, |s| s.can_swipe(direction)) {
// Before handling the swipe we make a copy of the original state so that we
// can render both states in the transition animation.
unwrap!(self.store.clone(Some(i)));
self.store.map_swipable(i, |s| s.swiped(ctx, direction));
Decision::Goto(self.state, direction)
} else {
Decision::Nothing
}
}
fn handle_event_child(&mut self, ctx: &mut EventCtx, event: Event) -> Decision<Q> {
let msg = self.store.event(self.state.index(), ctx, event);
if let Some(msg) = msg {
self.state.handle_event(msg)
} else {
Decision::Nothing
}
}
}
impl<Q: FlowState, S: FlowStore> Component for Flow<Q, S> {
type Msg = FlowMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// Save screen size for slide animation. Once we have reasonable constants trait
// this can be set in constructor.
self.anim_offset = bounds.size();
self.swipe.place(bounds);
self.store.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// 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);
}
// Ignore events while transition is running.
if self.transition.is_some() {
return None;
}
let mut decision = Decision::Nothing;
if let Some(direction) = self.swipe.event(ctx, event) {
decision = self
.handle_swipe_child(ctx, direction)
.or_else(|| self.state.handle_swipe(direction));
}
decision = decision.or_else(|| self.handle_event_child(ctx, event));
match decision {
Decision::Nothing => None,
Decision::Goto(next_state, direction) => {
self.goto(ctx, direction, next_state);
None
}
Decision::Return(msg) => Some(msg),
}
}
fn paint(&mut self) {}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(transition) = &self.transition {
self.render_transition(transition, target)
} else {
self.render_state(self.state, target)
}
}
}
#[cfg(feature = "ui_debug")]
impl<Q: FlowState, S: FlowStore> crate::trace::Trace for Flow<Q, S> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.store.trace(self.state.index(), t)
}
}
#[cfg(feature = "micropython")]
impl<Q: FlowState, S: FlowStore> crate::ui::layout::obj::ComponentMsgObj for Flow<Q, S> {
fn msg_try_into_obj(
&self,
msg: Self::Msg,
) -> Result<crate::micropython::obj::Obj, error::Error> {
match msg {
FlowMsg::Confirmed => Ok(crate::ui::layout::result::CONFIRMED.as_obj()),
FlowMsg::Cancelled => Ok(crate::ui::layout::result::CANCELLED.as_obj()),
FlowMsg::Info => Ok(crate::ui::layout::result::INFO.as_obj()),
FlowMsg::Choice(i) => {
Ok((crate::ui::layout::result::CONFIRMED.as_obj(), i.try_into()?).try_into()?)
}
}
}
}

@ -0,0 +1,10 @@
pub mod base;
mod flow;
pub mod page;
mod store;
mod swipe;
pub use base::{FlowMsg, FlowState, Swipable, SwipeDirection};
pub use flow::Flow;
pub use page::{IgnoreSwipe, SwipePage};
pub use store::{flow_store, FlowStore};

@ -0,0 +1,132 @@
use crate::ui::{
component::{Component, Event, EventCtx, Paginate},
flow::base::{Swipable, SwipeDirection},
geometry::{Axis, Rect},
shape::Renderer,
};
/// Allows any implementor of `Paginate` to be part of `Swipable` UI flow.
#[derive(Clone)]
pub struct SwipePage<T> {
inner: T,
axis: Axis,
pages: usize,
current: usize,
}
impl<T> SwipePage<T> {
pub fn vertical(inner: T) -> Self {
Self {
inner,
axis: Axis::Vertical,
pages: 1,
current: 0,
}
}
}
impl<T: Component + Paginate> Component for SwipePage<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
let result = self.inner.place(bounds);
self.pages = self.inner.page_count();
result
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.inner.event(ctx, event);
//self.pages = self.inner.page_count();
msg
}
fn paint(&mut self) {
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target)
}
}
impl<T: Component + Paginate> Swipable for SwipePage<T> {
fn can_swipe(&self, direction: SwipeDirection) -> bool {
match (self.axis, direction) {
(Axis::Horizontal, SwipeDirection::Up | SwipeDirection::Down) => false,
(Axis::Vertical, SwipeDirection::Left | SwipeDirection::Right) => false,
(_, SwipeDirection::Left | SwipeDirection::Up) => self.current + 1 < self.pages,
(_, SwipeDirection::Right | SwipeDirection::Down) => self.current > 0,
}
}
fn swiped(&mut self, ctx: &mut EventCtx, direction: SwipeDirection) {
match (self.axis, direction) {
(Axis::Horizontal, SwipeDirection::Up | SwipeDirection::Down) => return,
(Axis::Vertical, SwipeDirection::Left | SwipeDirection::Right) => return,
(_, SwipeDirection::Left | SwipeDirection::Up) => {
self.current = (self.current + 1).min(self.pages - 1);
}
(_, SwipeDirection::Right | SwipeDirection::Down) => {
self.current = self.current.saturating_sub(1);
}
}
self.inner.change_page(self.current);
ctx.request_paint();
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SwipePage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.inner.trace(t)
}
}
/// Make any component swipable by ignoring all swipe events.
pub struct IgnoreSwipe<T>(T);
impl<T> IgnoreSwipe<T> {
pub fn new(inner: T) -> Self {
IgnoreSwipe(inner)
}
}
impl<T: Component> Component for IgnoreSwipe<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
self.0.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.0.event(ctx, event)
}
fn paint(&mut self) {
self.0.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.0.render(target)
}
}
impl<T> Swipable for IgnoreSwipe<T> {
fn can_swipe(&self, _direction: SwipeDirection) -> bool {
false
}
fn swiped(&mut self, _ctx: &mut EventCtx, _direction: SwipeDirection) {}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for IgnoreSwipe<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.0.trace(t)
}
}

@ -0,0 +1,184 @@
use crate::{
error,
maybe_trace::MaybeTrace,
ui::{
component::{Component, Event, EventCtx},
flow::base::{FlowMsg, Swipable},
geometry::Rect,
shape::Renderer,
},
};
use crate::micropython::gc::Gc;
struct FlowEmpty;
pub fn flow_store() -> impl FlowStore {
FlowEmpty {}
}
pub struct FlowComponent<T: Component, P> {
pub elem: Gc<T>,
pub func: fn(T::Msg) -> Option<FlowMsg>,
pub cloned: Option<Gc<T>>,
pub next: P,
}
impl<E: Component, P> FlowComponent<E, P> {
fn as_ref(&self) -> &E {
&self.elem
}
fn as_mut(&mut self) -> &mut E {
unsafe { Gc::as_mut(&mut self.elem) }
}
}
pub trait FlowStore {
fn place(&mut self, bounds: Rect) -> Rect;
fn event(&mut self, i: usize, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg>;
fn render<'s>(&'s self, i: usize, target: &mut impl Renderer<'s>);
fn trace(&self, i: usize, t: &mut dyn crate::trace::Tracer);
fn map_swipable<T>(&mut self, i: usize, func: impl FnOnce(&mut dyn Swipable) -> T) -> T;
fn clone(&mut self, i: Option<usize>) -> Result<(), error::Error>;
fn render_cloned<'s>(&'s self, target: &mut impl Renderer<'s>);
fn add<E: Component + MaybeTrace + Swipable + Clone>(
self,
elem: E,
func: fn(E::Msg) -> Option<FlowMsg>,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized;
}
impl FlowStore for FlowEmpty {
fn place(&mut self, bounds: Rect) -> Rect {
bounds
}
fn event(&mut self, _i: usize, _ctx: &mut EventCtx, _event: Event) -> Option<FlowMsg> {
panic!()
}
fn render<'s>(&'s self, _i: usize, _target: &mut impl Renderer<'s>) {
panic!()
}
fn trace(&self, _i: usize, _t: &mut dyn crate::trace::Tracer) {
panic!()
}
fn map_swipable<T>(&mut self, _i: usize, _func: impl FnOnce(&mut dyn Swipable) -> T) -> T {
panic!()
}
fn clone(&mut self, _i: Option<usize>) -> Result<(), error::Error> {
Ok(())
}
fn render_cloned<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
fn add<E: Component + 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,
})
}
}
impl<E, P> FlowStore for FlowComponent<E, P>
where
E: Component + MaybeTrace + Swipable + Clone,
P: FlowStore,
{
fn place(&mut self, bounds: Rect) -> Rect {
self.as_mut().place(bounds);
self.next.place(bounds);
bounds
}
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)
} else {
self.next.event(i - 1, ctx, event)
}
}
fn render<'s>(&'s self, i: usize, target: &mut impl Renderer<'s>) {
if i == 0 {
self.as_ref().render(target)
} else {
self.next.render(i - 1, target)
}
}
fn trace(&self, i: usize, t: &mut dyn crate::trace::Tracer) {
if i == 0 {
self.as_ref().trace(t)
} else {
self.next.trace(i - 1, t)
}
}
fn map_swipable<T>(&mut self, i: usize, func: impl FnOnce(&mut dyn Swipable) -> T) -> T {
if i == 0 {
func(self.as_mut())
} else {
self.next.map_swipable(i - 1, func)
}
}
fn clone(&mut self, i: Option<usize>) -> Result<(), error::Error> {
match i {
None => {
// FIXME: how to ensure the allocation is returned?
self.cloned = None;
self.next.clone(None)?
}
Some(0) => {
self.cloned = Some(Gc::new(self.as_ref().clone())?);
self.next.clone(None)?
}
Some(i) => {
self.cloned = None;
self.next.clone(Some(i - 1))?
}
}
Ok(())
}
fn render_cloned<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(cloned) = &self.cloned {
cloned.render(target)
}
self.next.render_cloned(target);
}
fn add<F: Component + 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)?,
})
}
}

@ -0,0 +1,143 @@
use crate::ui::{
component::{Component, Event, EventCtx},
event::TouchEvent,
flow::base::SwipeDirection,
geometry::{Point, Rect},
shape::Renderer,
};
pub struct Swipe {
pub area: Rect,
pub allow_up: bool,
pub allow_down: bool,
pub allow_left: bool,
pub allow_right: bool,
origin: Option<Point>,
}
impl Swipe {
const DISTANCE: i32 = 120;
const THRESHOLD: f32 = 0.3;
pub fn new() -> Self {
Self {
area: Rect::zero(),
allow_up: false,
allow_down: false,
allow_left: false,
allow_right: false,
origin: None,
}
}
pub fn vertical() -> Self {
Self::new().up().down()
}
pub fn horizontal() -> Self {
Self::new().left().right()
}
pub fn up(mut self) -> Self {
self.allow_up = true;
self
}
pub fn down(mut self) -> Self {
self.allow_down = true;
self
}
pub fn left(mut self) -> Self {
self.allow_left = true;
self
}
pub fn right(mut self) -> Self {
self.allow_right = true;
self
}
fn is_active(&self) -> bool {
self.allow_up || self.allow_down || self.allow_left || self.allow_right
}
fn ratio(&self, dist: i16) -> f32 {
(dist as f32 / Self::DISTANCE as f32).min(1.0)
}
}
impl Component for Swipe {
type Msg = SwipeDirection;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if !self.is_active() {
return None;
}
match (event, self.origin) {
(Event::Touch(TouchEvent::TouchStart(pos)), _) if self.area.contains(pos) => {
// Mark the starting position of this touch.
self.origin.replace(pos);
}
(Event::Touch(TouchEvent::TouchMove(pos)), Some(origin)) => {
// Consider our allowed directions and the touch distance and modify the display
// backlight accordingly.
let ofs = pos - origin;
let abs = ofs.abs();
if abs.x > abs.y && (self.allow_left || self.allow_right) {
// Horizontal direction.
if (ofs.x < 0 && self.allow_left) || (ofs.x > 0 && self.allow_right) {
// self.backlight(self.ratio(abs.x));
}
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
// Vertical direction.
if (ofs.y < 0 && self.allow_up) || (ofs.y > 0 && self.allow_down) {
// self.backlight(self.ratio(abs.y));
}
};
}
(Event::Touch(TouchEvent::TouchEnd(pos)), Some(origin)) => {
// Touch interaction is over, reset the position.
self.origin.take();
// Compare the touch distance with our allowed directions and determine if it
// constitutes a valid swipe.
let ofs = pos - origin;
let abs = ofs.abs();
if abs.x > abs.y && (self.allow_left || self.allow_right) {
// Horizontal direction.
if self.ratio(abs.x) >= Self::THRESHOLD {
if ofs.x < 0 && self.allow_left {
return Some(SwipeDirection::Left);
} else if ofs.x > 0 && self.allow_right {
return Some(SwipeDirection::Right);
}
}
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
// Vertical direction.
if self.ratio(abs.y) >= Self::THRESHOLD {
if ofs.y < 0 && self.allow_up {
return Some(SwipeDirection::Up);
} else if ofs.y > 0 && self.allow_down {
return Some(SwipeDirection::Down);
}
}
};
}
_ => {
// Do nothing.
}
}
None
}
fn paint(&mut self) {}
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
}

@ -134,6 +134,12 @@ impl From<Point> for Offset {
}
}
impl Lerp for Offset {
fn lerp(a: Self, b: Self, t: f32) -> Self {
Offset::new(i16::lerp(a.x, b.x, t), i16::lerp(a.y, b.y, t))
}
}
/// A point in 2D space defined by the the `x` and `y` coordinate. Relative
/// coordinates, vectors, and offsets are represented by the `Offset` type.
#[derive(Copy, Clone, PartialEq, Eq)]

@ -7,6 +7,8 @@ pub mod component;
pub mod constant;
pub mod display;
pub mod event;
#[cfg(feature = "micropython")]
pub mod flow;
pub mod geometry;
pub mod lerp;
pub mod screens;

@ -97,7 +97,7 @@ where
let mut dummy_ctx = EventCtx::new();
self.xpub_view
.update_title(&mut dummy_ctx, self.xpubs[i].0.clone());
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.clone());
let npages = p.page_count();
p.change_page(page);

@ -23,6 +23,7 @@ pub enum ButtonMsg {
LongPressed,
}
#[derive(Clone)]
pub struct Button<T> {
area: Rect,
touch_expand: Option<Insets>,
@ -400,7 +401,7 @@ where
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone)]
enum State {
Initial,
Pressed,
@ -408,7 +409,7 @@ enum State {
Disabled,
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone)]
pub enum ButtonContent<T> {
Empty,
Text(T),
@ -533,6 +534,7 @@ impl<T> Button<T> {
}
}
#[derive(Copy, Clone)]
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
@ -555,7 +557,7 @@ pub enum SelectWordMsg {
Selected(usize),
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone)]
pub struct IconText<T> {
text: T,
icon: Icon,
@ -618,7 +620,7 @@ where
}
}
pub fn render<'s>(
& self,
&self,
target: &mut impl Renderer<'s>,
area: Rect,
style: &ButtonStyle,

@ -97,6 +97,7 @@ where
}
}
#[derive(Clone)]
pub struct IconDialog<T, U> {
image: Child<BlendedImage>,
paragraphs: Paragraphs<ParagraphVecShort<T>>,
@ -231,3 +232,5 @@ where
t.child("controls", &self.controls);
}
}
impl<T, U> crate::ui::flow::Swipable for IconDialog<T, U> {}

@ -12,6 +12,7 @@ use crate::ui::{
const TITLE_HEIGHT: i16 = 42;
const TITLE_SPACE: i16 = 2;
#[derive(Clone)]
pub struct Frame<T, U> {
border: Insets,
title: Child<Label<U>>,
@ -107,10 +108,10 @@ where
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where
F: Fn(&mut T) -> R,
F: Fn(&mut EventCtx, &mut T) -> R,
{
self.content.mutate(ctx, |ctx, c| {
let res = update_fn(c);
let res = update_fn(ctx, c);
c.request_complete_repaint(ctx);
res
})
@ -163,7 +164,7 @@ where
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target);
self.subtitle.render(target);
// self.subtitle.render(target); // FIXME crashes! try again after rebase
self.button.render(target);
self.content.render(target);
}
@ -195,3 +196,17 @@ where
}
}
}
impl<T, U> crate::ui::flow::Swipable for Frame<T, U>
where
T: Component + crate::ui::flow::Swipable,
U: AsRef<str>,
{
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))
}
}

@ -8,12 +8,7 @@ use crate::ui::{
use super::theme;
pub enum SwipeDirection {
Up,
Down,
Left,
Right,
}
pub use crate::ui::flow::SwipeDirection;
pub struct Swipe {
pub area: Rect,

@ -30,6 +30,7 @@ const MENU_SEP_HEIGHT: i16 = 2;
type VerticalMenuButtons<T> = Vec<Button<T>, N_ITEMS>;
type AreasForSeparators = Vec<Rect, N_SEPS>;
#[derive(Clone)]
pub struct VerticalMenu<T> {
area: Rect,
/// buttons placed vertically from top to bottom
@ -154,3 +155,5 @@ where
});
}
}
impl<T> crate::ui::flow::Swipable for VerticalMenu<T> {}

@ -0,0 +1,185 @@
use crate::{
error,
ui::{
component::{
image::BlendedImage,
text::paragraphs::{Paragraph, Paragraphs},
Qr, Timeout,
},
flow::{
base::Decision, flow_store, Flow, FlowMsg, FlowState, FlowStore, SwipeDirection,
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> {
// Result<Flow<GetAddress, impl FlowStore>, error::Error> {
let store = flow_store()
.add(
Frame::left_aligned(
"Receive",
SwipePage::vertical(Paragraphs::new(Paragraph::new(
&theme::TEXT_MONO,
StrBuffer::from(LONGSTRING),
))),
)
.with_subtitle("address")
.with_info_button(),
|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info),
)?
.add(
Frame::left_aligned(
"",
VerticalMenu::context_menu([
("Address QR code", theme::ICON_QR_CODE),
("Account info", theme::ICON_CHEVRON_RIGHT),
("Cancel transaction", 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",
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",
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",
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 = Flow::new(GetAddress::Address, store)?;
Ok(LayoutObj::new(res)?.into())
}
}

@ -0,0 +1,3 @@
pub mod get_address;
pub use get_address::GetAddress;

@ -55,7 +55,7 @@ use super::{
SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, VerticalMenu,
VerticalMenuChoiceMsg,
},
theme,
flow, theme,
};
impl TryFrom<CancelConfirmMsg> for Obj {
@ -2195,6 +2195,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def show_wait_text(message: str, /) -> LayoutObj[None]:
/// """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(),
/// 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)]

@ -4,6 +4,7 @@ pub mod component;
pub mod constant;
pub mod theme;
pub mod flow;
#[cfg(feature = "micropython")]
pub mod layout;
pub mod screens;

@ -70,6 +70,11 @@ def check_homescreen_format(data: bytes) -> bool:
"""Check homescreen format and dimensions."""
# rust/src/ui/model_mercury/layout.rs
def flow_get_address() -> LayoutObj[UiResult]:
"""Get address / receive funds."
# rust/src/ui/model_mercury/layout.rs
def confirm_action(
*,

@ -240,9 +240,9 @@ async def handle_EndSession(msg: EndSession) -> Success:
async def handle_Ping(msg: Ping) -> Success:
if msg.button_protection:
from trezor.enums import ButtonRequestType as B
from trezor.ui.layouts import confirm_action
from trezor.ui.layouts.mercury import flow_demo
await confirm_action("ping", TR.words__confirm, "ping", br_code=B.ProtectCall)
await flow_demo()
return Success(message=msg.message)

@ -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(
br_type: str,
title: str,

Loading…
Cancel
Save