1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

refactor(core/ui): T3T1 flow animation

[no changelog]
This commit is contained in:
Martin Milata 2024-04-30 22:27:47 +02:00
parent b05b54dfd8
commit dc2525196b
10 changed files with 215 additions and 168 deletions

View File

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

View File

@ -1,7 +1,6 @@
use super::{Component, Event, EventCtx};
use crate::ui::{geometry::Rect, shape::Renderer};
#[derive(Clone)]
pub struct MsgMap<T, F> {
inner: T,
func: F,
@ -57,11 +56,11 @@ 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 swipe_start(&mut self, ctx: &mut EventCtx, direction: super::SwipeDirection) -> bool {
self.inner.swipe_start(ctx, direction)
}
fn swiped(&mut self, ctx: &mut EventCtx, direction: crate::ui::component::SwipeDirection) {
self.inner.swiped(ctx, direction)
fn swipe_finished(&self) -> bool {
self.inner.swipe_finished()
}
}

View File

@ -25,7 +25,6 @@ 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,

View File

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

View File

@ -16,18 +16,21 @@ impl SwipeDirection {
}
/// 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.
///
/// Default implementation ignores every swipe.
pub trait Swipable {
/// Return true if component can handle swipe in a given direction.
fn can_swipe(&self, _direction: SwipeDirection) -> bool {
/// Attempt a swipe. Return false if component in its current state doesn't
/// accept a swipe in the given direction. Start a transition animation
/// if true is returned.
fn swipe_start(&mut self, _ctx: &mut EventCtx, _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) {}
/// Return true when transition animation is finished. SwipeFlow needs to
/// know this in order to resume normal input processing.
fn swipe_finished(&self) -> bool {
true
}
}
/// Component::Msg for component parts of a flow. Converting results of

View File

@ -23,21 +23,28 @@ pub struct SwipeFlow<Q, S> {
state: Q,
/// FlowStore with all screens/components.
store: S,
/// `Some` when state transition animation is in progress.
transition: Option<Transition<Q>>,
/// `Transition::None` when state transition animation is in not progress.
transition: Transition<Q>,
/// Swipe detector.
swipe: Swipe,
/// Animation parameter.
anim_offset: Offset,
}
struct Transition<Q> {
/// State we are transitioning _from_.
prev_state: Q,
/// Animation progress.
animation: Animation<Offset>,
/// Direction of the slide animation.
direction: SwipeDirection,
enum Transition<Q> {
/// SwipeFlow is performing transition between different states.
External {
/// State we are transitioning _from_.
prev_state: Q,
/// Animation progress.
animation: Animation<Offset>,
/// Direction of the slide animation.
direction: SwipeDirection,
},
/// Transition runs in child component, we forward events and wait.
Internal,
/// No transition.
None,
}
impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
@ -45,14 +52,18 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
Ok(Self {
state: init,
store,
transition: None,
transition: 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 {
if state == self.state {
self.transition = Transition::Internal;
return;
}
self.transition = Transition::External {
prev_state: self.state,
animation: Animation::new(
Offset::zero(),
@ -61,58 +72,71 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
Instant::now(),
),
direction,
});
};
self.state = state;
ctx.request_anim_frame();
ctx.request_paint()
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 render_transition<'s>(
&'s self,
prev_state: &Q,
animation: &Animation<Offset>,
direction: &SwipeDirection,
target: &mut impl Renderer<'s>,
) {
let off = animation.value(Instant::now());
target.with_origin(off, &|target| {
self.render_state(*prev_state, target);
});
target.with_origin(off - 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();
fn handle_transition(&mut self, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg> {
let i = self.state.index();
let mut finished = false;
let result = match &self.transition {
Transition::External { animation, .. }
if matches!(event, Event::Timer(EventCtx::ANIM_FRAME_TIMER)) =>
{
if animation.finished(Instant::now()) {
finished = true;
ctx.request_paint();
self.store.event(i, ctx, Event::Attach)
} else {
ctx.request_anim_frame();
ctx.request_paint();
None
}
}
ctx.request_paint();
Transition::External { .. } => None, // ignore all events until animation finishes
Transition::Internal => {
let msg = self.store.event(i, ctx, event);
if self.store.map_swipable(i, |s| s.swipe_finished()) {
finished = true;
};
msg
}
Transition::None => unreachable!(),
};
if finished {
self.transition = Transition::None;
}
return result;
}
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));
if self
.store
.map_swipable(i, |s| s.swipe_start(ctx, direction))
{
Decision::Goto(self.state, direction)
} else {
Decision::Nothing
@ -142,14 +166,8 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
}
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);
return None;
}
// Ignore events while transition is running.
if self.transition.is_some() {
return None;
if !matches!(self.transition, Transition::None) {
return self.handle_transition(ctx, event);
}
let mut decision = Decision::Nothing;
@ -173,10 +191,13 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
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)
match &self.transition {
Transition::None | Transition::Internal => self.render_state(self.state, target),
Transition::External {
prev_state,
animation,
direction,
} => self.render_transition(prev_state, animation, direction, target),
}
}
}

