mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-18 12:28:09 +00:00
feat(core/ui): rust-based UI flows
[no changelog]
This commit is contained in:
parent
ac39b026cf
commit
10234787a4
@ -4,10 +4,10 @@
|
|||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
# the last commit from master as of 2023-04-14
|
# the last commit from master as of 2024-01-22
|
||||||
rustOverlay = import (builtins.fetchTarball {
|
rustOverlay = import (builtins.fetchTarball {
|
||||||
url = "https://github.com/oxalica/rust-overlay/archive/db7bf4a2dd295adeeaa809d36387098926a15487.tar.gz";
|
url = "https://github.com/oxalica/rust-overlay/archive/e36f66bb10b09f5189dc3b1706948eaeb9a1c555.tar.gz";
|
||||||
sha256 = "0gk6kag09w8lyn9was8dpjgslxw5p81bx04379m9v6ky09kw482d";
|
sha256 = "1vivsmqmqajbvv7181y7mfl48fxmm75hq2c8rj6h1l2ymq28zcpg";
|
||||||
});
|
});
|
||||||
# define this variable and devTools if you want nrf{util,connect}
|
# define this variable and devTools if you want nrf{util,connect}
|
||||||
acceptJlink = builtins.getEnv "TREZOR_FIRMWARE_ACCEPT_JLINK_LICENSE" == "yes";
|
acceptJlink = builtins.getEnv "TREZOR_FIRMWARE_ACCEPT_JLINK_LICENSE" == "yes";
|
||||||
@ -60,7 +60,7 @@ let
|
|||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
# NOTE: don't forget to update Minimum Supported Rust Version in docs/core/build/emulator.md
|
# NOTE: don't forget to update Minimum Supported Rust Version in docs/core/build/emulator.md
|
||||||
rustProfiles = nixpkgs.rust-bin.nightly."2023-04-14";
|
rustProfiles = nixpkgs.rust-bin.nightly."2024-01-21";
|
||||||
rustNightly = rustProfiles.minimal.override {
|
rustNightly = rustProfiles.minimal.override {
|
||||||
targets = [
|
targets = [
|
||||||
"thumbv7em-none-eabihf" # TT
|
"thumbv7em-none-eabihf" # TT
|
||||||
|
@ -59,4 +59,8 @@ impl<T> Animation<T> {
|
|||||||
panic!("offset is too large");
|
panic!("offset is too large");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn finished(&self, now: Instant) -> bool {
|
||||||
|
self.elapsed(now) >= self.duration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
95
core/embed/rust/src/ui/flow/base.rs
Normal file
95
core/embed/rust/src/ui/flow/base.rs
Normal file
@ -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>;
|
||||||
|
}
|
202
core/embed/rust/src/ui/flow/flow.rs
Normal file
202
core/embed/rust/src/ui/flow/flow.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
/// Given a state enum and a corresponding FlowStore, create a Component that
|
||||||
|
/// implements a swipe navigation between the states with animated transitions.
|
||||||
|
///
|
||||||
|
/// If a swipe is detected:
|
||||||
|
/// - currently active component is asked to handle the event,
|
||||||
|
/// - if it can't then FlowState::handle_swipe is consulted.
|
||||||
|
pub struct SwipeFlow<Q, S> {
|
||||||
|
/// Current state.
|
||||||
|
state: Q,
|
||||||
|
/// FlowStore with all screens/components.
|
||||||
|
store: S,
|
||||||
|
/// `Some` when state transition animation is in progress.
|
||||||
|
transition: Option<Transition<Q>>,
|
||||||
|
/// Swipe detector.
|
||||||
|
swipe: Swipe,
|
||||||
|
/// Animation parameter.
|
||||||
|
anim_offset: Offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Transition<Q> {
|
||||||
|
prev_state: Q,
|
||||||
|
animation: Animation<Offset>,
|
||||||
|
direction: SwipeDirection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Q: FlowState, S: FlowStore> SwipeFlow<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 SwipeFlow<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 the 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 SwipeFlow<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 SwipeFlow<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()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
core/embed/rust/src/ui/flow/mod.rs
Normal file
10
core/embed/rust/src/ui/flow/mod.rs
Normal file
@ -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::SwipeFlow;
|
||||||
|
pub use page::{IgnoreSwipe, SwipePage};
|
||||||
|
pub use store::{flow_store, FlowStore};
|
132
core/embed/rust/src/ui/flow/page.rs
Normal file
132
core/embed/rust/src/ui/flow/page.rs
Normal file
@ -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);
|
||||||
|
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.
|
||||||
|
#[derive(Clone)]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
219
core/embed/rust/src/ui/flow/store.rs
Normal file
219
core/embed/rust/src/ui/flow/store.rs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
use crate::{
|
||||||
|
error,
|
||||||
|
maybe_trace::MaybeTrace,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx},
|
||||||
|
flow::base::{FlowMsg, Swipable},
|
||||||
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::micropython::gc::Gc;
|
||||||
|
|
||||||
|
/// `FlowStore` is essentially `Vec<Gc<dyn Component + Swipable>>` except that
|
||||||
|
/// `trait Component` is not object-safe so it ends up being a kind of
|
||||||
|
/// recursively-defined tuple.
|
||||||
|
///
|
||||||
|
/// Additionally the store makes it possible to make a clone of one of its
|
||||||
|
/// items, in order to make it possible to render transition animations.
|
||||||
|
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.
|
||||||
|
fn event(&mut self, i: usize, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg>;
|
||||||
|
|
||||||
|
/// Call `Component::render` on i-th element.
|
||||||
|
fn render<'s>(&'s self, i: usize, target: &mut impl Renderer<'s>);
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
/// Call `Trace::trace` on i-th element.
|
||||||
|
fn trace(&self, i: usize, t: &mut dyn crate::trace::Tracer);
|
||||||
|
|
||||||
|
/// Forward `Swipable` methods to i-th element.
|
||||||
|
fn map_swipable<T>(&mut self, i: usize, func: impl FnOnce(&mut dyn Swipable) -> T) -> T;
|
||||||
|
|
||||||
|
/// Make a clone of i-th element, or free all clones if None is given.
|
||||||
|
fn clone(&mut self, i: Option<usize>) -> Result<(), error::Error>;
|
||||||
|
|
||||||
|
/// Call `Component::render` on the cloned element.
|
||||||
|
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>(
|
||||||
|
self,
|
||||||
|
elem: E,
|
||||||
|
func: fn(E::Msg) -> Option<FlowMsg>,
|
||||||
|
) -> Result<impl FlowStore, error::Error>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new empty flow store.
|
||||||
|
pub fn flow_store() -> impl FlowStore {
|
||||||
|
FlowEmpty {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminating element of a recursive structure.
|
||||||
|
struct FlowEmpty;
|
||||||
|
|
||||||
|
// Methods that take an index panic because it's always out of bounds.
|
||||||
|
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!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlowComponent<E: Component, 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> {
|
||||||
|
fn as_ref(&self) -> &E {
|
||||||
|
&self.elem
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_mut(&mut self) -> &mut E {
|
||||||
|
// SAFETY: micropython can only access this object through LayoutObj which wraps
|
||||||
|
// us in a RefCell which guarantees uniqueness
|
||||||
|
unsafe { Gc::as_mut(&mut self.elem) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
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)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
144
core/embed/rust/src/ui/flow/swipe.rs
Normal file
144
core/embed/rust/src/ui/flow/swipe.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
component::{Component, Event, EventCtx},
|
||||||
|
event::TouchEvent,
|
||||||
|
flow::base::SwipeDirection,
|
||||||
|
geometry::{Point, Rect},
|
||||||
|
shape::Renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Copy of `model_tt/component/swipe.rs` but without the backlight handling.
|
||||||
|
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.2;
|
||||||
|
|
||||||
|
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
|
/// A point in 2D space defined by the the `x` and `y` coordinate. Relative
|
||||||
/// coordinates, vectors, and offsets are represented by the `Offset` type.
|
/// coordinates, vectors, and offsets are represented by the `Offset` type.
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
@ -7,6 +7,8 @@ pub mod component;
|
|||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
#[cfg(all(feature = "micropython", feature = "model_mercury"))]
|
||||||
|
pub mod flow;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
pub mod lerp;
|
pub mod lerp;
|
||||||
pub mod shape;
|
pub mod shape;
|
||||||
|
@ -8,12 +8,7 @@ use crate::ui::{
|
|||||||
|
|
||||||
use super::theme;
|
use super::theme;
|
||||||
|
|
||||||
pub enum SwipeDirection {
|
pub use crate::ui::flow::SwipeDirection;
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Swipe {
|
pub struct Swipe {
|
||||||
pub area: Rect,
|
pub area: Rect,
|
||||||
|
@ -6,7 +6,10 @@ pub mod component;
|
|||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
||||||
mod screens;
|
pub mod flow;
|
||||||
|
#[cfg(feature = "micropython")]
|
||||||
|
pub mod layout;
|
||||||
|
pub mod screens;
|
||||||
|
|
||||||
pub struct ModelMercuryFeatures;
|
pub struct ModelMercuryFeatures;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user