mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-24 14:20:57 +00:00
feat(core/ui): T3T1 share words animation
[no changelog]
This commit is contained in:
parent
e5e8e27abc
commit
30ca8bdd62
@ -1,7 +1,7 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
event::TouchEvent,
|
event::TouchEvent,
|
||||||
geometry::{Point, Rect},
|
geometry::{Offset, Point, Rect},
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -13,6 +13,17 @@ pub enum SwipeDirection {
|
|||||||
Right,
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy of `model_tt/component/swipe.rs` but without the backlight handling.
|
/// Copy of `model_tt/component/swipe.rs` but without the backlight handling.
|
||||||
pub struct Swipe {
|
pub struct Swipe {
|
||||||
pub area: Rect,
|
pub area: Rect,
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
use crate::ui::{
|
use crate::ui::component::{EventCtx, SwipeDirection};
|
||||||
component::{EventCtx, SwipeDirection},
|
|
||||||
geometry::Offset,
|
|
||||||
};
|
|
||||||
use num_traits::ToPrimitive;
|
use num_traits::ToPrimitive;
|
||||||
|
|
||||||
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.
|
/// Component must implement this trait in order to be part of swipe-based flow.
|
||||||
///
|
///
|
||||||
/// Default implementation ignores every swipe.
|
/// Default implementation ignores every swipe.
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
micropython::gc::Gc,
|
micropython::gc::Gc,
|
||||||
time::{Duration, Instant},
|
time::Instant,
|
||||||
ui::{
|
ui::{
|
||||||
animation::Animation,
|
animation::Animation,
|
||||||
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
|
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
|
||||||
flow::base::Swipable,
|
flow::base::Swipable,
|
||||||
geometry::{Axis, Offset, Rect},
|
geometry::{Axis, Rect},
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
util,
|
util,
|
||||||
},
|
},
|
||||||
@ -15,13 +15,11 @@ pub struct Transition<T> {
|
|||||||
/// Clone of the component before page change.
|
/// Clone of the component before page change.
|
||||||
cloned: Gc<T>,
|
cloned: Gc<T>,
|
||||||
/// Animation progress.
|
/// Animation progress.
|
||||||
animation: Animation<Offset>,
|
animation: Animation<f32>,
|
||||||
/// Direction of the slide animation.
|
/// Direction of the slide animation.
|
||||||
direction: SwipeDirection,
|
direction: SwipeDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ANIMATION_DURATION: Duration = Duration::from_millis(333);
|
|
||||||
|
|
||||||
/// Allows any implementor of `Paginate` to be part of `Swipable` UI flow.
|
/// Allows any implementor of `Paginate` to be part of `Swipable` UI flow.
|
||||||
/// Renders sliding animation when changing pages.
|
/// Renders sliding animation when changing pages.
|
||||||
pub struct SwipePage<T> {
|
pub struct SwipePage<T> {
|
||||||
@ -74,16 +72,13 @@ impl<T: Component + Paginate + Clone> SwipePage<T> {
|
|||||||
transition: &'s Transition<T>,
|
transition: &'s Transition<T>,
|
||||||
target: &mut impl Renderer<'s>,
|
target: &mut impl Renderer<'s>,
|
||||||
) {
|
) {
|
||||||
let off = transition.animation.value(Instant::now());
|
|
||||||
target.in_clip(self.bounds, &|target| {
|
target.in_clip(self.bounds, &|target| {
|
||||||
target.with_origin(off, &|target| {
|
util::render_slide(
|
||||||
transition.cloned.render(target);
|
|target| transition.cloned.render(target),
|
||||||
});
|
|target| self.inner.render(target),
|
||||||
target.with_origin(
|
transition.animation.value(Instant::now()),
|
||||||
off - transition.direction.as_offset(self.bounds.size()),
|
transition.direction,
|
||||||
&|target| {
|
target,
|
||||||
self.inner.render(target);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -147,12 +142,7 @@ impl<T: Component + Paginate + Clone> Swipable for SwipePage<T> {
|
|||||||
}
|
}
|
||||||
self.transition = Some(Transition {
|
self.transition = Some(Transition {
|
||||||
cloned: unwrap!(Gc::new(self.inner.clone())),
|
cloned: unwrap!(Gc::new(self.inner.clone())),
|
||||||
animation: Animation::new(
|
animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION, Instant::now()),
|
||||||
Offset::zero(),
|
|
||||||
direction.as_offset(self.bounds.size()),
|
|
||||||
ANIMATION_DURATION,
|
|
||||||
Instant::now(),
|
|
||||||
),
|
|
||||||
direction,
|
direction,
|
||||||
});
|
});
|
||||||
self.inner.change_page(self.current);
|
self.inner.change_page(self.current);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
error,
|
error,
|
||||||
time::{Duration, Instant},
|
time::Instant,
|
||||||
ui::{
|
ui::{
|
||||||
animation::Animation,
|
animation::Animation,
|
||||||
component::{Component, Event, EventCtx, Swipe, SwipeDirection},
|
component::{Component, Event, EventCtx, Swipe, SwipeDirection},
|
||||||
@ -11,8 +11,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMATION_DURATION: Duration = Duration::from_millis(333);
|
|
||||||
|
|
||||||
/// Given a state enum and a corresponding FlowStore, create a Component that
|
/// Given a state enum and a corresponding FlowStore, create a Component that
|
||||||
/// implements a swipe navigation between the states with animated transitions.
|
/// implements a swipe navigation between the states with animated transitions.
|
||||||
///
|
///
|
||||||
@ -28,8 +26,6 @@ pub struct SwipeFlow<Q, S> {
|
|||||||
transition: Transition<Q>,
|
transition: Transition<Q>,
|
||||||
/// Swipe detector.
|
/// Swipe detector.
|
||||||
swipe: Swipe,
|
swipe: Swipe,
|
||||||
/// Animation parameter.
|
|
||||||
anim_offset: Offset,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Transition<Q> {
|
enum Transition<Q> {
|
||||||
@ -38,7 +34,7 @@ enum Transition<Q> {
|
|||||||
/// State we are transitioning _from_.
|
/// State we are transitioning _from_.
|
||||||
prev_state: Q,
|
prev_state: Q,
|
||||||
/// Animation progress.
|
/// Animation progress.
|
||||||
animation: Animation<Offset>,
|
animation: Animation<f32>,
|
||||||
/// Direction of the slide animation.
|
/// Direction of the slide animation.
|
||||||
direction: SwipeDirection,
|
direction: SwipeDirection,
|
||||||
},
|
},
|
||||||
@ -55,7 +51,6 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
|
|||||||
store,
|
store,
|
||||||
transition: Transition::None,
|
transition: Transition::None,
|
||||||
swipe: Swipe::new().down().up().left().right(),
|
swipe: Swipe::new().down().up().left().right(),
|
||||||
anim_offset: Offset::zero(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,12 +67,7 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
|
|||||||
}
|
}
|
||||||
self.transition = Transition::External {
|
self.transition = Transition::External {
|
||||||
prev_state: self.state,
|
prev_state: self.state,
|
||||||
animation: Animation::new(
|
animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION, Instant::now()),
|
||||||
Offset::zero(),
|
|
||||||
direction.as_offset(self.anim_offset),
|
|
||||||
ANIMATION_DURATION,
|
|
||||||
Instant::now(),
|
|
||||||
),
|
|
||||||
direction,
|
direction,
|
||||||
};
|
};
|
||||||
self.state = state;
|
self.state = state;
|
||||||
@ -92,17 +82,17 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
|
|||||||
fn render_transition<'s>(
|
fn render_transition<'s>(
|
||||||
&'s self,
|
&'s self,
|
||||||
prev_state: &Q,
|
prev_state: &Q,
|
||||||
animation: &Animation<Offset>,
|
animation: &Animation<f32>,
|
||||||
direction: &SwipeDirection,
|
direction: &SwipeDirection,
|
||||||
target: &mut impl Renderer<'s>,
|
target: &mut impl Renderer<'s>,
|
||||||
) {
|
) {
|
||||||
let off = animation.value(Instant::now());
|
util::render_slide(
|
||||||
target.with_origin(off, &|target| {
|
|target| self.render_state(*prev_state, target),
|
||||||
self.render_state(*prev_state, target);
|
|target| self.render_state(self.state, target),
|
||||||
});
|
animation.value(Instant::now()),
|
||||||
target.with_origin(off - direction.as_offset(self.anim_offset), &|target| {
|
*direction,
|
||||||
self.render_state(self.state, target);
|
target,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_transition(&mut self, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg> {
|
fn handle_transition(&mut self, ctx: &mut EventCtx, event: Event) -> Option<FlowMsg> {
|
||||||
@ -164,10 +154,6 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
|
|||||||
type Msg = FlowMsg;
|
type Msg = FlowMsg;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
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.swipe.place(bounds);
|
||||||
self.store.place(bounds)
|
self.store.place(bounds)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::ui::lerp::Lerp;
|
use crate::ui::lerp::Lerp;
|
||||||
use core::ops::{Add, Neg, Sub};
|
use core::ops::{Add, Mul, Neg, Sub};
|
||||||
|
|
||||||
const fn min(a: i16, b: i16) -> i16 {
|
const fn min(a: i16, b: i16) -> i16 {
|
||||||
if a < b {
|
if a < b {
|
||||||
@ -128,6 +128,17 @@ impl Sub<Offset> for Offset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Mul<f32> for Offset {
|
||||||
|
type Output = Offset;
|
||||||
|
|
||||||
|
fn mul(self, rhs: f32) -> Self::Output {
|
||||||
|
Offset::new(
|
||||||
|
(f32::from(self.x) * rhs) as i16,
|
||||||
|
(f32::from(self.y) * rhs) as i16,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Point> for Offset {
|
impl From<Point> for Offset {
|
||||||
fn from(val: Point) -> Self {
|
fn from(val: Point) -> Self {
|
||||||
Offset::new(val.x, val.y)
|
Offset::new(val.x, val.y)
|
||||||
|
@ -71,6 +71,8 @@ impl_lerp_for_uint!(u8);
|
|||||||
impl_lerp_for_uint!(u16);
|
impl_lerp_for_uint!(u16);
|
||||||
impl_lerp_for_uint!(u32);
|
impl_lerp_for_uint!(u32);
|
||||||
|
|
||||||
|
impl_lerp_for_int!(f32);
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -7,10 +7,8 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, TimerToken},
|
component::{Component, Event, EventCtx, TimerToken},
|
||||||
display::{
|
display::{
|
||||||
self,
|
|
||||||
image::{ImageInfo, ToifFormat},
|
image::{ImageInfo, ToifFormat},
|
||||||
tjpgd::jpeg_info,
|
toif::Icon,
|
||||||
toif::{Icon, Toif},
|
|
||||||
Color, Font,
|
Color, Font,
|
||||||
},
|
},
|
||||||
event::{TouchEvent, USBEvent},
|
event::{TouchEvent, USBEvent},
|
||||||
|
@ -67,7 +67,7 @@ pub use prompt_screen::PromptScreen;
|
|||||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||||
pub use scroll::ScrollBar;
|
pub use scroll::ScrollBar;
|
||||||
#[cfg(feature = "translations")]
|
#[cfg(feature = "translations")]
|
||||||
pub use share_words::{ShareWords, ShareWordsMsg};
|
pub use share_words::ShareWords;
|
||||||
pub use simple_page::SimplePage;
|
pub use simple_page::SimplePage;
|
||||||
pub use status_screen::StatusScreen;
|
pub use status_screen::StatusScreen;
|
||||||
pub use swipe::{Swipe, SwipeDirection};
|
pub use swipe::{Swipe, SwipeDirection};
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
use super::theme;
|
use super::theme;
|
||||||
use crate::{
|
use crate::{
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
|
time::{Duration, Instant},
|
||||||
translations::TR,
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, Paginate, Swipe, SwipeDirection},
|
animation::Animation,
|
||||||
|
component::{Component, Event, EventCtx, Never, SwipeDirection},
|
||||||
flow::Swipable,
|
flow::Swipable,
|
||||||
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
||||||
model_mercury::component::Footer,
|
model_mercury::component::Footer,
|
||||||
shape,
|
shape,
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
|
util,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use heapless::{String, Vec};
|
use heapless::{String, Vec};
|
||||||
|
|
||||||
const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less
|
const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less
|
||||||
|
const ANIMATION_DURATION: Duration = Duration::from_millis(166);
|
||||||
|
|
||||||
/// Component showing mnemonic/share words during backup procedure. Model T3T1
|
/// Component showing mnemonic/share words during backup procedure. Model T3T1
|
||||||
/// contains one word per screen. A user is instructed to swipe up/down to see
|
/// contains one word per screen. A user is instructed to swipe up/down to see
|
||||||
@ -22,19 +26,15 @@ pub struct ShareWords<'a> {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
share_words: Vec<TString<'a>, MAX_WORDS>,
|
share_words: Vec<TString<'a>, MAX_WORDS>,
|
||||||
page_index: usize,
|
page_index: usize,
|
||||||
|
prev_index: usize,
|
||||||
/// Area reserved for a shown word from mnemonic/share
|
/// Area reserved for a shown word from mnemonic/share
|
||||||
area_word: Rect,
|
area_word: Rect,
|
||||||
/// TODO: review when swipe concept done for T3T1
|
/// `Some` when transition animation is in progress
|
||||||
swipe: Swipe,
|
animation: Option<Animation<f32>>,
|
||||||
/// Footer component for instructions and word counting
|
/// Footer component for instructions and word counting
|
||||||
footer: Footer<'static>,
|
footer: Footer<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ShareWordsMsg {
|
|
||||||
GoPrevScreen,
|
|
||||||
WordsSeen,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ShareWords<'a> {
|
impl<'a> ShareWords<'a> {
|
||||||
const AREA_WORD_HEIGHT: i16 = 91;
|
const AREA_WORD_HEIGHT: i16 = 91;
|
||||||
|
|
||||||
@ -43,8 +43,9 @@ impl<'a> ShareWords<'a> {
|
|||||||
area: Rect::zero(),
|
area: Rect::zero(),
|
||||||
share_words,
|
share_words,
|
||||||
page_index: 0,
|
page_index: 0,
|
||||||
|
prev_index: 0,
|
||||||
area_word: Rect::zero(),
|
area_word: Rect::zero(),
|
||||||
swipe: Swipe::new().up().down(),
|
animation: None,
|
||||||
footer: Footer::new(TR::instructions__swipe_up),
|
footer: Footer::new(TR::instructions__swipe_up),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,10 +57,23 @@ impl<'a> ShareWords<'a> {
|
|||||||
fn is_final_page(&self) -> bool {
|
fn is_final_page(&self) -> bool {
|
||||||
self.page_index == self.share_words.len() - 1
|
self.page_index == self.share_words.len() - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_word<'s>(&'s self, word_index: usize, target: &mut impl Renderer<'s>) {
|
||||||
|
// the share word
|
||||||
|
let word = self.share_words[word_index];
|
||||||
|
let word_baseline = target.viewport().clip.center()
|
||||||
|
+ Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2);
|
||||||
|
word.map(|w| {
|
||||||
|
shape::Text::new(word_baseline, w)
|
||||||
|
.with_font(theme::TEXT_SUPER.text_font)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Component for ShareWords<'a> {
|
impl<'a> Component for ShareWords<'a> {
|
||||||
type Msg = ShareWordsMsg;
|
type Msg = Never;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
self.area = bounds;
|
self.area = bounds;
|
||||||
@ -76,30 +90,19 @@ impl<'a> Component for ShareWords<'a> {
|
|||||||
self.footer
|
self.footer
|
||||||
.place(used_area.split_bottom(Footer::HEIGHT_SIMPLE).1);
|
.place(used_area.split_bottom(Footer::HEIGHT_SIMPLE).1);
|
||||||
|
|
||||||
self.swipe.place(bounds); // Swipe possible on the whole screen area
|
|
||||||
self.area
|
self.area
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||||
// ctx.set_page_count(self.share_words.len());
|
// ctx.set_page_count(self.share_words.len());
|
||||||
let swipe = self.swipe.event(ctx, event);
|
if let Some(a) = &self.animation {
|
||||||
match swipe {
|
if a.finished(Instant::now()) {
|
||||||
Some(SwipeDirection::Up) => {
|
self.animation = None;
|
||||||
if self.is_final_page() {
|
} else {
|
||||||
return Some(ShareWordsMsg::WordsSeen);
|
ctx.request_anim_frame();
|
||||||
}
|
}
|
||||||
self.change_page(self.page_index + 1);
|
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
Some(SwipeDirection::Down) => {
|
|
||||||
if self.is_first_page() {
|
|
||||||
return Some(ShareWordsMsg::GoPrevScreen);
|
|
||||||
}
|
|
||||||
self.change_page(self.page_index.saturating_sub(1));
|
|
||||||
ctx.request_paint();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,16 +132,25 @@ impl<'a> Component for ShareWords<'a> {
|
|||||||
.with_fg(theme::GREY)
|
.with_fg(theme::GREY)
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
// the share word
|
if let Some(animation) = &self.animation {
|
||||||
let word = self.share_words[self.page_index];
|
target.in_clip(self.area_word, &|target| {
|
||||||
let word_baseline = self.area_word.center()
|
util::render_slide(
|
||||||
+ Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2);
|
|target| self.render_word(self.prev_index, target),
|
||||||
word.map(|w| {
|
|target| self.render_word(self.page_index, target),
|
||||||
shape::Text::new(word_baseline, w)
|
animation.value(Instant::now()),
|
||||||
.with_font(theme::TEXT_SUPER.text_font)
|
if self.prev_index < self.page_index {
|
||||||
.with_align(Alignment::Center)
|
SwipeDirection::Up
|
||||||
.render(target);
|
} else {
|
||||||
|
SwipeDirection::Down
|
||||||
|
},
|
||||||
|
target,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
target.in_clip(self.area_word, &|target| {
|
||||||
|
self.render_word(self.page_index, target);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// footer with instructions
|
// footer with instructions
|
||||||
self.footer.render(target);
|
self.footer.render(target);
|
||||||
@ -148,15 +160,36 @@ impl<'a> Component for ShareWords<'a> {
|
|||||||
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Swipable for ShareWords<'a> {}
|
impl<'a> Swipable for ShareWords<'a> {
|
||||||
|
fn swipe_start(&mut self, ctx: &mut EventCtx, direction: SwipeDirection) -> bool {
|
||||||
impl<'a> Paginate for ShareWords<'a> {
|
match direction {
|
||||||
fn page_count(&mut self) -> usize {
|
SwipeDirection::Up if !self.is_final_page() => {
|
||||||
self.share_words.len()
|
self.prev_index = self.page_index;
|
||||||
|
self.page_index = (self.page_index + 1).min(self.share_words.len() - 1);
|
||||||
|
}
|
||||||
|
SwipeDirection::Down if !self.is_first_page() => {
|
||||||
|
self.prev_index = self.page_index;
|
||||||
|
self.page_index = self.page_index.saturating_sub(1);
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
if util::animation_disabled() {
|
||||||
|
ctx.request_paint();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.animation = Some(Animation::new(
|
||||||
|
0.0f32,
|
||||||
|
1.0f32,
|
||||||
|
ANIMATION_DURATION,
|
||||||
|
Instant::now(),
|
||||||
|
));
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_page(&mut self, active_page: usize) {
|
fn swipe_finished(&self) -> bool {
|
||||||
self.page_index = active_page;
|
self.animation.is_none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ use crate::{
|
|||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
use super::super::{
|
use super::super::{
|
||||||
component::{Frame, FrameMsg, PromptScreen, ShareWords, ShareWordsMsg, SwipeDirection},
|
component::{Frame, FrameMsg, PromptScreen, ShareWords, SwipeDirection},
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,6 +37,12 @@ impl FlowState for ShowShareWords {
|
|||||||
(ShowShareWords::Confirm, SwipeDirection::Down) => {
|
(ShowShareWords::Confirm, SwipeDirection::Down) => {
|
||||||
Decision::Goto(ShowShareWords::Words, direction)
|
Decision::Goto(ShowShareWords::Words, direction)
|
||||||
}
|
}
|
||||||
|
(ShowShareWords::Words, SwipeDirection::Up) => {
|
||||||
|
Decision::Goto(ShowShareWords::Confirm, direction)
|
||||||
|
}
|
||||||
|
(ShowShareWords::Words, SwipeDirection::Down) => {
|
||||||
|
Decision::Goto(ShowShareWords::Instruction, direction)
|
||||||
|
}
|
||||||
(ShowShareWords::CheckBackupIntro, SwipeDirection::Up) => {
|
(ShowShareWords::CheckBackupIntro, SwipeDirection::Up) => {
|
||||||
Decision::Return(FlowMsg::Confirmed)
|
Decision::Return(FlowMsg::Confirmed)
|
||||||
}
|
}
|
||||||
@ -85,11 +91,7 @@ impl ShowShareWords {
|
|||||||
.map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed));
|
.map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed));
|
||||||
|
|
||||||
let content_words =
|
let content_words =
|
||||||
Frame::left_aligned(title, ShareWords::new(share_words_vec)).map(|msg| match msg {
|
Frame::left_aligned(title, ShareWords::new(share_words_vec)).map(|_| None);
|
||||||
FrameMsg::Content(ShareWordsMsg::GoPrevScreen) => Some(FlowMsg::Cancelled),
|
|
||||||
FrameMsg::Content(ShareWordsMsg::WordsSeen) => Some(FlowMsg::Confirmed),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let content_confirm =
|
let content_confirm =
|
||||||
Frame::left_aligned(text_confirm, PromptScreen::new_hold_to_confirm())
|
Frame::left_aligned(text_confirm, PromptScreen::new_hold_to_confirm())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
strutil::{ShortString, TString},
|
strutil::{ShortString, TString},
|
||||||
|
time::Duration,
|
||||||
ui::{
|
ui::{
|
||||||
component::text::TextStyle,
|
component::text::TextStyle,
|
||||||
display,
|
display,
|
||||||
@ -179,6 +180,32 @@ macro_rules! include_icon {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const SLIDE_DURATION: Duration = Duration::from_millis(333);
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
pub fn render_slide<'s, F0, F1, R>(
|
||||||
|
render_old: F0,
|
||||||
|
render_new: F1,
|
||||||
|
progress: f32,
|
||||||
|
direction: crate::ui::component::SwipeDirection,
|
||||||
|
target: &mut R,
|
||||||
|
) where
|
||||||
|
R: crate::ui::shape::Renderer<'s>,
|
||||||
|
F0: Fn(&mut R),
|
||||||
|
F1: Fn(&mut R),
|
||||||
|
{
|
||||||
|
let bounds = target.viewport().clip;
|
||||||
|
let full_offset = direction.as_offset(bounds.size());
|
||||||
|
let current_offset = full_offset * progress;
|
||||||
|
|
||||||
|
target.with_origin(current_offset, &|target| {
|
||||||
|
render_old(target);
|
||||||
|
});
|
||||||
|
target.with_origin(current_offset - full_offset, &|target| {
|
||||||
|
render_new(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::strutil;
|
use crate::strutil;
|
||||||
|
Loading…
Reference in New Issue
Block a user