View File

@ -1,42 +1,101 @@
use crate::ui::{
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
flow::base::Swipable,
geometry::{Axis, Rect},
shape::Renderer,
use crate::{
micropython::gc::Gc,
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
flow::base::Swipable,
geometry::{Axis, Offset, Rect},
shape::Renderer,
},
};
pub struct Transition<T> {
/// Clone of the component before page change.
cloned: Gc<T>,
/// Animation progress.
animation: Animation<Offset>,
/// Direction of the slide animation.
direction: SwipeDirection,
}
const ANIMATION_DURATION: Duration = Duration::from_millis(333);
/// Allows any implementor of `Paginate` to be part of `Swipable` UI flow.
#[derive(Clone)]
/// Renders sliding animation when changing pages.
pub struct SwipePage<T> {
inner: T,
bounds: Rect,
axis: Axis,
pages: usize,
current: usize,
transition: Option<Transition<T>>,
}
impl<T> SwipePage<T> {
impl<T: Component + Paginate + Clone> SwipePage<T> {
pub fn vertical(inner: T) -> Self {
Self {
inner,
bounds: Rect::zero(),
axis: Axis::Vertical,
pages: 1,
current: 0,
transition: None,
}
}
fn handle_transition(ctx: &mut EventCtx, event: Event, transition: &mut Transition<T>) -> bool {
let mut finished = false;
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if transition.animation.finished(Instant::now()) {
finished = true;
} else {
ctx.request_anim_frame();
}
ctx.request_paint()
}
finished
}
fn render_transition<'s>(
&'s self,
transition: &'s Transition<T>,
target: &mut impl Renderer<'s>,
) {
let off = transition.animation.value(Instant::now());
target.in_clip(self.bounds, &|target| {
target.with_origin(off, &|target| {
transition.cloned.render(target);
});
target.with_origin(
off - transition.direction.as_offset(self.bounds.size()),
&|target| {
self.inner.render(target);
},
);
});
}
}
impl<T: Component + Paginate> Component for SwipePage<T> {
impl<T: Component + Paginate + Clone> Component for SwipePage<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
let result = self.inner.place(bounds);
self.bounds = self.inner.place(bounds);
self.pages = self.inner.page_count();
result
self.bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.inner.event(ctx, event);
msg
if let Some(t) = &mut self.transition {
let finished = Self::handle_transition(ctx, event, t);
if finished {
// FIXME: how to ensure the Gc allocation is returned?
self.transition = None
}
return None;
}
self.inner.event(ctx, event)
}
fn paint(&mut self) {
@ -44,33 +103,49 @@ impl<T: Component + Paginate> Component for SwipePage<T> {
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(t) = &self.transition {
return self.render_transition(t, target);
}
self.inner.render(target)
}
}
impl<T: Component + Paginate> Swipable for SwipePage<T> {
fn can_swipe(&self, direction: SwipeDirection) -> bool {
impl<T: Component + Paginate + Clone> Swipable for SwipePage<T> {
fn swipe_start(&mut self, ctx: &mut EventCtx, 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,
}
// Wrong direction
(Axis::Horizontal, SwipeDirection::Up | SwipeDirection::Down) => return false,
(Axis::Vertical, SwipeDirection::Left | SwipeDirection::Right) => return false,
// Begin
(_, SwipeDirection::Right | SwipeDirection::Down) if self.current == 0 => return false,
// End
(_, SwipeDirection::Left | SwipeDirection::Up) if self.current + 1 >= self.pages => {
return false
}
_ => {}
};
self.transition = Some(Transition {
cloned: unwrap!(Gc::new(self.inner.clone())),
animation: Animation::new(
Offset::zero(),
direction.as_offset(self.bounds.size()),
ANIMATION_DURATION,
Instant::now(),
),
direction,
});
self.current = match direction {
SwipeDirection::Left | SwipeDirection::Up => (self.current + 1).min(self.pages - 1),
SwipeDirection::Right | SwipeDirection::Down => self.current.saturating_sub(1),
};
self.inner.change_page(self.current);
ctx.request_anim_frame();
ctx.request_paint();
true
}
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();
fn swipe_finished(&self) -> bool {
self.transition.is_none()
}
}
@ -85,7 +160,6 @@ where
}
/// Make any component swipable by ignoring all swipe events.
#[derive(Clone)]
pub struct IgnoreSwipe<T>(T);
impl<T> IgnoreSwipe<T> {
@ -114,12 +188,7 @@ impl<T: Component> Component for IgnoreSwipe<T> {
}
}
impl<T> Swipable for IgnoreSwipe<T> {
fn can_swipe(&self, _direction: SwipeDirection) -> bool {
false
}
fn swiped(&mut self, _ctx: &mut EventCtx, _direction: SwipeDirection) {}
}
impl<T> Swipable for IgnoreSwipe<T> {}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for IgnoreSwipe<T>

