From 90f4492cec49ea28c4cf300486d47c7cb2e52abd Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Fri, 12 Apr 2024 00:19:20 +0200 Subject: [PATCH] feat(core/ui): rust-based UI flows [no changelog] --- ci/shell.nix | 8 +- core/embed/rust/src/ui/animation.rs | 4 + core/embed/rust/src/ui/flow/base.rs | 95 ++++++++ core/embed/rust/src/ui/flow/flow.rs | 202 ++++++++++++++++ core/embed/rust/src/ui/flow/mod.rs | 10 + core/embed/rust/src/ui/flow/page.rs | 132 +++++++++++ core/embed/rust/src/ui/flow/store.rs | 219 ++++++++++++++++++ core/embed/rust/src/ui/flow/swipe.rs | 144 ++++++++++++ core/embed/rust/src/ui/geometry.rs | 6 + core/embed/rust/src/ui/mod.rs | 2 + .../src/ui/model_mercury/component/swipe.rs | 7 +- core/embed/rust/src/ui/model_mercury/mod.rs | 5 +- 12 files changed, 823 insertions(+), 11 deletions(-) create mode 100644 core/embed/rust/src/ui/flow/base.rs create mode 100644 core/embed/rust/src/ui/flow/flow.rs create mode 100644 core/embed/rust/src/ui/flow/mod.rs create mode 100644 core/embed/rust/src/ui/flow/page.rs create mode 100644 core/embed/rust/src/ui/flow/store.rs create mode 100644 core/embed/rust/src/ui/flow/swipe.rs diff --git a/ci/shell.nix b/ci/shell.nix index ba4770fa9..f06a03170 100644 --- a/ci/shell.nix +++ b/ci/shell.nix @@ -4,10 +4,10 @@ }: 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 { - url = "https://github.com/oxalica/rust-overlay/archive/db7bf4a2dd295adeeaa809d36387098926a15487.tar.gz"; - sha256 = "0gk6kag09w8lyn9was8dpjgslxw5p81bx04379m9v6ky09kw482d"; + url = "https://github.com/oxalica/rust-overlay/archive/e36f66bb10b09f5189dc3b1706948eaeb9a1c555.tar.gz"; + sha256 = "1vivsmqmqajbvv7181y7mfl48fxmm75hq2c8rj6h1l2ymq28zcpg"; }); # define this variable and devTools if you want nrf{util,connect} acceptJlink = builtins.getEnv "TREZOR_FIRMWARE_ACCEPT_JLINK_LICENSE" == "yes"; @@ -49,7 +49,7 @@ let done ''; # 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 { targets = [ "thumbv7em-none-eabihf" # TT diff --git a/core/embed/rust/src/ui/animation.rs b/core/embed/rust/src/ui/animation.rs index 746949139..66039faba 100644 --- a/core/embed/rust/src/ui/animation.rs +++ b/core/embed/rust/src/ui/animation.rs @@ -59,4 +59,8 @@ impl Animation { panic!("offset is too large"); } } + + pub fn finished(&self, now: Instant) -> bool { + self.elapsed(now) >= self.duration + } } diff --git a/core/embed/rust/src/ui/flow/base.rs b/core/embed/rust/src/ui/flow/base.rs new file mode 100644 index 000000000..a01f8f0bc --- /dev/null +++ b/core/embed/rust/src/ui/flow/base.rs @@ -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 { + /// Do nothing, continue with processing next handler. + Nothing, + + /// Initiate transition to another state, end event processing. + /// NOTE: it might make sense to include Option here + Goto(Q, SwipeDirection), + + /// Yield a message to the caller of the flow (i.e. micropython), end event + /// processing. + Return(FlowMsg), +} + +impl Decision { + 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; + + /// What to do when the current component emits a message in response to an + /// event. + fn handle_event(&self, msg: FlowMsg) -> Decision; +} diff --git a/core/embed/rust/src/ui/flow/flow.rs b/core/embed/rust/src/ui/flow/flow.rs new file mode 100644 index 000000000..2bb622786 --- /dev/null +++ b/core/embed/rust/src/ui/flow/flow.rs @@ -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 { + /// Current state. + state: Q, + /// FlowStore with all screens/components. + store: S, + /// `Some` when state transition animation is in progress. + transition: Option>, + /// Swipe detector. + swipe: Swipe, + /// Animation parameter. + anim_offset: Offset, +} + +struct Transition { + prev_state: Q, + animation: Animation, + direction: SwipeDirection, +} + +impl SwipeFlow { + pub fn new(init: Q, store: S) -> Result { + 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, 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 { + 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 { + let msg = self.store.event(self.state.index(), ctx, event); + if let Some(msg) = msg { + self.state.handle_event(msg) + } else { + Decision::Nothing + } + } +} + +impl Component for SwipeFlow { + 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 { + // 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 crate::trace::Trace for SwipeFlow { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.store.trace(self.state.index(), t) + } +} + +#[cfg(feature = "micropython")] +impl crate::ui::layout::obj::ComponentMsgObj for SwipeFlow { + fn msg_try_into_obj( + &self, + msg: Self::Msg, + ) -> Result { + 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()?) + } + } + } +} diff --git a/core/embed/rust/src/ui/flow/mod.rs b/core/embed/rust/src/ui/flow/mod.rs new file mode 100644 index 000000000..6f3d1f45a --- /dev/null +++ b/core/embed/rust/src/ui/flow/mod.rs @@ -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}; diff --git a/core/embed/rust/src/ui/flow/page.rs b/core/embed/rust/src/ui/flow/page.rs new file mode 100644 index 000000000..8d5ac3598 --- /dev/null +++ b/core/embed/rust/src/ui/flow/page.rs @@ -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 { + inner: T, + axis: Axis, + pages: usize, + current: usize, +} + +impl SwipePage { + pub fn vertical(inner: T) -> Self { + Self { + inner, + axis: Axis::Vertical, + pages: 1, + current: 0, + } + } +} + +impl Component for SwipePage { + 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 { + 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 Swipable for SwipePage { + 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 crate::trace::Trace for SwipePage +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); + +impl IgnoreSwipe { + pub fn new(inner: T) -> Self { + IgnoreSwipe(inner) + } +} + +impl Component for IgnoreSwipe { + 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.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 Swipable for IgnoreSwipe { + fn can_swipe(&self, _direction: SwipeDirection) -> bool { + false + } + fn swiped(&mut self, _ctx: &mut EventCtx, _direction: SwipeDirection) {} +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for IgnoreSwipe +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.0.trace(t) + } +} diff --git a/core/embed/rust/src/ui/flow/store.rs b/core/embed/rust/src/ui/flow/store.rs new file mode 100644 index 000000000..993170154 --- /dev/null +++ b/core/embed/rust/src/ui/flow/store.rs @@ -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>` 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; + + /// 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(&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) -> 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( + self, + elem: E, + func: fn(E::Msg) -> Option, + ) -> Result + 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 { + 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(&mut self, _i: usize, _func: impl FnOnce(&mut dyn Swipable) -> T) -> T { + panic!() + } + + fn clone(&mut self, _i: Option) -> Result<(), error::Error> { + Ok(()) + } + fn render_cloned<'s>(&'s self, _target: &mut impl Renderer<'s>) {} + + fn add( + self, + elem: E, + func: fn(E::Msg) -> Option, + ) -> Result + where + Self: Sized, + { + Ok(FlowComponent { + elem: Gc::new(elem)?, + func, + cloned: None, + next: Self, + }) + } +} + +struct FlowComponent { + /// Component allocated on micropython heap. + pub elem: Gc, + + /// Clone. + pub cloned: Option>, + + /// Function to convert message to `FlowMsg`. + pub func: fn(E::Msg) -> Option, + + /// Nested FlowStore. + pub next: P, +} + +impl FlowComponent { + 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 FlowStore for FlowComponent +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 { + 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(&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) -> 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( + self, + elem: F, + func: fn(F::Msg) -> Option, + ) -> Result + where + Self: Sized, + { + Ok(FlowComponent { + elem: self.elem, + func: self.func, + cloned: None, + next: self.next.add(elem, func)?, + }) + } +} diff --git a/core/embed/rust/src/ui/flow/swipe.rs b/core/embed/rust/src/ui/flow/swipe.rs new file mode 100644 index 000000000..1d4d6989c --- /dev/null +++ b/core/embed/rust/src/ui/flow/swipe.rs @@ -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, +} + +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 { + 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>) {} +} diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index ec2be3ba3..bca2e523c 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -134,6 +134,12 @@ impl From 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)] diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index b08f77789..22501c6e9 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -6,6 +6,8 @@ pub mod component; pub mod constant; pub mod display; pub mod event; +#[cfg(all(feature = "micropython", feature = "model_mercury"))] +pub mod flow; pub mod geometry; pub mod lerp; pub mod shape; diff --git a/core/embed/rust/src/ui/model_mercury/component/swipe.rs b/core/embed/rust/src/ui/model_mercury/component/swipe.rs index b4cab1d2e..b4244e38c 100644 --- a/core/embed/rust/src/ui/model_mercury/component/swipe.rs +++ b/core/embed/rust/src/ui/model_mercury/component/swipe.rs @@ -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, diff --git a/core/embed/rust/src/ui/model_mercury/mod.rs b/core/embed/rust/src/ui/model_mercury/mod.rs index 78d03001e..bb3c85347 100644 --- a/core/embed/rust/src/ui/model_mercury/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/mod.rs @@ -6,7 +6,10 @@ pub mod component; pub mod constant; pub mod theme; -mod screens; +pub mod flow; +#[cfg(feature = "micropython")] +pub mod layout; +pub mod screens; pub struct ModelMercuryFeatures;