View File

@ -14,9 +14,6 @@ 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;
@ -34,14 +31,8 @@ pub trait FlowStore {
/// 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<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: E,
) -> Result<impl FlowStore, error::Error>
@ -80,12 +71,7 @@ impl FlowStore for FlowEmpty {
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<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: E,
) -> Result<impl FlowStore, error::Error>
@ -94,7 +80,6 @@ impl FlowStore for FlowEmpty {
{
Ok(FlowComponent {
elem: Gc::new(elem)?,
cloned: None,
next: Self,
})
}
@ -104,9 +89,6 @@ struct FlowComponent<E: Component<Msg = FlowMsg>, P> {
/// Component allocated on micropython heap.
pub elem: Gc<E>,
/// Clone.
pub cloned: Option<Gc<E>>,
/// Nested FlowStore.
pub next: P,
}
@ -125,7 +107,7 @@ impl<E: Component<Msg = FlowMsg>, P> FlowComponent<E, P> {
impl<E, P> FlowStore for FlowComponent<E, P>
where
E: Component<Msg = FlowMsg> + MaybeTrace + Swipable + Clone,
E: Component<Msg = FlowMsg> + MaybeTrace + Swipable,
P: FlowStore,
{
fn place(&mut self, bounds: Rect) -> Rect {
@ -167,33 +149,7 @@ where
}
}
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<Msg = FlowMsg> + MaybeTrace + Swipable + Clone>(
fn add<F: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: F,
) -> Result<impl FlowStore, error::Error>
@ -202,7 +158,6 @@ where
{
Ok(FlowComponent {
elem: self.elem,
cloned: None,
next: self.next.add(elem)?,
})
}

View File

@ -97,7 +97,6 @@ where
}
}
#[derive(Clone)]
pub struct IconDialog<U> {
image: Child<BlendedImage>,
paragraphs: Paragraphs<ParagraphVecShort<'static>>,

View File

@ -250,11 +250,15 @@ impl<T> crate::ui::flow::Swipable for Frame<T>
where
T: Component + crate::ui::flow::Swipable,
{
fn can_swipe(&self, direction: crate::ui::component::SwipeDirection) -> bool {
self.inner().can_swipe(direction)
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: crate::ui::component::SwipeDirection,
) -> bool {
self.update_content(ctx, |ctx, inner| inner.swipe_start(ctx, direction))
}
fn swiped(&mut self, ctx: &mut EventCtx, direction: crate::ui::component::SwipeDirection) {
self.update_content(ctx, |ctx, inner| inner.swiped(ctx, direction))
fn swipe_finished(&self) -> bool {
self.inner().swipe_finished()
}
}