1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 14:58:09 +00:00

feat(core) screen transitions for mercury UI

[no changelog]
This commit is contained in:
tychovrahe 2024-05-26 12:35:42 +02:00 committed by matejcik
parent ed58409888
commit a67bc19bac
52 changed files with 1919 additions and 857 deletions

View File

@ -88,7 +88,7 @@ SECTIONS {
} >SRAM1
.stack : ALIGN(8) {
. = 16K; /* Overflow causes UsageFault */
. = 32K; /* Overflow causes UsageFault */
} >SRAM2
.confidential : ALIGN(512) {

View File

@ -4,6 +4,7 @@ use crate::{
};
/// Running, time-based linear progression of a value.
#[derive(Clone)]
pub struct Animation<T> {
/// Starting value.
pub from: T,

View File

@ -16,11 +16,13 @@ use crate::{
#[cfg(feature = "button")]
use crate::ui::event::ButtonEvent;
#[cfg(feature = "touch")]
use crate::ui::event::TouchEvent;
use crate::ui::event::USBEvent;
#[cfg(feature = "touch")]
use crate::ui::event::{SwipeEvent, TouchEvent};
use super::Paginate;
#[cfg(feature = "touch")]
use super::SwipeDirection;
/// Type used by components that do not return any messages.
///
@ -466,6 +468,13 @@ where
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum AttachType {
Initial,
#[cfg(feature = "touch")]
Swipe(SwipeDirection),
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Event {
#[cfg(feature = "button")]
@ -480,10 +489,13 @@ pub enum Event {
Progress(u16, TString<'static>),
/// Component has been attached to component tree. This event is sent once
/// before any other events.
Attach,
Attach(AttachType),
/// Internally-handled event to inform all `Child` wrappers in a sub-tree to
/// get scheduled for painting.
RequestPaint,
/// Swipe and transition events
#[cfg(feature = "touch")]
Swipe(SwipeEvent),
}
#[derive(Copy, Clone, PartialEq, Eq)]
@ -511,6 +523,8 @@ pub struct EventCtx {
page_count: Option<usize>,
button_request: Option<ButtonRequest>,
root_repaint_requested: bool,
swipe_disable_req: bool,
swipe_enable_req: bool,
}
impl EventCtx {
@ -538,6 +552,8 @@ impl EventCtx {
page_count: None,
button_request: None,
root_repaint_requested: false,
swipe_disable_req: false,
swipe_enable_req: false,
}
}
@ -612,6 +628,22 @@ impl EventCtx {
self.timers.pop()
}
pub fn disable_swipe(&mut self) {
self.swipe_disable_req = true;
}
pub fn disable_swipe_requested(&self) -> bool {
self.swipe_disable_req
}
pub fn enable_swipe(&mut self) {
self.swipe_enable_req = true;
}
pub fn enable_swipe_requested(&self) -> bool {
self.swipe_enable_req
}
pub fn clear(&mut self) {
self.place_requested = false;
self.paint_requested = false;
@ -621,6 +653,8 @@ impl EventCtx {
assert!(self.button_request.is_none());
self.button_request = None;
self.root_repaint_requested = false;
self.swipe_disable_req = false;
self.swipe_enable_req = false;
}
fn register_timer(&mut self, token: TimerToken, deadline: Duration) {

View File

@ -4,6 +4,9 @@ use crate::ui::{
geometry::Rect,
};
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
use crate::ui::component::swipe_detect::SwipeConfig;
/// Component that sends a ButtonRequest after receiving Event::Attach. The
/// request is only sent once.
#[derive(Clone)]
@ -29,7 +32,7 @@ impl<T: Component> Component for OneButtonRequest<T> {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if matches!(event, Event::Attach) {
if matches!(event, Event::Attach(_)) {
if let Some(button_request) = self.button_request.take() {
ctx.send_button_request(button_request.code, button_request.br_type)
}
@ -46,6 +49,17 @@ impl<T: Component> Component for OneButtonRequest<T> {
}
}
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T: crate::ui::flow::Swipable> crate::ui::flow::Swipable for OneButtonRequest<T> {
fn get_swipe_config(&self) -> SwipeConfig {
self.inner.get_swipe_config()
}
fn get_internal_page_count(&self) -> usize {
self.inner.get_internal_page_count()
}
}
#[cfg(feature = "ui_debug")]
impl<T: crate::trace::Trace> crate::trace::Trace for OneButtonRequest<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
@ -63,21 +77,3 @@ pub trait ButtonRequestExt {
}
impl<T: Component> ButtonRequestExt for T {}
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T> crate::ui::flow::Swipable<T::Msg> for OneButtonRequest<T>
where
T: Component + crate::ui::flow::Swipable<T::Msg>,
{
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: crate::ui::component::SwipeDirection,
) -> crate::ui::flow::SwipableResult<T::Msg> {
self.inner.swipe_start(ctx, direction)
}
fn swipe_finished(&self) -> bool {
self.inner.swipe_finished()
}
}

View File

@ -1,6 +1,9 @@
use super::{Component, Event, EventCtx};
use crate::ui::{geometry::Rect, shape::Renderer};
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
use crate::ui::component::swipe_detect::SwipeConfig;
pub struct MsgMap<T, F> {
inner: T,
func: F,
@ -41,6 +44,19 @@ where
}
}
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T, F> crate::ui::flow::Swipable for MsgMap<T, F>
where
T: Component + crate::ui::flow::Swipable,
{
fn get_swipe_config(&self) -> SwipeConfig {
self.inner.get_swipe_config()
}
fn get_internal_page_count(&self) -> usize {
self.inner.get_internal_page_count()
}
}
#[cfg(feature = "ui_debug")]
impl<T, F> crate::trace::Trace for MsgMap<T, F>
where
@ -51,25 +67,6 @@ where
}
}
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T, F, U> crate::ui::flow::Swipable<U> for MsgMap<T, F>
where
T: Component + crate::ui::flow::Swipable<T::Msg>,
F: Fn(T::Msg) -> Option<U>,
{
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: super::SwipeDirection,
) -> crate::ui::flow::SwipableResult<U> {
self.inner.swipe_start(ctx, direction).map(&self.func)
}
fn swipe_finished(&self) -> bool {
self.inner.swipe_finished()
}
}
pub struct PageMap<T, F> {
inner: T,
func: F,
@ -123,19 +120,14 @@ where
}
#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T, F> crate::ui::flow::Swipable<T::Msg> for PageMap<T, F>
impl<T, F> crate::ui::flow::Swipable for PageMap<T, F>
where
T: Component + crate::ui::flow::Swipable<T::Msg>,
T: Component + crate::ui::flow::Swipable,
{
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: super::SwipeDirection,
) -> crate::ui::flow::SwipableResult<T::Msg> {
self.inner.swipe_start(ctx, direction)
fn get_swipe_config(&self) -> SwipeConfig {
self.inner.get_swipe_config()
}
fn swipe_finished(&self) -> bool {
self.inner.swipe_finished()
fn get_internal_page_count(&self) -> usize {
self.inner.get_internal_page_count()
}
}

View File

@ -19,6 +19,8 @@ pub mod placed;
pub mod qr_code;
#[cfg(feature = "touch")]
pub mod swipe;
#[cfg(feature = "touch")]
pub mod swipe_detect;
pub mod text;
pub mod timeout;
@ -39,6 +41,8 @@ pub use placed::{FixedHeightBar, Floating, GridPlaced, Split};
pub use qr_code::Qr;
#[cfg(feature = "touch")]
pub use swipe::{Swipe, SwipeDirection};
#[cfg(feature = "touch")]
pub use swipe_detect::{SwipeDetect, SwipeDetectMsg};
pub use text::{
formatted::FormattedText,
layout::{LineBreaking, PageBreaking, TextLayout},

View File

@ -5,7 +5,7 @@ use crate::ui::{
shape::Renderer,
};
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum SwipeDirection {
Up,
Down,

View File

@ -0,0 +1,415 @@
use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Event, EventCtx, SwipeDirection},
event::TouchEvent,
geometry::{Offset, Point},
util::animation_disabled,
},
};
#[derive(Clone)]
pub struct SwipeSettings {
pub duration: Duration,
}
impl SwipeSettings {
pub const fn new(duration: Duration) -> Self {
Self { duration }
}
pub const fn default() -> Self {
Self {
duration: Duration::from_millis(333),
}
}
pub const fn immediate() -> Self {
Self {
duration: Duration::from_millis(0),
}
}
}
#[derive(Clone, Default)]
pub struct SwipeConfig {
pub horizontal_pages: bool,
pub vertical_pages: bool,
pub up: Option<SwipeSettings>,
pub down: Option<SwipeSettings>,
pub left: Option<SwipeSettings>,
pub right: Option<SwipeSettings>,
}
impl SwipeConfig {
pub const fn new() -> Self {
Self {
horizontal_pages: false,
vertical_pages: false,
up: None,
down: None,
left: None,
right: None,
}
}
pub fn with_swipe(&self, dir: SwipeDirection, settings: SwipeSettings) -> Self {
let mut new = self.clone();
match dir {
SwipeDirection::Up => new.up = Some(settings),
SwipeDirection::Down => new.down = Some(settings),
SwipeDirection::Left => new.left = Some(settings),
SwipeDirection::Right => new.right = Some(settings),
}
new
}
pub fn is_allowed(&self, dir: SwipeDirection) -> bool {
match dir {
SwipeDirection::Up => self.up.is_some(),
SwipeDirection::Down => self.down.is_some(),
SwipeDirection::Left => self.left.is_some(),
SwipeDirection::Right => self.right.is_some(),
}
}
pub fn duration(&self, dir: SwipeDirection) -> Option<Duration> {
match dir {
SwipeDirection::Up => self.up.as_ref().map(|s| s.duration),
SwipeDirection::Down => self.down.as_ref().map(|s| s.duration),
SwipeDirection::Left => self.left.as_ref().map(|s| s.duration),
SwipeDirection::Right => self.right.as_ref().map(|s| s.duration),
}
}
pub fn has_horizontal_pages(&self) -> bool {
self.horizontal_pages
}
pub fn has_vertical_pages(&self) -> bool {
self.vertical_pages
}
pub fn with_horizontal_pages(&self) -> Self {
let mut new = self.clone();
new.horizontal_pages = true;
new
}
pub fn with_vertical_pages(&self) -> Self {
let mut new = self.clone();
new.vertical_pages = true;
new
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum SwipeDetectMsg {
Move(SwipeDirection, i16),
Trigger(SwipeDirection),
}
#[derive(Clone)]
pub struct SwipeDetect {
origin: Option<Point>,
locked: Option<SwipeDirection>,
final_animation: Option<Animation<i16>>,
moved: i16,
}
impl SwipeDetect {
const DISTANCE: i16 = 120;
pub const PROGRESS_MAX: i16 = 1000;
const DURATION_MS: u32 = 333;
const TRIGGER_THRESHOLD: f32 = 0.3;
const DETECT_THRESHOLD: f32 = 0.1;
pub fn new() -> Self {
Self {
origin: None,
locked: None,
final_animation: None,
moved: 0,
}
}
fn min_lock(&self) -> i16 {
(Self::DISTANCE as f32 * Self::DETECT_THRESHOLD) as i16
}
fn min_trigger(&self) -> i16 {
(Self::DISTANCE as f32 * Self::TRIGGER_THRESHOLD) as i16
}
fn progress(&self, val: i16) -> i16 {
((val.max(0) as f32 / Self::DISTANCE as f32) * Self::PROGRESS_MAX as f32) as i16
}
pub fn trigger(&mut self, ctx: &mut EventCtx, dir: SwipeDirection, config: SwipeConfig) {
ctx.request_anim_frame();
ctx.request_paint();
let duration = config
.duration(dir)
.unwrap_or(Duration::from_millis(Self::DURATION_MS));
self.locked = Some(dir);
self.final_animation = Some(Animation::new(
0,
Self::PROGRESS_MAX,
duration,
Instant::now(),
));
}
pub(crate) fn reset(&mut self) {
self.origin = None;
self.locked = None;
self.final_animation = None;
self.moved = 0;
}
pub(crate) fn event(
&mut self,
ctx: &mut EventCtx,
event: Event,
config: SwipeConfig,
) -> Option<SwipeDetectMsg> {
match (event, self.origin) {
(Event::Touch(TouchEvent::TouchStart(pos)), _) => {
// Mark the starting position of this touch.
self.origin.replace(pos);
}
(Event::Touch(TouchEvent::TouchMove(pos)), Some(origin)) => {
if self.final_animation.is_none() {
// Compare the touch distance with our allowed directions and determine if it
// constitutes a valid swipe.
let ofs = pos - origin;
let ofs_min = ofs.abs() - Offset::new(self.min_lock(), self.min_lock());
let mut res = None;
if self.locked.is_none() {
if ofs.x > 0 && ofs_min.x > 0 && config.is_allowed(SwipeDirection::Right) {
self.locked = Some(SwipeDirection::Right);
res = Some(SwipeDetectMsg::Move(
SwipeDirection::Right,
self.progress(ofs_min.x),
));
}
if ofs.x < 0 && ofs_min.x > 0 && config.is_allowed(SwipeDirection::Left) {
self.locked = Some(SwipeDirection::Left);
res = Some(SwipeDetectMsg::Move(
SwipeDirection::Left,
self.progress(ofs_min.x),
));
}
if ofs.y < 0 && ofs_min.y > 0 && config.is_allowed(SwipeDirection::Up) {
self.locked = Some(SwipeDirection::Up);
res = Some(SwipeDetectMsg::Move(
SwipeDirection::Up,
self.progress(ofs_min.y),
));
}
if ofs.y > 0 && ofs_min.y > 0 && config.is_allowed(SwipeDirection::Down) {
self.locked = Some(SwipeDirection::Down);
res = Some(SwipeDetectMsg::Move(
SwipeDirection::Down,
self.progress(ofs_min.y),
));
}
} else {
res = match self.locked.unwrap() {
SwipeDirection::Left => {
if ofs.x > 0 {
Some(SwipeDetectMsg::Move(SwipeDirection::Left, 0))
} else {
Some(SwipeDetectMsg::Move(
SwipeDirection::Left,
self.progress(ofs_min.x),
))
}
}
SwipeDirection::Right => {
if ofs.x < 0 {
Some(SwipeDetectMsg::Move(SwipeDirection::Right, 0))
} else {
Some(SwipeDetectMsg::Move(
SwipeDirection::Right,
self.progress(ofs_min.x),
))
}
}
SwipeDirection::Up => {
if ofs.y > 0 {
Some(SwipeDetectMsg::Move(SwipeDirection::Up, 0))
} else {
Some(SwipeDetectMsg::Move(
SwipeDirection::Up,
self.progress(ofs_min.y),
))
}
}
SwipeDirection::Down => {
if ofs.y < 0 {
Some(SwipeDetectMsg::Move(SwipeDirection::Down, 0))
} else {
Some(SwipeDetectMsg::Move(
SwipeDirection::Down,
self.progress(ofs_min.y),
))
}
}
};
}
// Todo trigger an action if distance is met
if let Some(SwipeDetectMsg::Move(_, ofs)) = res {
self.moved = ofs;
}
if animation_disabled() {
return None;
}
return res;
}
}
(Event::Touch(TouchEvent::TouchEnd(pos)), Some(origin)) => {
if self.final_animation.is_none() {
// 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 ofs_min = ofs.abs() - Offset::new(self.min_trigger(), self.min_trigger());
match self.locked {
// advance in locked direction only
Some(locked) if config.progress(locked, ofs, 0) > 0 => (),
// advance in direction other than locked clears the lock -- touch ends
// without triggering
Some(_) => self.locked = None,
None => {
for dir in SwipeDirection::iter() {
// insta-lock if the movement went at least the trigger distance
if config.progress(dir, ofs, self.min_trigger()) > 0 {
self.locked = Some(dir);
break;
}
}
}
};
let Some(locked) = self.locked else {
// No direction is locked. Touch ended without triggering a swipe.
return None;
};
ctx.request_anim_frame();
ctx.request_paint();
if !animation_disabled() {
let done = self.moved as f32 / Self::PROGRESS_MAX as f32;
let ratio = 1.0 - done;
let duration = config
.duration(locked)
.unwrap_or(Duration::from_millis(Self::DURATION_MS));
let duration = ((duration.to_millis() as f32 * ratio) as u32).max(0);
self.final_animation = Some(Animation::new(
self.moved as i16,
Self::PROGRESS_MAX,
Duration::from_millis(duration),
Instant::now(),
));
} else {
// clear animation
self.final_animation = None;
self.moved = 0;
self.locked = None;
return Some(SwipeDetectMsg::Trigger(locked));
}
if finalize {
if !animation_disabled() {
ctx.request_anim_frame();
ctx.request_paint();
let done = self.moved as f32 / Self::PROGRESS_MAX as f32;
let ratio = 1.0 - done;
let duration = config
.duration(self.locked.unwrap())
.unwrap_or(Duration::from_millis(Self::DURATION_MS));
let duration = ((duration.to_millis() as f32 * ratio) as u32).max(0);
self.final_animation = Some(Animation::new(
self.moved,
final_value,
Duration::from_millis(duration),
Instant::now(),
));
} else {
ctx.request_anim_frame();
ctx.request_paint();
self.final_animation = None;
self.moved = 0;
let locked = self.locked.take();
if final_value != 0 {
return Some(SwipeDetectMsg::Trigger(locked.unwrap()));
}
}
}
return None;
}
}
(Event::Timer(EventCtx::ANIM_FRAME_TIMER), _) => {
if self.locked.is_some() {
let mut finish = false;
let res = if let Some(animation) = &self.final_animation {
if animation.finished(Instant::now()) {
finish = true;
if animation.to != 0 {
Some(SwipeDetectMsg::Trigger(self.locked.unwrap()))
} else {
Some(SwipeDetectMsg::Move(self.locked.unwrap(), 0))
}
} else {
ctx.request_anim_frame();
ctx.request_paint();
if animation_disabled() {
None
} else {
Some(SwipeDetectMsg::Move(
self.locked.unwrap(),
animation.value(Instant::now()),
))
}
}
} else {
None
};
if finish {
self.locked = None;
ctx.request_anim_frame();
ctx.request_paint();
self.final_animation = None;
self.moved = 0;
}
return res;
}
}
_ => {
// Do nothing.
}
}
None
}
}

View File

@ -32,7 +32,7 @@ impl Component for Timeout {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
// Set up timer.
Event::Attach => {
Event::Attach(_) => {
self.timer = Some(ctx.request_timer(Duration::from_millis(self.time_ms)));
None
}

View File

@ -1,6 +1,9 @@
use crate::{error, ui::geometry::Point};
use core::convert::TryInto;
#[cfg(feature = "touch")]
use crate::ui::component::SwipeDirection;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum PhysicalButton {
Left,
@ -65,3 +68,10 @@ pub enum USBEvent {
/// USB host has connected/disconnected.
Connected(bool),
}
#[cfg(feature = "touch")]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum SwipeEvent {
Move(SwipeDirection, i16),
End(SwipeDirection),
}

View File

@ -1,50 +1,10 @@
use crate::ui::component::{EventCtx, SwipeDirection};
use crate::ui::component::{swipe_detect::SwipeConfig, SwipeDirection};
use num_traits::ToPrimitive;
/// Component must implement this trait in order to be part of swipe-based flow.
///
/// Default implementation ignores every swipe.
pub trait Swipable<T> {
/// Attempt a swipe. Return `Ignored` if the component in its current state
/// doesn't accept a swipe in that direction. Return `Animating` if
/// component accepted the swipe and started a transition animation. The
/// `Return(x)` variant indicates that the current flow should be terminated
/// with the result `x`.
fn swipe_start(
&mut self,
_ctx: &mut EventCtx,
_direction: SwipeDirection,
) -> SwipableResult<T> {
SwipableResult::Ignored
}
pub trait Swipable {
fn get_swipe_config(&self) -> SwipeConfig;
/// 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
}
}
pub enum SwipableResult<T> {
Ignored,
Animating,
Return(T),
}
impl<T> SwipableResult<T> {
pub fn map<U>(self, func: impl FnOnce(T) -> Option<U>) -> SwipableResult<U> {
match self {
SwipableResult::Ignored => SwipableResult::Ignored,
SwipableResult::Animating => SwipableResult::Animating,
SwipableResult::Return(x) => {
if let Some(res) = func(x) {
SwipableResult::Return(res)
} else {
SwipableResult::Ignored
}
}
}
}
fn get_internal_page_count(&self) -> usize;
}
/// Component::Msg for component parts of a flow. Converting results of

View File

@ -3,7 +3,7 @@ pub mod page;
mod store;
mod swipe;
pub use base::{FlowMsg, FlowState, Swipable, SwipableResult};
pub use page::{IgnoreSwipe, SwipePage};
pub use base::{FlowMsg, FlowState, Swipable};
pub use page::SwipePage;
pub use store::{flow_store, FlowStore};
pub use swipe::SwipeFlow;

View File

@ -1,25 +1,10 @@
use crate::{
micropython::gc::Gc,
time::Instant,
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
flow::base::{Swipable, SwipableResult},
geometry::{Axis, Rect},
shape::Renderer,
util,
},
use crate::ui::{
component::{Component, Event, EventCtx, Paginate, SwipeDirection},
event::SwipeEvent,
geometry::{Axis, Rect},
shape::Renderer,
};
pub struct Transition<T> {
/// Clone of the component before page change.
cloned: Gc<T>,
/// Animation progress.
animation: Animation<f32>,
/// Direction of the slide animation.
direction: SwipeDirection,
}
/// Allows any implementor of `Paginate` to be part of `Swipable` UI flow.
/// Renders sliding animation when changing pages.
pub struct SwipePage<T> {
@ -28,7 +13,6 @@ pub struct SwipePage<T> {
axis: Axis,
pages: usize,
current: usize,
transition: Option<Transition<T>>,
}
impl<T: Component + Paginate + Clone> SwipePage<T> {
@ -39,7 +23,6 @@ impl<T: Component + Paginate + Clone> SwipePage<T> {
axis: Axis::Vertical,
pages: 1,
current: 0,
transition: None,
}
}
@ -50,38 +33,8 @@ impl<T: Component + Paginate + Clone> SwipePage<T> {
axis: Axis::Horizontal,
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>,
) {
target.in_clip(self.bounds, &|target| {
util::render_slide(
|target| transition.cloned.render(target),
|target| self.inner.render(target),
transition.animation.value(Instant::now()),
transition.direction,
target,
);
});
}
}
impl<T: Component + Paginate + Clone> Component for SwipePage<T> {
@ -95,14 +48,33 @@ impl<T: Component + Paginate + Clone> Component for SwipePage<T> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.pages);
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
if let Event::Swipe(SwipeEvent::End(direction)) = event {
match (self.axis, direction) {
(Axis::Vertical, SwipeDirection::Up) => {
self.current = (self.current + 1).min(self.pages - 1);
self.inner.change_page(self.current);
ctx.request_paint();
}
(Axis::Vertical, SwipeDirection::Down) => {
self.current = self.current.saturating_sub(1);
self.inner.change_page(self.current);
ctx.request_paint();
}
(Axis::Horizontal, SwipeDirection::Left) => {
self.current = (self.current + 1).min(self.pages - 1);
self.inner.change_page(self.current);
ctx.request_paint();
}
(Axis::Horizontal, SwipeDirection::Right) => {
self.current = self.current.saturating_sub(1);
self.inner.change_page(self.current);
ctx.request_paint();
}
_ => {}
}
return None;
}
self.inner.event(ctx, event)
}
@ -111,62 +83,10 @@ impl<T: Component + Paginate + Clone> 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 + Clone> Swipable<T::Msg> for SwipePage<T> {
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: SwipeDirection,
) -> SwipableResult<T::Msg> {
match (self.axis, direction) {
// Wrong direction
(Axis::Horizontal, SwipeDirection::Up | SwipeDirection::Down) => {
return SwipableResult::Ignored
}
(Axis::Vertical, SwipeDirection::Left | SwipeDirection::Right) => {
return SwipableResult::Ignored
}
// Begin
(_, SwipeDirection::Right | SwipeDirection::Down) if self.current == 0 => {
return SwipableResult::Ignored
}
// End
(_, SwipeDirection::Left | SwipeDirection::Up) if self.current + 1 >= self.pages => {
return SwipableResult::Ignored;
}
_ => {}
};
self.current = match direction {
SwipeDirection::Left | SwipeDirection::Up => (self.current + 1).min(self.pages - 1),
SwipeDirection::Right | SwipeDirection::Down => self.current.saturating_sub(1),
};
if util::animation_disabled() {
self.inner.change_page(self.current);
ctx.request_paint();
return SwipableResult::Animating;
}
self.transition = Some(Transition {
cloned: unwrap!(Gc::new(self.inner.clone())),
animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION_MS, Instant::now()),
direction,
});
self.inner.change_page(self.current);
ctx.request_anim_frame();
ctx.request_paint();
SwipableResult::Animating
}
fn swipe_finished(&self) -> bool {
self.transition.is_none()
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SwipePage<T>
where
@ -176,44 +96,3 @@ where
self.inner.trace(t)
}
}
/// Make any component swipable by ignoring all swipe events.
pub struct IgnoreSwipe<T>(T);
impl<T> IgnoreSwipe<T> {
pub fn new(inner: T) -> Self {
IgnoreSwipe(inner)
}
}
impl<T: Component> Component for IgnoreSwipe<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
self.0.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.0.event(ctx, event)
}
fn paint(&mut self) {
self.0.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.0.render(target)
}
}
impl<T: Component> Swipable<T::Msg> for IgnoreSwipe<T> {}
#[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)
}
}

View File

@ -3,17 +3,21 @@ use crate::{
maybe_trace::MaybeTrace,
ui::{
component::{Component, Event, EventCtx},
flow::base::{FlowMsg, Swipable},
flow::base::FlowMsg,
geometry::Rect,
shape::Renderer,
},
};
use crate::micropython::gc::Gc;
use crate::{
micropython::gc::Gc,
ui::{component::swipe_detect::SwipeConfig, flow::Swipable},
};
/// `FlowStore` is essentially `Vec<Gc<dyn Component + Swipable>>` except that
/// `trait Component` is not object-safe so it ends up being a kind of
/// `FlowStore` is essentially `Vec<Gc<dyn Component + SimpleSwipable>>` except
/// that `trait Component` is not object-safe so it ends up being a kind of
/// recursively-defined tuple.
/// Implementors are something like the V in MVC.
pub trait FlowStore {
/// Call `Component::place` on all elements.
fn place(&mut self, bounds: Rect) -> Rect;
@ -28,15 +32,15 @@ pub trait FlowStore {
/// 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<FlowMsg>) -> T,
) -> T;
/// Forward `SimpleSwipable` methods to i-th element.
fn map_swipable<T>(&mut self, i: usize, func: impl FnOnce(&mut dyn Swipable) -> T) -> T;
fn get_swipe_config(&self, i: usize) -> SwipeConfig;
fn get_internal_page_count(&mut self, i: usize) -> usize;
/// Add a Component to the end of a `FlowStore`.
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable<FlowMsg>>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: E,
) -> Result<impl FlowStore, error::Error>
@ -62,7 +66,7 @@ impl FlowStore for FlowEmpty {
panic!()
}
fn render<'s>(&'s self, _i: usize, _target: &mut impl Renderer<'s>) {
fn render<'s>(&self, _i: usize, _target: &mut impl Renderer<'s>) {
panic!()
}
@ -71,29 +75,31 @@ impl FlowStore for FlowEmpty {
panic!()
}
fn map_swipable<T>(
&mut self,
_i: usize,
_func: impl FnOnce(&mut dyn Swipable<FlowMsg>) -> T,
) -> T {
fn map_swipable<T>(&mut self, _i: usize, _func: impl FnOnce(&mut dyn Swipable) -> T) -> T {
panic!()
}
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable<FlowMsg>>(
fn add<E: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: E,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized,
{
Ok(FlowComponent {
Ok(FlowComponent2 {
elem: Gc::new(elem)?,
next: Self,
})
}
fn get_swipe_config(&self, _i: usize) -> SwipeConfig {
SwipeConfig::new()
}
fn get_internal_page_count(&mut self, _i: usize) -> usize {
1
}
}
struct FlowComponent<E: Component<Msg = FlowMsg>, P> {
struct FlowComponent2<E: Component<Msg = FlowMsg>, P> {
/// Component allocated on micropython heap.
pub elem: Gc<E>,
@ -101,7 +107,7 @@ struct FlowComponent<E: Component<Msg = FlowMsg>, P> {
pub next: P,
}
impl<E: Component<Msg = FlowMsg>, P> FlowComponent<E, P> {
impl<E: Component<Msg = FlowMsg>, P> FlowComponent2<E, P> {
fn as_ref(&self) -> &E {
&self.elem
}
@ -113,9 +119,9 @@ impl<E: Component<Msg = FlowMsg>, P> FlowComponent<E, P> {
}
}
impl<E, P> FlowStore for FlowComponent<E, P>
impl<E, P> FlowStore for FlowComponent2<E, P>
where
E: Component<Msg = FlowMsg> + MaybeTrace + Swipable<FlowMsg>,
E: Component<Msg = FlowMsg> + MaybeTrace + Swipable,
P: FlowStore,
{
fn place(&mut self, bounds: Rect) -> Rect {
@ -149,11 +155,7 @@ where
}
}
fn map_swipable<T>(
&mut self,
i: usize,
func: impl FnOnce(&mut dyn Swipable<FlowMsg>) -> T,
) -> 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 {
@ -161,16 +163,28 @@ where
}
}
fn add<F: Component<Msg = FlowMsg> + MaybeTrace + Swipable<FlowMsg>>(
fn add<F: Component<Msg = FlowMsg> + MaybeTrace + Swipable>(
self,
elem: F,
) -> Result<impl FlowStore, error::Error>
where
Self: Sized,
{
Ok(FlowComponent {
Ok(FlowComponent2 {
elem: self.elem,
next: self.next.add(elem)?,
})
}
fn get_swipe_config(&self, i: usize) -> SwipeConfig {
if i == 0 {
self.as_ref().get_swipe_config()
} else {
self.next.get_swipe_config(i - 1)
}
}
fn get_internal_page_count(&mut self, i: usize) -> usize {
self.map_swipable(i, |swipable| swipable.get_internal_page_count())
}
}

View File

@ -1,13 +1,15 @@
use crate::{
error,
time::Instant,
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Swipe, SwipeDirection},
flow::{base::Decision, FlowMsg, FlowState, FlowStore, SwipableResult},
component::{
base::AttachType, swipe_detect::SwipeSettings, Component, Event, EventCtx, SwipeDetect,
SwipeDetectMsg, SwipeDirection,
},
event::SwipeEvent,
flow::{base::Decision, FlowMsg, FlowState, FlowStore},
geometry::Rect,
shape::Renderer,
util,
util::animation_disabled,
},
};
@ -22,61 +24,54 @@ pub struct SwipeFlow<Q, S> {
state: Q,
/// FlowStore with all screens/components.
store: S,
/// `Transition::None` when state transition animation is in not progress.
transition: Transition<Q>,
/// Swipe detector.
swipe: Swipe,
}
enum Transition<Q> {
/// SwipeFlow is performing transition between different states.
External {
/// State we are transitioning _from_.
prev_state: Q,
/// Animation progress.
animation: Animation<f32>,
/// Direction of the slide animation.
direction: SwipeDirection,
},
/// Transition runs in child component, we forward events and wait.
Internal,
/// No transition.
None,
swipe: SwipeDetect,
/// Swipe allowed
allow_swipe: bool,
/// Current internal state
internal_state: u16,
/// Internal pages count
internal_pages: u16,
/// If triggering swipe by event, make this decision instead of default
/// after the swipe.
decision_override: Option<Decision<Q>>,
}
impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
pub fn new(init: Q, store: S) -> Result<Self, error::Error> {
Ok(Self {
state: init,
swipe: SwipeDetect::new(),
store,
transition: Transition::None,
swipe: Swipe::new().down().up().left().right(),
allow_swipe: true,
internal_state: 0,
internal_pages: 1,
decision_override: None,
})
}
fn goto(&mut self, ctx: &mut EventCtx, direction: SwipeDirection, state: Q) {
if util::animation_disabled() {
if state == self.state {
assert!(self
.store
.map_swipable(state.index(), |s| s.swipe_finished()));
}
self.state = state;
self.store.event(state.index(), ctx, Event::Attach);
ctx.request_paint();
return;
}
if state == self.state {
self.transition = Transition::Internal;
return;
}
self.transition = Transition::External {
prev_state: self.state,
animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION_MS, Instant::now()),
direction,
};
self.state = state;
ctx.request_anim_frame();
self.swipe = SwipeDetect::new();
self.allow_swipe = true;
self.store.event(
state.index(),
ctx,
Event::Attach(AttachType::Swipe(direction)),
);
self.internal_pages = self.store.get_internal_page_count(state.index()) as u16;
match direction {
SwipeDirection::Up => {
self.internal_state = 0;
}
SwipeDirection::Down => {
self.internal_state = self.internal_pages.saturating_sub(1);
}
_ => {}
}
ctx.request_paint();
}
@ -84,69 +79,17 @@ impl<Q: FlowState, S: FlowStore> SwipeFlow<Q, S> {
self.store.render(state.index(), target)
}
fn render_transition<'s>(
&'s self,
prev_state: &Q,
animation: &Animation<f32>,
direction: &SwipeDirection,
target: &mut impl Renderer<'s>,
) {
util::render_slide(
|target| self.render_state(*prev_state, target),
|target| self.render_state(self.state, target),
animation.value(Instant::now()),
*direction,
target,
);
}
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
}
}
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;
}
result
}
fn handle_swipe_child(&mut self, ctx: &mut EventCtx, direction: SwipeDirection) -> Decision<Q> {
let i = self.state.index();
match self
.store
.map_swipable(i, |s| s.swipe_start(ctx, direction))
{
SwipableResult::Ignored => Decision::Nothing,
SwipableResult::Animating => Decision::Goto(self.state, direction),
SwipableResult::Return(x) => Decision::Return(x),
}
fn handle_swipe_child(
&mut self,
_ctx: &mut EventCtx,
direction: SwipeDirection,
) -> Decision<Q> {
self.state.handle_swipe(direction)
}
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 {
@ -159,44 +102,146 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
type Msg = FlowMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.swipe.place(bounds);
self.store.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if !matches!(self.transition, Transition::None) {
return self.handle_transition(ctx, event);
}
let mut decision: Decision<Q> = Decision::Nothing;
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));
let mut attach = false;
let e = if self.allow_swipe {
let mut config = self.store.get_swipe_config(self.state.index());
self.internal_pages = self.store.get_internal_page_count(self.state.index()) as u16;
// add additional swipe directions if there are more internal pages
// todo can we get internal settings from config somehow?
// might wanna different duration or something
if config.vertical_pages && self.internal_state > 0 {
config = config.with_swipe(SwipeDirection::Down, SwipeSettings::default())
}
if config.horizontal_pages && self.internal_state > 0 {
config = config.with_swipe(SwipeDirection::Right, SwipeSettings::default())
}
if config.vertical_pages && self.internal_state < self.internal_pages - 1 {
config = config.with_swipe(SwipeDirection::Up, SwipeSettings::default())
}
if config.horizontal_pages && self.internal_state < self.internal_pages - 1 {
config = config.with_swipe(SwipeDirection::Left, SwipeSettings::default())
}
match self.swipe.event(ctx, event, config.clone()) {
Some(SwipeDetectMsg::Trigger(dir)) => {
if let Some(override_decision) = self.decision_override.take() {
decision = override_decision;
} else {
decision = self.handle_swipe_child(ctx, dir);
}
let states_num = self.internal_pages;
if states_num > 0 {
if config.has_horizontal_pages() {
let current_state = self.internal_state;
if dir == SwipeDirection::Left && current_state < states_num - 1 {
self.internal_state += 1;
decision = Decision::Nothing;
attach = true;
} else if dir == SwipeDirection::Right && current_state > 0 {
self.internal_state -= 1;
decision = Decision::Nothing;
attach = true;
}
}
if config.has_vertical_pages() {
let current_state = self.internal_state;
if dir == SwipeDirection::Up && current_state < states_num - 1 {
self.internal_state += 1;
decision = Decision::Nothing;
attach = true;
} else if dir == SwipeDirection::Down && current_state > 0 {
self.internal_state -= 1;
decision = Decision::Nothing;
attach = true;
}
}
}
Some(Event::Swipe(SwipeEvent::End(dir)))
}
Some(SwipeDetectMsg::Move(dir, progress)) => {
Some(Event::Swipe(SwipeEvent::Move(dir, progress)))
}
_ => Some(event),
}
} else {
Some(event)
};
if let Some(e) = e {
match decision {
Decision::Nothing => {
decision = self.handle_event_child(ctx, e);
// when doing internal transition, pass attach event to the child after sending
// swipe end.
if attach {
if let Event::Swipe(SwipeEvent::End(dir)) = e {
self.store.event(
self.state.index(),
ctx,
Event::Attach(AttachType::Swipe(dir)),
);
}
}
if ctx.disable_swipe_requested() {
self.swipe.reset();
self.allow_swipe = false;
}
if ctx.enable_swipe_requested() {
self.swipe.reset();
self.allow_swipe = true;
};
let config = self.store.get_swipe_config(self.state.index());
if let Decision::Goto(_, direction) = decision {
if config.is_allowed(direction) {
if !animation_disabled() {
self.swipe.trigger(ctx, direction, config);
self.decision_override = Some(decision);
decision = Decision::Nothing;
}
self.allow_swipe = true;
}
}
}
_ => {
//ignore message, we are already transitioning
self.store.event(self.state.index(), ctx, event);
}
}
}
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),
Decision::Return(msg) => {
self.swipe.reset();
self.allow_swipe = true;
Some(msg)
}
_ => None,
}
}
fn paint(&mut self) {}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
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),
}
self.render_state(self.state, target);
}
}

View File

@ -25,6 +25,7 @@ use crate::{
},
};
use crate::ui::component::base::AttachType;
#[cfg(feature = "new_rendering")]
use crate::ui::{display::Color, shape::render_on_display};
@ -379,7 +380,7 @@ extern "C" fn ui_layout_attach_timer_fn(this: Obj, timer_fn: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;
this.obj_set_timer_fn(timer_fn);
let msg = this.obj_event(Event::Attach)?;
let msg = this.obj_event(Event::Attach(AttachType::Initial))?;
assert!(msg == Obj::const_none());
Ok(Obj::const_none())
};

View File

@ -6,9 +6,12 @@ use crate::{
translations::TR,
ui::{
component::{
swipe_detect::{SwipeConfig, SwipeSettings},
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
Component, Event, EventCtx, Paginate,
Component, Event, EventCtx, Paginate, SwipeDirection,
},
event::SwipeEvent,
flow::Swipable,
geometry::Rect,
shape::Renderer,
},
@ -56,12 +59,15 @@ impl AddressDetails {
}
let result = Self {
details: Frame::left_aligned(details_title, para.into_paragraphs())
.with_cancel_button(),
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.with_horizontal_pages(),
xpub_view: Frame::left_aligned(
" \n ".into(),
Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, "").into_paragraphs(),
)
.with_cancel_button(),
.with_cancel_button()
.with_horizontal_pages(),
xpubs: Vec::new(),
xpub_page_count: Vec::new(),
current_page: 0,
@ -113,8 +119,7 @@ impl AddressDetails {
impl Paginate for AddressDetails {
fn page_count(&mut self) -> usize {
let total_xpub_pages: u8 = self.xpub_page_count.iter().copied().sum();
1usize.saturating_add(total_xpub_pages.into())
self.get_internal_page_count()
}
fn change_page(&mut self, to_page: usize) {
@ -144,6 +149,22 @@ impl Component for AddressDetails {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.page_count());
match event {
Event::Swipe(SwipeEvent::End(SwipeDirection::Right)) => {
let to_page = self.current_page.saturating_sub(1);
self.change_page(to_page);
}
Event::Swipe(SwipeEvent::End(SwipeDirection::Left)) => {
let to_page = self
.current_page
.saturating_add(1)
.min(self.page_count() - 1);
self.change_page(to_page);
}
_ => {}
}
let msg = match self.current_page {
0 => self.details.event(ctx, event),
_ => self.xpub_view.event(ctx, event),
@ -177,6 +198,20 @@ impl Component for AddressDetails {
}
}
impl Swipable for AddressDetails {
fn get_swipe_config(&self) -> SwipeConfig {
match self.current_page {
0 => self.details.get_swipe_config(),
_ => self.xpub_view.get_swipe_config(),
}
}
fn get_internal_page_count(&self) -> usize {
let total_xpub_pages: u8 = self.xpub_page_count.iter().copied().sum();
1usize.saturating_add(total_xpub_pages.into())
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for AddressDetails {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -350,6 +350,7 @@ impl Component for Button {
if let Some(duration) = self.long_press {
self.long_timer = Some(ctx.request_timer(duration));
}
ctx.disable_swipe();
return Some(ButtonMsg::Pressed);
}
}
@ -360,6 +361,7 @@ impl Component for Button {
State::Pressed if !touch_area.contains(pos) => {
// Touch is leaving our area, transform to `Released` state.
self.set(ctx, State::Released);
ctx.enable_swipe();
return Some(ButtonMsg::Released);
}
_ => {
@ -375,11 +377,13 @@ impl Component for Button {
State::Pressed if touch_area.contains(pos) => {
// Touch finished in our area, we got clicked.
self.set(ctx, State::Initial);
ctx.enable_swipe();
return Some(ButtonMsg::Clicked);
}
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
ctx.enable_swipe();
self.long_timer = None;
}
}

View File

@ -92,7 +92,7 @@ where
_ if animation_disabled() => {
return None;
}
Event::Attach if self.indeterminate => {
Event::Attach(_) if self.indeterminate => {
ctx.request_anim_frame();
}
Event::Timer(EventCtx::ANIM_FRAME_TIMER) => {

View File

@ -228,6 +228,3 @@ where
t.child("controls", &self.controls);
}
}
#[cfg(feature = "micropython")]
impl<U: Component> crate::ui::flow::Swipable<U::Msg> for IconDialog<U> {}

View File

@ -1,9 +1,13 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Never},
component::{text::TextStyle, Component, Event, EventCtx, Never, SwipeDirection},
display::Color,
event::SwipeEvent,
geometry::{Alignment, Offset, Rect},
lerp::Lerp,
model_mercury::theme,
shape,
shape::{Renderer, Text},
},
};
@ -21,6 +25,10 @@ pub struct Footer<'a> {
text_description: Option<TString<'a>>,
style_instruction: &'static TextStyle,
style_description: &'static TextStyle,
swipe_allow_up: bool,
swipe_allow_down: bool,
progress: i16,
dir: SwipeDirection,
}
impl<'a> Footer<'a> {
@ -36,6 +44,10 @@ impl<'a> Footer<'a> {
text_description: None,
style_instruction: &theme::TEXT_SUB_GREY,
style_description: &theme::TEXT_SUB_GREY_LIGHT,
swipe_allow_down: false,
swipe_allow_up: false,
progress: 0,
dir: SwipeDirection::Up,
}
}
@ -73,6 +85,19 @@ impl<'a> Footer<'a> {
Footer::HEIGHT_SIMPLE
}
}
pub fn with_swipe_up(self) -> Self {
Self {
swipe_allow_up: true,
..self
}
}
pub fn with_swipe_down(self) -> Self {
Self {
swipe_allow_down: true,
..self
}
}
}
impl<'a> Component for Footer<'a> {
@ -85,7 +110,29 @@ impl<'a> Component for Footer<'a> {
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Attach(_) => {
self.progress = 0;
}
Event::Swipe(SwipeEvent::Move(dir, progress)) => match dir {
SwipeDirection::Up => {
if self.swipe_allow_up {
self.progress = progress;
self.dir = dir;
}
}
SwipeDirection::Down => {
if self.swipe_allow_down {
self.progress = progress;
self.dir = dir;
}
}
_ => {}
},
_ => {}
};
None
}
@ -95,41 +142,68 @@ impl<'a> Component for Footer<'a> {
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// show description only if there is space for it
if self.area.height() == Footer::HEIGHT_DEFAULT {
if let Some(description) = self.text_description {
let area_description = self.area.split_top(Footer::HEIGHT_SIMPLE).0;
let text_description_font_descent = self
.style_description
.text_font
.visible_text_height_ex("Ay")
.1;
let text_description_baseline =
area_description.bottom_center() - Offset::y(text_description_font_descent);
let progress = self.progress as f32 / 1000.0;
description.map(|t| {
Text::new(text_description_baseline, t)
.with_font(self.style_description.text_font)
.with_fg(self.style_description.text_color)
.with_align(Alignment::Center)
.render(target);
});
let shift = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
let offset = i16::lerp(0, 20, shift.eval(progress));
let mask = u8::lerp(0, 255, shift.eval(progress));
let offset = match self.dir {
SwipeDirection::Up => Offset::y(-offset),
SwipeDirection::Down => Offset::y(3 * offset),
_ => Offset::zero(),
};
target.with_origin(offset, &|target| {
// show description only if there is space for it
if self.area.height() == Footer::HEIGHT_DEFAULT {
if let Some(description) = self.text_description {
let area_description = self.area.split_top(Footer::HEIGHT_SIMPLE).0;
let text_description_font_descent = self
.style_description
.text_font
.visible_text_height_ex("Ay")
.1;
let text_description_baseline =
area_description.bottom_center() - Offset::y(text_description_font_descent);
description.map(|t| {
Text::new(text_description_baseline, t)
.with_font(self.style_description.text_font)
.with_fg(self.style_description.text_color)
.with_align(Alignment::Center)
.render(target);
});
}
}
}
let area_instruction = self.area.split_bottom(Footer::HEIGHT_SIMPLE).1;
let text_instruction_font_descent = self
.style_instruction
.text_font
.visible_text_height_ex("Ay")
.1;
let text_instruction_baseline =
area_instruction.bottom_center() - Offset::y(text_instruction_font_descent);
self.text_instruction.map(|t| {
Text::new(text_instruction_baseline, t)
.with_font(self.style_instruction.text_font)
.with_fg(self.style_instruction.text_color)
.with_align(Alignment::Center)
let area_instruction = self.area.split_bottom(Footer::HEIGHT_SIMPLE).1;
let text_instruction_font_descent = self
.style_instruction
.text_font
.visible_text_height_ex("Ay")
.1;
let text_instruction_baseline =
area_instruction.bottom_center() - Offset::y(text_instruction_font_descent);
self.text_instruction.map(|t| {
Text::new(text_instruction_baseline, t)
.with_font(self.style_instruction.text_font)
.with_fg(self.style_instruction.text_color)
.with_align(Alignment::Center)
.render(target);
});
shape::Bar::new(self.area)
.with_alpha(mask)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
});
}

View File

@ -2,11 +2,20 @@ use crate::{
strutil::TString,
ui::{
component::{
base::ComponentExt, label::Label, text::TextStyle, Child, Component, Event, EventCtx,
base::ComponentExt,
label::Label,
swipe_detect::{SwipeConfig, SwipeSettings},
text::TextStyle,
Child, Component, Event,
Event::Swipe,
EventCtx, SwipeDetect, SwipeDirection,
},
display::Icon,
geometry::{Alignment, Insets, Rect},
event::SwipeEvent,
geometry::{Alignment, Insets, Point, Rect},
lerp::Lerp,
model_mercury::theme::TITLE_HEIGHT,
shape,
shape::Renderer,
},
};
@ -18,13 +27,17 @@ const BUTTON_EXPAND_BORDER: i16 = 32;
#[derive(Clone)]
pub struct Frame<T> {
border: Insets,
bounds: Rect,
title: Child<Label<'static>>,
subtitle: Option<Child<Label<'static>>>,
button: Option<Child<Button>>,
button_msg: CancelInfoConfirmMsg,
content: Child<T>,
footer: Option<Footer<'static>>,
overlapping_content: bool,
swipe: SwipeConfig,
internal_page_cnt: usize,
progress: i16,
dir: SwipeDirection,
}
pub enum FrameMsg<T> {
@ -41,13 +54,17 @@ where
title: Child::new(
Label::new(title, alignment, theme::label_title_main()).vertically_centered(),
),
bounds: Rect::zero(),
subtitle: None,
border: theme::borders(),
button: None,
button_msg: CancelInfoConfirmMsg::Cancelled,
content: Child::new(content),
footer: None,
overlapping_content: false,
swipe: SwipeConfig::new(),
internal_page_cnt: 1,
progress: 0,
dir: SwipeDirection::Up,
}
}
@ -155,6 +172,31 @@ where
res
})
}
pub fn with_swipe(self, dir: SwipeDirection, settings: SwipeSettings) -> Self {
Self {
footer: self.footer.map(|f| match dir {
SwipeDirection::Up => f.with_swipe_up(),
SwipeDirection::Down => f.with_swipe_down(),
_ => f,
}),
swipe: self.swipe.with_swipe(dir, settings),
..self
}
}
pub fn with_horizontal_pages(self) -> Self {
Self {
swipe: self.swipe.with_horizontal_pages(),
..self
}
}
pub fn with_vertical_pages(self) -> Self {
Self {
swipe: self.swipe.with_vertical_pages(),
..self
}
}
}
impl<T> Component for Frame<T>
@ -164,6 +206,8 @@ where
type Msg = FrameMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.bounds = bounds;
let (mut header_area, mut content_area) = bounds.split_top(TITLE_HEIGHT);
content_area = content_area.inset(Insets::top(theme::SPACING));
header_area = header_area.inset(Insets::sides(theme::SPACING));
@ -190,18 +234,37 @@ where
footer.place(footer_area);
content_area = remaining;
}
if self.overlapping_content {
self.content.place(bounds);
} else {
self.content.place(content_area);
}
self.content.place(content_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Attach(_) = event {
self.progress = 0;
}
if let Swipe(SwipeEvent::Move(dir, progress)) = event {
if self.swipe.is_allowed(dir) {
match dir {
SwipeDirection::Left | SwipeDirection::Right => {
self.progress = progress;
self.dir = dir;
}
_ => {}
}
}
}
self.title.event(ctx, event);
self.subtitle.event(ctx, event);
self.footer.event(ctx, event);
let msg = self.content.event(ctx, event).map(FrameMsg::Content);
if let Some(count) = ctx.page_count() {
self.internal_page_cnt = count;
}
if msg.is_some() {
return msg;
}
@ -224,6 +287,32 @@ where
self.button.render(target);
self.footer.render(target);
self.content.render(target);
if self.progress > 0 {
match self.dir {
SwipeDirection::Left => {
let shift = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Circ,
1.0,
pareen::constant(1.0),
);
let p = Point::lerp(
self.bounds.top_right(),
self.bounds.top_left(),
shift.eval(self.progress as f32 / SwipeDetect::PROGRESS_MAX as f32),
);
shape::Bar::new(Rect::new(p, self.bounds.bottom_right()))
.with_fg(theme::BLACK)
.with_bg(theme::BLACK)
.render(target);
}
SwipeDirection::Right => {}
_ => {}
}
}
}
#[cfg(feature = "ui_bounds")]
@ -236,6 +325,17 @@ where
}
}
#[cfg(feature = "micropython")]
impl<T> crate::ui::flow::Swipable for Frame<T> {
fn get_swipe_config(&self) -> SwipeConfig {
self.swipe
}
fn get_internal_page_count(&self) -> usize {
self.internal_page_cnt
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Frame<T>
where
@ -256,22 +356,3 @@ where
}
}
}
#[cfg(feature = "micropython")]
impl<T> crate::ui::flow::Swipable<FrameMsg<T::Msg>> for Frame<T>
where
T: Component + crate::ui::flow::Swipable<T::Msg>,
{
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: crate::ui::component::SwipeDirection,
) -> crate::ui::flow::SwipableResult<FrameMsg<T::Msg>> {
self.update_content(ctx, |ctx, inner| inner.swipe_start(ctx, direction))
.map(|x| Some(FrameMsg::Content(x)))
}
fn swipe_finished(&self) -> bool {
self.inner().swipe_finished()
}
}

View File

@ -36,6 +36,10 @@ impl HoldToConfirmAnim {
const DURATION_MS: u32 = 2200;
pub fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(Self::DURATION_MS))
}
@ -332,9 +336,6 @@ impl Component for HoldToConfirm {
}
}
#[cfg(feature = "micropython")]
impl crate::ui::flow::Swipable<()> for HoldToConfirm {}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for HoldToConfirm {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -328,12 +328,12 @@ impl Component for Lockscreen {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if event == Event::Attach {
if let Event::Attach(_) = event {
ctx.request_anim_frame();
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if (!animation_disabled()) {
if !animation_disabled() {
if !self.anim.timer.is_running() {
self.anim.timer.start();
}

View File

@ -186,7 +186,7 @@ impl Component for PinKeyboard<'_> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
// Set up timer to switch off warning prompt.
Event::Attach if self.major_warning.is_some() => {
Event::Attach(_) if self.major_warning.is_some() => {
self.warning_timer = Some(ctx.request_timer(Duration::from_secs(2)));
}
// Hide warning, show major prompt.

View File

@ -36,6 +36,8 @@ mod set_brightness;
mod share_words;
mod simple_page;
mod status_screen;
mod swipe_content;
#[cfg(feature = "translations")]
mod swipe_up_screen;
#[cfg(feature = "translations")]
mod tap_to_confirm;
@ -85,6 +87,8 @@ pub use set_brightness::SetBrightnessDialog;
pub use share_words::ShareWords;
pub use simple_page::SimplePage;
pub use status_screen::StatusScreen;
pub use swipe_content::SwipeContent;
#[cfg(feature = "translations")]
pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg};
#[cfg(feature = "translations")]
pub use tap_to_confirm::TapToConfirm;

View File

@ -9,7 +9,7 @@ use crate::{
Child, Component, Event, EventCtx, Pad, SwipeDirection,
},
display::Font,
flow::{Swipable, SwipableResult},
event::SwipeEvent,
geometry::{Alignment, Grid, Insets, Offset, Rect},
shape::{self, Renderer},
},
@ -89,6 +89,10 @@ where
if let Some(NumberInputMsg::Changed(i)) = self.input.event(ctx, event) {
self.update_text(ctx, i);
}
if let Event::Swipe(SwipeEvent::End(SwipeDirection::Up)) = event {
return Some(NumberInputDialogMsg(self.input.inner().value));
}
self.paragraphs.event(ctx, event);
None
}
@ -111,26 +115,6 @@ where
}
}
impl<F> Swipable<NumberInputDialogMsg> for NumberInputDialog<F>
where
F: Fn(u32) -> TString<'static>,
{
fn swipe_start(
&mut self,
_ctx: &mut EventCtx,
direction: SwipeDirection,
) -> SwipableResult<NumberInputDialogMsg> {
match direction {
SwipeDirection::Up => SwipableResult::Return(NumberInputDialogMsg(self.value())),
_ => SwipableResult::Ignored,
}
}
fn swipe_finished(&self) -> bool {
true
}
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for NumberInputDialog<F>
where

View File

@ -68,9 +68,6 @@ impl Component for PromptScreen {
}
}
#[cfg(feature = "micropython")]
impl crate::ui::flow::Swipable<()> for PromptScreen {}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PromptScreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -1,12 +1,12 @@
use super::theme;
use crate::{
strutil::TString,
time::{Duration, Instant},
time::Duration,
translations::TR,
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Never, SwipeDirection},
flow::{Swipable, SwipableResult},
event::SwipeEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
model_mercury::component::Footer,
shape,
@ -25,14 +25,15 @@ const ANIMATION_DURATION_MS: Duration = Duration::from_millis(166);
pub struct ShareWords<'a> {
area: Rect,
share_words: Vec<TString<'a>, MAX_WORDS>,
page_index: usize,
prev_index: usize,
page_index: i16,
next_index: i16,
/// Area reserved for a shown word from mnemonic/share
area_word: Rect,
/// `Some` when transition animation is in progress
animation: Option<Animation<f32>>,
/// Footer component for instructions and word counting
footer: Footer<'static>,
progress: i16,
}
impl<'a> ShareWords<'a> {
@ -43,10 +44,11 @@ impl<'a> ShareWords<'a> {
area: Rect::zero(),
share_words,
page_index: 0,
prev_index: 0,
next_index: 0,
area_word: Rect::zero(),
animation: None,
footer: Footer::new(TR::instructions__swipe_up),
progress: 0,
}
}
@ -55,12 +57,15 @@ impl<'a> ShareWords<'a> {
}
fn is_final_page(&self) -> bool {
self.page_index == self.share_words.len() - 1
self.page_index == self.share_words.len() as i16 - 1
}
fn render_word<'s>(&self, word_index: usize, target: &mut impl Renderer<'s>) {
fn render_word<'s>(&self, word_index: i16, target: &mut impl Renderer<'s>) {
// the share word
let word = self.share_words[word_index];
if word_index >= self.share_words.len() as _ || word_index < 0 {
return;
}
let word = self.share_words[word_index as usize];
let word_baseline = target.viewport().clip.center()
+ Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2);
word.map(|w| {
@ -93,16 +98,43 @@ impl<'a> Component for ShareWords<'a> {
self.area
}
fn event(&mut self, ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
// ctx.set_page_count(self.share_words.len());
if let Some(a) = &self.animation {
if a.finished(Instant::now()) {
self.animation = None;
} else {
ctx.request_anim_frame();
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.share_words.len());
match event {
Event::Attach(_) => {
self.progress = 0;
}
ctx.request_paint();
Event::Swipe(SwipeEvent::End(dir)) => match dir {
SwipeDirection::Up if !self.is_final_page() => {
self.progress = 0;
self.page_index = (self.page_index + 1).min(self.share_words.len() as i16 - 1);
ctx.request_paint();
}
SwipeDirection::Down if !self.is_first_page() => {
self.progress = 0;
self.page_index = self.page_index.saturating_sub(1);
ctx.request_paint();
}
_ => {}
},
Event::Swipe(SwipeEvent::Move(dir, progress)) => {
match dir {
SwipeDirection::Up => {
self.next_index = self.page_index + 1;
self.progress = progress;
}
SwipeDirection::Down => {
self.next_index = self.page_index - 1;
self.progress = progress;
}
_ => {}
}
ctx.request_paint();
}
_ => {}
}
None
}
@ -132,13 +164,20 @@ impl<'a> Component for ShareWords<'a> {
.with_fg(theme::GREY)
.render(target);
if let Some(animation) = &self.animation {
if self.progress > 0 {
target.in_clip(self.area_word, &|target| {
let progress = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
util::render_slide(
|target| self.render_word(self.prev_index, target),
|target| self.render_word(self.page_index, target),
animation.value(Instant::now()),
if self.prev_index < self.page_index {
|target| self.render_word(self.next_index, target),
progress.eval(self.progress as f32 / 1000.0),
if self.page_index < self.next_index {
SwipeDirection::Up
} else {
SwipeDirection::Down
@ -160,48 +199,11 @@ impl<'a> Component for ShareWords<'a> {
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
}
impl<'a> Swipable<Never> for ShareWords<'a> {
fn swipe_start(
&mut self,
ctx: &mut EventCtx,
direction: SwipeDirection,
) -> SwipableResult<Never> {
match direction {
SwipeDirection::Up if !self.is_final_page() => {
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 SwipableResult::Ignored,
};
if util::animation_disabled() {
ctx.request_paint();
return SwipableResult::Animating;
}
self.animation = Some(Animation::new(
0.0f32,
1.0f32,
ANIMATION_DURATION_MS,
Instant::now(),
));
ctx.request_anim_frame();
ctx.request_paint();
SwipableResult::Animating
}
fn swipe_finished(&self) -> bool {
self.animation.is_none()
}
}
#[cfg(feature = "ui_debug")]
impl<'a> crate::trace::Trace for ShareWords<'a> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ShareWords");
let word = &self.share_words[self.page_index];
let word = &self.share_words[self.page_index as usize];
let content =
word.map(|w| build_string!(50, inttostr!(self.page_index as u8 + 1), ". ", w, "\n"));
t.string("screen_content", content.as_str().into());

View File

@ -1,7 +1,7 @@
use crate::{
time::{Duration, Stopwatch},
ui::{
component::{Component, Event, EventCtx, Swipe, SwipeDirection, Timeout},
component::{Component, Event, EventCtx, Timeout},
constant::screen,
display::{Color, Icon},
geometry::{Alignment2D, Insets, Rect},
@ -23,13 +23,17 @@ struct StatusAnimation {
impl StatusAnimation {
pub fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(TIMEOUT_MS))
}
pub fn eval(&self) -> f32 {
if animation_disabled() {
return 1.0;
return TIMEOUT_MS as f32 / 1000.0;
}
self.timer.elapsed().to_millis() as f32 / 1000.0
}
@ -96,7 +100,7 @@ pub struct StatusScreen {
#[derive(Clone)]
enum DismissType {
SwipeUp(Swipe),
SwipeUp,
Timeout(Timeout),
}
@ -117,7 +121,7 @@ impl StatusScreen {
theme::ICON_SIMPLE_CHECKMARK,
theme::GREEN_LIME,
theme::GREEN_LIGHT,
DismissType::SwipeUp(Swipe::new().up()),
DismissType::SwipeUp,
)
}
@ -135,7 +139,7 @@ impl StatusScreen {
theme::ICON_SIMPLE_CHECKMARK,
theme::GREY_EXTRA_LIGHT,
theme::GREY_DARK,
DismissType::SwipeUp(Swipe::new().up()),
DismissType::SwipeUp,
)
}
@ -154,14 +158,11 @@ impl Component for StatusScreen {
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
if let DismissType::SwipeUp(swipe) = &mut self.dismiss_type {
swipe.place(bounds);
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Attach = event {
if let Event::Attach(_) = event {
self.anim.start();
ctx.request_paint();
ctx.request_anim_frame();
@ -173,17 +174,9 @@ impl Component for StatusScreen {
}
}
match self.dismiss_type {
DismissType::SwipeUp(ref mut swipe) => {
let swipe_dir = swipe.event(ctx, event);
if let Some(SwipeDirection::Up) = swipe_dir {
return Some(());
}
}
DismissType::Timeout(ref mut timeout) => {
if timeout.event(ctx, event).is_some() {
return Some(());
}
if let DismissType::Timeout(ref mut timeout) = self.dismiss_type {
if timeout.event(ctx, event).is_some() {
return Some(());
}
}

View File

@ -0,0 +1,258 @@
use crate::{
time::{Duration, Stopwatch},
ui::{
component::{base::AttachType, Component, Event, EventCtx, SwipeDirection},
display::Color,
event::SwipeEvent,
geometry::{Offset, Rect},
lerp::Lerp,
shape,
shape::Renderer,
util::animation_disabled,
},
};
#[derive(Default, Clone)]
struct AttachAnimation {
pub timer: Stopwatch,
}
impl AttachAnimation {
const DURATION_MS: u32 = 500;
pub fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(Self::DURATION_MS))
}
pub fn eval(&self) -> f32 {
if animation_disabled() {
return 1.0;
}
self.timer.elapsed().to_millis() as f32 / 1000.0
}
pub fn get_offset(&self, t: f32, attach_type: Option<AttachType>) -> Offset {
let value = pareen::constant(0.0).seq_ease_in(
0.0,
easer::functions::Linear,
Self::DURATION_MS as f32 / 1000.0,
pareen::constant(1.0),
);
match attach_type {
Some(AttachType::Swipe(dir)) => match dir {
SwipeDirection::Up => {
Offset::lerp(Offset::new(0, 20), Offset::zero(), value.eval(t))
}
SwipeDirection::Down => {
Offset::lerp(Offset::new(0, -20), Offset::zero(), value.eval(t))
}
_ => Offset::zero(),
},
_ => Offset::zero(),
}
}
pub fn get_opacity(&self, t: f32, attach_type: Option<AttachType>) -> u8 {
let value = pareen::constant(0.0).seq_ease_in_out(
0.0,
easer::functions::Cubic,
0.2,
pareen::constant(1.0),
);
match attach_type {
Some(AttachType::Swipe(SwipeDirection::Up))
| Some(AttachType::Swipe(SwipeDirection::Down)) => {}
_ => {
return 255;
}
}
u8::lerp(0, 255, value.eval(t))
}
pub fn start(&mut self) {
self.timer.start();
}
pub fn reset(&mut self) {
self.timer = Stopwatch::new_stopped();
}
}
pub struct SwipeContent<T> {
inner: T,
bounds: Rect,
progress: i16,
dir: SwipeDirection,
normal: Option<AttachType>,
attach_animation: AttachAnimation,
attach_type: Option<AttachType>,
}
impl<T: Component> SwipeContent<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
bounds: Rect::zero(),
progress: 0,
dir: SwipeDirection::Up,
normal: Some(AttachType::Swipe(SwipeDirection::Down)),
attach_animation: AttachAnimation::default(),
attach_type: None,
}
}
pub fn with_normal_attach(self, attach_type: Option<AttachType>) -> Self {
Self {
normal: attach_type,
..self
}
}
}
impl<T: Component> Component for SwipeContent<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bounds = self.inner.place(bounds);
self.bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Attach(attach_type) = event {
self.progress = 0;
if let AttachType::Initial = attach_type {
self.attach_type = self.normal;
} else {
self.attach_type = Some(attach_type);
}
self.attach_animation.reset();
ctx.request_anim_frame();
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if !animation_disabled() {
if !self.attach_animation.timer.is_running() {
self.attach_animation.timer.start();
}
if self.attach_animation.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
}
}
}
if let Event::Swipe(SwipeEvent::Move(dir, progress)) = event {
match dir {
SwipeDirection::Up | SwipeDirection::Down => {
self.dir = dir;
self.progress = progress;
}
_ => {}
}
ctx.request_paint();
ctx.request_anim_frame();
}
match event {
Event::Touch(_) => {
if self.attach_animation.is_active() {
None
} else {
self.inner.event(ctx, event)
}
}
_ => self.inner.event(ctx, event),
}
}
fn paint(&mut self) {
self.inner.paint()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let progress = self.progress as f32 / 1000.0;
let shift = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
let offset = i16::lerp(0, 50, shift.eval(progress));
let mask = u8::lerp(0, 255, shift.eval(progress));
if self.progress > 0 {
match self.dir {
SwipeDirection::Up => {
let offset = Offset::y(-offset);
target.in_clip(self.bounds, &|target| {
target.with_origin(offset, &|target| {
self.inner.render(target);
shape::Bar::new(self.bounds)
.with_alpha(mask)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
});
});
}
SwipeDirection::Down => {
let offset = Offset::y(offset);
target.with_origin(offset, &|target| {
self.inner.render(target);
shape::Bar::new(self.bounds)
.with_alpha(mask)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
});
}
_ => {}
};
} else {
let t = self.attach_animation.eval();
let offset = self.attach_animation.get_offset(t, self.attach_type);
let opacity = self.attach_animation.get_opacity(t, self.attach_type);
if offset.x != 0 || offset.y != 0 {
target.in_clip(self.bounds, &|target| {
target.with_origin(offset, &|target| {
self.inner.render(target);
shape::Bar::new(self.bounds)
.with_alpha(255 - opacity)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
});
});
} else {
// some components draw outside their bounds during animations
// let them do that unless we are animating here
self.inner.render(target);
shape::Bar::new(self.bounds)
.with_alpha(255 - opacity)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
}
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SwipeContent<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("SwipeContent");
t.child("content", &self.inner);
}
}

View File

@ -1,5 +1,7 @@
use crate::ui::{
component::{Component, Event, EventCtx, Swipe, SwipeDirection},
component::{Component, Event, EventCtx, SwipeDetect, SwipeDetectMsg},
event::SwipeEvent,
flow::Swipable,
geometry::Rect,
shape::Renderer,
};
@ -7,7 +9,7 @@ use crate::ui::{
/// Wrapper component adding "swipe up" handling to `content`.
pub struct SwipeUpScreen<T> {
content: T,
swipe: Swipe,
swipe: SwipeDetect,
}
pub enum SwipeUpScreenMsg<T> {
@ -19,33 +21,40 @@ impl<T> SwipeUpScreen<T>
where
T: Component,
{
pub fn new(content: T) -> Self {
pub fn new(content: T) -> Self
where
T: Swipable,
{
Self {
content,
swipe: Swipe::new().up(),
swipe: SwipeDetect::new(),
}
}
}
impl<T> Component for SwipeUpScreen<T>
where
T: Component,
{
impl<T: Swipable + Component> Component for SwipeUpScreen<T> {
type Msg = SwipeUpScreenMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.swipe.place(bounds);
self.content.place(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(SwipeDirection::Up) = self.swipe.event(ctx, event) {
return Some(SwipeUpScreenMsg::Swiped);
}
self.content
.event(ctx, event)
.map(SwipeUpScreenMsg::Content)
let e = match self
.swipe
.event(ctx, event, self.content.get_swipe_config())
{
Some(SwipeDetectMsg::Trigger(_)) => {
return Some(SwipeUpScreenMsg::Swiped);
}
Some(SwipeDetectMsg::Move(dir, progress)) => {
Event::Swipe(SwipeEvent::Move(dir, progress))
}
_ => event,
};
self.content.event(ctx, e).map(SwipeUpScreenMsg::Content)
}
fn paint(&mut self) {

View File

@ -25,6 +25,10 @@ impl TapToConfirmAmin {
const DURATION_MS: u32 = 600;
pub fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(Self::DURATION_MS))
}
@ -161,33 +165,27 @@ impl Component for TapToConfirm {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let btn_msg = self.button.event(ctx, event);
match btn_msg {
Some(ButtonMsg::Pressed) => {
self.anim.start();
ctx.request_anim_frame();
ctx.request_paint();
if let Some(ButtonMsg::Clicked) = btn_msg {
if animation_disabled() {
return Some(());
}
Some(ButtonMsg::Released) => {
self.anim.reset();
ctx.request_anim_frame();
ctx.request_paint();
}
Some(ButtonMsg::Clicked) => {
if animation_disabled() {
return Some(());
self.anim.start();
ctx.request_anim_frame();
ctx.request_paint();
}
if !animation_disabled() {
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.anim.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
}
}
_ => (),
if self.anim.is_finished() {
ctx.enable_swipe();
return Some(());
};
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.anim.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
}
}
if self.anim.is_finished() {
return Some(());
};
None
}
@ -245,9 +243,6 @@ impl Component for TapToConfirm {
}
}
#[cfg(feature = "micropython")]
impl crate::ui::flow::Swipable<()> for TapToConfirm {}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for TapToConfirm {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -3,12 +3,16 @@ use heapless::Vec;
use super::theme;
use crate::{
strutil::TString,
time::{Duration, Stopwatch},
ui::{
component::{base::Component, Event, EventCtx},
display::Icon,
geometry::Rect,
constant::screen,
display::{Color, Icon},
geometry::{Offset, Rect},
lerp::Lerp,
model_mercury::component::button::{Button, ButtonMsg, IconText},
shape::{Bar, Renderer},
util::animation_disabled,
},
};
@ -33,6 +37,84 @@ const MENU_SEP_HEIGHT: i16 = 2;
type VerticalMenuButtons = Vec<Button, N_ITEMS>;
type AreasForSeparators = Vec<Rect, N_SEPS>;
#[derive(Default, Clone)]
struct AttachAnimation {
pub timer: Stopwatch,
pub active: bool,
}
impl AttachAnimation {
const DURATION_MS: u32 = 350;
fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(Self::DURATION_MS))
}
fn eval(&self) -> f32 {
if animation_disabled() {
return 1.0;
}
self.timer.elapsed().to_millis() as f32 / 1000.0
}
fn get_offset(&self, t: f32) -> Offset {
let value = pareen::constant(0.0).seq_ease_in(
0.0,
easer::functions::Cubic,
Self::DURATION_MS as f32 / 1000.0,
pareen::constant(1.0),
);
Offset::lerp(Offset::new(-40, 0), Offset::zero(), value.eval(t))
}
fn get_mask_width(&self, t: f32) -> i16 {
let value = pareen::constant(0.0).seq_ease_in(
0.0,
easer::functions::Circ,
0.15,
pareen::constant(1.0),
);
//todo screen here is incorrect
i16::lerp(screen().width(), 0, value.eval(t))
}
fn start(&mut self) {
self.active = true;
self.timer.start();
}
fn reset(&mut self) {
self.active = false;
self.timer = Stopwatch::new_stopped();
}
fn lazy_start(&mut self, ctx: &mut EventCtx, event: Event) {
if let Event::Attach(_) = event {
self.reset();
ctx.request_anim_frame();
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if !self.timer.is_running() {
self.start();
}
if self.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
} else if self.active {
self.active = false;
ctx.request_anim_frame();
ctx.request_paint();
}
}
}
}
#[derive(Clone)]
pub struct VerticalMenu {
area: Rect,
@ -40,6 +122,8 @@ pub struct VerticalMenu {
buttons: VerticalMenuButtons,
/// areas for visual separators between buttons
areas_sep: AreasForSeparators,
attach_animation: AttachAnimation,
}
impl VerticalMenu {
@ -48,6 +132,7 @@ impl VerticalMenu {
area: Rect::zero(),
buttons,
areas_sep: AreasForSeparators::new(),
attach_animation: AttachAnimation::default(),
}
}
pub fn select_word(words: [TString<'static>; 3]) -> Self {
@ -107,9 +192,13 @@ impl Component for VerticalMenu {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
for (i, button) in self.buttons.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = button.event(ctx, event) {
return Some(VerticalMenuChoiceMsg::Selected(i));
self.attach_animation.lazy_start(ctx, event);
if !self.attach_animation.is_active() {
for (i, button) in self.buttons.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = button.event(ctx, event) {
return Some(VerticalMenuChoiceMsg::Selected(i));
}
}
}
None
@ -120,16 +209,31 @@ impl Component for VerticalMenu {
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// render buttons separated by thin bars
for button in &self.buttons {
button.render(target);
}
for area in self.areas_sep.iter() {
Bar::new(*area)
.with_thickness(MENU_SEP_HEIGHT)
.with_fg(theme::GREY_EXTRA_DARK)
let t = self.attach_animation.eval();
let offset = self.attach_animation.get_offset(t);
let mask_width = self.attach_animation.get_mask_width(t);
target.with_origin(offset, &|target| {
// render buttons separated by thin bars
for button in &self.buttons {
button.render(target);
}
for area in self.areas_sep.iter() {
Bar::new(*area)
.with_thickness(MENU_SEP_HEIGHT)
.with_fg(theme::GREY_EXTRA_DARK)
.render(target);
}
// todo screen here is incorrect
let r = Rect::from_size(Offset::new(mask_width, screen().height()));
Bar::new(r)
.with_fg(Color::black())
.with_bg(Color::black())
.render(target);
}
});
}
#[cfg(feature = "ui_bounds")]
@ -149,6 +253,3 @@ impl crate::trace::Trace for VerticalMenu {
});
}
}
#[cfg(feature = "micropython")]
impl crate::ui::flow::Swipable<VerticalMenuChoiceMsg> for VerticalMenu {}

View File

@ -4,10 +4,8 @@ use crate::{
strutil::TString,
translations::TR,
ui::{
component::{
text::paragraphs::Paragraph, Component, ComponentExt, Paginate, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
component::{text::paragraphs::Paragraph, ComponentExt, SwipeDirection},
flow::{base::Decision, FlowMsg, FlowState, FlowStore},
},
};
@ -107,8 +105,14 @@ impl FlowState for ConfirmActionSimple {
use crate::{
micropython::{map::Map, obj::Obj, qstr::Qstr, util},
ui::{
component::text::paragraphs::{ParagraphSource, ParagraphVecShort, VecExt},
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{ParagraphSource, ParagraphVecShort, VecExt},
Component, Paginate,
},
flow::{flow_store, SwipeFlow, SwipePage},
layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
@ -154,16 +158,22 @@ fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Err
paragraphs.into_paragraphs()
};
let mut content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None);
let mut content_intro =
Frame::left_aligned(title, SwipeContent::new(SwipePage::vertical(paragraphs)))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.with_vertical_pages();
if let Some(subtitle) = subtitle {
content_intro = content_intro.with_subtitle(subtitle);
}
let content_intro =
content_intro.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
let content_intro = content_intro.map(move |msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
_ => None,
});
let content_menu = if let Some(verb_cancel) = verb_cancel {
Frame::left_aligned(
@ -177,6 +187,7 @@ fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Err
)
}
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(move |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(_)) => Some(FlowMsg::Choice(0)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
@ -199,9 +210,11 @@ fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Err
)
};
let mut content_confirm = Frame::left_aligned(title, prompt)
let mut content_confirm = Frame::left_aligned(title, SwipeContent::new(prompt))
.with_footer(prompt_action, None)
.with_menu_button();
.with_menu_button()
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default());
if let Some(subtitle) = subtitle {
content_confirm = content_confirm.with_subtitle(subtitle);
@ -228,9 +241,12 @@ pub fn new_confirm_action_simple<T: Component + Paginate + Clone + MaybeTrace +
verb: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
) -> Result<Obj, error::Error> {
let mut frame = Frame::left_aligned(title, SwipePage::vertical(content))
let mut frame = Frame::left_aligned(title, SwipeContent::new(SwipePage::vertical(content)))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), verb);
.with_footer(TR::instructions__swipe_up.into(), verb)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::immediate())
.with_vertical_pages();
if let Some(subtitle) = subtitle {
frame = frame.with_subtitle(subtitle)
}
@ -249,6 +265,7 @@ pub fn new_confirm_action_simple<T: Component + Paginate + Clone + MaybeTrace +
)
}
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(move |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),

View File

@ -6,7 +6,7 @@ use crate::{
ui::{
button_request::ButtonRequest,
component::{ButtonRequestExt, ComponentExt, SwipeDirection},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
},
};
@ -76,7 +76,10 @@ impl FlowState for ConfirmOutput {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings, layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -117,6 +120,7 @@ impl ConfirmOutput {
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_text_mono(text_mono)
.with_swipe_down()
.into_layout()?
.one_button_request(ButtonRequest::from_num(br_code, br_type));
@ -128,6 +132,7 @@ impl ConfirmOutput {
.danger(theme::ICON_CANCEL, "Cancel sign".into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
@ -135,15 +140,17 @@ impl ConfirmOutput {
// AccountInfo
let ad = AddressDetails::new(TR::send__send_from.into(), account, account_path)?;
let content_account = SwipePage::horizontal(ad).map(|_| Some(FlowMsg::Cancelled));
let content_account = ad.map(|_| Some(FlowMsg::Cancelled));
// CancelTap
let content_cancel_tap = Frame::left_aligned(
TR::send__cancel_sign.into(),
PromptScreen::new_tap_to_cancel(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),

View File

@ -6,16 +6,18 @@ use crate::{
ui::{
button_request::ButtonRequestCode,
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, Paragraphs},
ButtonRequestExt, ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
layout::obj::LayoutObj,
model_mercury::component::{PromptScreen, SwipeContent},
},
};
use super::super::{
component::{Frame, FrameMsg, HoldToConfirm, VerticalMenu, VerticalMenuChoiceMsg},
component::{Frame, FrameMsg, VerticalMenu, VerticalMenuChoiceMsg},
theme,
};
@ -32,15 +34,18 @@ impl FlowState for ConfirmResetCreate {
(ConfirmResetCreate::Intro, SwipeDirection::Left) => {
Decision::Goto(ConfirmResetCreate::Menu, direction)
}
(ConfirmResetCreate::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmResetCreate::Intro, direction)
}
(ConfirmResetCreate::Intro, SwipeDirection::Up) => {
Decision::Goto(ConfirmResetCreate::Confirm, direction)
}
(ConfirmResetCreate::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmResetCreate::Intro, direction)
}
(ConfirmResetCreate::Confirm, SwipeDirection::Down) => {
Decision::Goto(ConfirmResetCreate::Intro, direction)
}
(ConfirmResetCreate::Confirm, SwipeDirection::Left) => {
Decision::Goto(ConfirmResetCreate::Menu, direction)
}
_ => Decision::Nothing,
}
}
@ -57,6 +62,9 @@ impl FlowState for ConfirmResetCreate {
(ConfirmResetCreate::Confirm, FlowMsg::Confirmed) => {
Decision::Return(FlowMsg::Confirmed)
}
(ConfirmResetCreate::Confirm, FlowMsg::Info) => {
Decision::Goto(ConfirmResetCreate::Menu, SwipeDirection::Left)
}
_ => Decision::Nothing,
}
}
@ -81,9 +89,11 @@ impl ConfirmResetCreate {
Paragraph::new(&theme::TEXT_SUB_GREY_LIGHT, TR::reset__tos_link),
];
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
let content_intro = Frame::left_aligned(title, SwipeContent::new(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.one_button_request(ButtonRequestCode::ResetDevice.with_type("setup_device"));
@ -92,21 +102,25 @@ impl ConfirmResetCreate {
VerticalMenu::empty().danger(theme::ICON_CANCEL, "Cancel".into()), // TODO: use TR
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let content_confirm =
Frame::left_aligned(TR::reset__title_create_wallet.into(), HoldToConfirm::new())
.with_footer(TR::instructions__hold_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
_ => Some(FlowMsg::Cancelled),
})
.one_button_request(
ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"),
);
let content_confirm = Frame::left_aligned(
TR::reset__title_create_wallet.into(),
SwipeContent::new(PromptScreen::new_hold_to_confirm()),
)
.with_menu_button()
.with_footer(TR::instructions__hold_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
})
.one_button_request(ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"));
let store = flow_store()
.add(content_intro)?

View File

@ -7,12 +7,12 @@ use crate::{
text::paragraphs::{Paragraph, Paragraphs},
ButtonRequestExt, ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
},
};
use super::super::{
component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg},
component::{Frame, FrameMsg, VerticalMenu, VerticalMenuChoiceMsg},
theme,
};
@ -54,7 +54,11 @@ impl FlowState for ConfirmResetRecover {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings,
layout::obj::LayoutObj,
model_mercury::component::{PromptScreen, SwipeContent},
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -77,10 +81,12 @@ impl ConfirmResetRecover {
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(
TR::recovery__title_recover.into(),
SwipePage::vertical(paragraphs),
SwipeContent::new(paragraphs),
)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.one_button_request(ButtonRequestCode::ProtectCall.with_type("recover_device"));
@ -92,6 +98,7 @@ impl ConfirmResetRecover {
),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
@ -99,9 +106,10 @@ impl ConfirmResetRecover {
let content_confirm = Frame::left_aligned(
TR::reset__title_create_wallet.into(),
PromptScreen::new_hold_to_confirm(),
SwipeContent::new(PromptScreen::new_hold_to_confirm()),
)
.with_footer(TR::instructions__hold_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
_ => Some(FlowMsg::Cancelled),

View File

@ -8,7 +8,7 @@ use crate::{
text::paragraphs::{Paragraph, Paragraphs},
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, FlowMsg, FlowState, FlowStore},
},
};
@ -31,13 +31,21 @@ impl FlowState for SetNewPin {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(SetNewPin::Intro, SwipeDirection::Left) => Decision::Goto(SetNewPin::Menu, direction),
(SetNewPin::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
(SetNewPin::Menu, SwipeDirection::Right) => Decision::Goto(SetNewPin::Intro, direction),
(SetNewPin::CancelPinIntro, SwipeDirection::Up) => {
Decision::Goto(SetNewPin::CancelPinConfirm, direction)
}
(SetNewPin::CancelPinIntro, SwipeDirection::Right) => {
Decision::Goto(SetNewPin::Intro, direction)
}
(SetNewPin::CancelPinConfirm, SwipeDirection::Down) => {
Decision::Goto(SetNewPin::CancelPinIntro, direction)
}
(SetNewPin::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
(SetNewPin::CancelPinConfirm, SwipeDirection::Right) => {
Decision::Goto(SetNewPin::Intro, direction)
}
_ => Decision::Nothing,
}
}
@ -54,7 +62,7 @@ impl FlowState for SetNewPin {
Decision::Goto(SetNewPin::Intro, SwipeDirection::Right)
}
(SetNewPin::CancelPinIntro, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::Menu, SwipeDirection::Right)
Decision::Goto(SetNewPin::Intro, SwipeDirection::Right)
}
(SetNewPin::CancelPinConfirm, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::CancelPinIntro, SwipeDirection::Right)
@ -69,7 +77,12 @@ impl FlowState for SetNewPin {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings,
flow::{flow_store, SwipeFlow},
layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -86,9 +99,11 @@ impl SetNewPin {
let par_array: [Paragraph<'static>; 1] =
[Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)];
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
let content_intro = Frame::left_aligned(title, SwipeContent::new(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| {
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
});
@ -98,6 +113,7 @@ impl SetNewPin {
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::pin__cancel_setup.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
@ -111,13 +127,15 @@ impl SetNewPin {
let paragraphs_cancel_intro = Paragraphs::new(par_array_cancel_intro);
let content_cancel_intro = Frame::left_aligned(
TR::pin__cancel_setup.into(),
SwipePage::vertical(paragraphs_cancel_intro),
SwipeContent::new(paragraphs_cancel_intro),
)
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::pin__cancel_description.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
@ -125,9 +143,12 @@ impl SetNewPin {
let content_cancel_confirm = Frame::left_aligned(
TR::pin__cancel_setup.into(),
PromptScreen::new_tap_to_cancel(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),

View File

@ -78,7 +78,10 @@ impl FlowState for ConfirmSummary {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings, layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -98,7 +101,8 @@ impl ConfirmSummary {
// Summary
let mut summary = ShowInfoParams::new(title)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None);
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe_up();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
summary = unwrap!(summary.add(label, value));
@ -112,10 +116,12 @@ impl ConfirmSummary {
// Hold to confirm
let content_hold = Frame::left_aligned(
TR::send__sign_transaction.into(),
PromptScreen::new_hold_to_confirm(),
SwipeContent::new(PromptScreen::new_hold_to_confirm()),
)
.with_menu_button()
.with_footer(TR::instructions__hold_to_sign.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
@ -130,6 +136,7 @@ impl ConfirmSummary {
.danger(theme::ICON_CANCEL, "Cancel sign".into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
@ -158,6 +165,7 @@ impl ConfirmSummary {
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),

View File

@ -9,10 +9,7 @@ use crate::{
text::paragraphs::{Paragraph, ParagraphSource, Paragraphs},
ButtonRequestExt, ComponentExt, Qr, SwipeDirection,
},
flow::{
base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeFlow,
SwipePage,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
layout::util::ConfirmBlob,
},
};
@ -49,6 +46,7 @@ impl FlowState for GetAddress {
(GetAddress::Tap, SwipeDirection::Down) => {
Decision::Goto(GetAddress::Address, direction)
}
(GetAddress::Tap, SwipeDirection::Left) => Decision::Goto(GetAddress::Menu, direction),
(GetAddress::Menu, SwipeDirection::Right) => {
Decision::Goto(GetAddress::Address, direction)
}
@ -56,7 +54,7 @@ impl FlowState for GetAddress {
Decision::Goto(GetAddress::Menu, direction)
}
(GetAddress::AccountInfo, SwipeDirection::Right) => {
Decision::Goto(GetAddress::Menu, direction)
Decision::Goto(GetAddress::Menu, SwipeDirection::Right)
}
(GetAddress::Cancel, SwipeDirection::Up) => {
Decision::Goto(GetAddress::CancelTap, direction)
@ -67,6 +65,9 @@ impl FlowState for GetAddress {
(GetAddress::CancelTap, SwipeDirection::Down) => {
Decision::Goto(GetAddress::Cancel, direction)
}
(GetAddress::CancelTap, SwipeDirection::Right) => {
Decision::Goto(GetAddress::Menu, direction)
}
_ => Decision::Nothing,
}
}
@ -81,6 +82,10 @@ impl FlowState for GetAddress {
Decision::Goto(GetAddress::Confirmed, SwipeDirection::Up)
}
(GetAddress::Tap, FlowMsg::Info) => {
Decision::Goto(GetAddress::Menu, SwipeDirection::Left)
}
(GetAddress::Confirmed, _) => Decision::Return(FlowMsg::Confirmed),
(GetAddress::Menu, FlowMsg::Choice(0)) => {
@ -124,7 +129,10 @@ impl FlowState for GetAddress {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings, flow::SwipePage, layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -167,30 +175,34 @@ impl GetAddress {
data_font: data_style,
}
.into_paragraphs();
let content_address = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.one_button_request(ButtonRequest::from_num(br_code, br_type))
// Count tap-to-confirm screen towards page count
.with_pages(|address_pages| address_pages + 1);
let content_address =
Frame::left_aligned(title, SwipeContent::new(SwipePage::vertical(paragraphs)))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.with_vertical_pages()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.one_button_request(ButtonRequest::from_num(br_code, br_type))
// Count tap-to-confirm screen towards page count
.with_pages(|address_pages| address_pages + 1);
// Tap
let content_tap = Frame::left_aligned(title, PromptScreen::new_tap_to_confirm())
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let content_tap =
Frame::left_aligned(title, SwipeContent::new(PromptScreen::new_tap_to_confirm()))
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
});
let content_confirmed = IgnoreSwipe::new(
Frame::left_aligned(
TR::address__confirmed.into(),
StatusScreen::new_success_timeout(),
)
.with_footer(TR::instructions__continue_in_app.into(), None),
let content_confirmed = Frame::left_aligned(
TR::address__confirmed.into(),
StatusScreen::new_success_timeout(),
)
.with_footer(TR::instructions__continue_in_app.into(), None)
.map(|_| Some(FlowMsg::Confirmed));
// Menu
@ -205,6 +217,7 @@ impl GetAddress {
.danger(theme::ICON_CANCEL, TR::address__cancel_receive.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
@ -213,11 +226,9 @@ impl GetAddress {
// QrCode
let content_qr = Frame::left_aligned(
title,
IgnoreSwipe::new(
address_qr
.map(|s| Qr::new(s, case_sensitive))?
.with_border(QR_BORDER),
),
address_qr
.map(|s| Qr::new(s, case_sensitive))?
.with_border(QR_BORDER),
)
.with_cancel_button()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled));
@ -228,18 +239,20 @@ impl GetAddress {
let [xtitle, text]: [TString; 2] = util::iter_into_array(i)?;
ad.add_xpub(xtitle, text)?;
}
let content_account = SwipePage::horizontal(ad).map(|_| Some(FlowMsg::Cancelled));
let content_account = ad.map(|_| Some(FlowMsg::Cancelled));
// Cancel
let content_cancel_info = Frame::left_aligned(
TR::address__cancel_receive.into(),
SwipePage::vertical(Paragraphs::new(Paragraph::new(
SwipeContent::new(Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::address__cancel_contact_support,
))),
)
.with_cancel_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled));
// CancelTap
@ -249,6 +262,8 @@ impl GetAddress {
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),

View File

@ -7,7 +7,7 @@ use crate::{
text::paragraphs::{Paragraph, Paragraphs},
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, FlowMsg, FlowState, FlowStore},
},
};
@ -32,13 +32,24 @@ impl FlowState for PromptBackup {
(PromptBackup::Intro, SwipeDirection::Left) => {
Decision::Goto(PromptBackup::Menu, direction)
}
(PromptBackup::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
(PromptBackup::Menu, SwipeDirection::Right) => {
Decision::Goto(PromptBackup::Intro, direction)
}
(PromptBackup::SkipBackupIntro, SwipeDirection::Up) => {
Decision::Goto(PromptBackup::SkipBackupConfirm, direction)
}
(PromptBackup::SkipBackupIntro, SwipeDirection::Right) => {
Decision::Goto(PromptBackup::Intro, direction)
}
(PromptBackup::SkipBackupConfirm, SwipeDirection::Down) => {
Decision::Goto(PromptBackup::SkipBackupIntro, direction)
}
(PromptBackup::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
(PromptBackup::SkipBackupConfirm, SwipeDirection::Right) => {
Decision::Goto(PromptBackup::Intro, direction)
}
_ => Decision::Nothing,
}
}
@ -70,7 +81,12 @@ impl FlowState for PromptBackup {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings,
flow::{flow_store, SwipeFlow},
layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -88,9 +104,11 @@ impl PromptBackup {
TString::from_str("Your wallet backup contains words in a specific order."),
)];
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
let content_intro = Frame::left_aligned(title, SwipeContent::new(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| {
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
});
@ -100,6 +118,7 @@ impl PromptBackup {
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::backup__title_skip.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
@ -116,13 +135,15 @@ impl PromptBackup {
let paragraphs_skip_intro = Paragraphs::new(par_array_skip_intro);
let content_skip_intro = Frame::left_aligned(
TR::backup__title_skip.into(),
SwipePage::vertical(paragraphs_skip_intro),
SwipeContent::new(paragraphs_skip_intro),
)
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::words__continue_anyway.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
@ -130,9 +151,12 @@ impl PromptBackup {
let content_skip_confirm = Frame::left_aligned(
TR::backup__title_skip.into(),
PromptScreen::new_tap_to_cancel(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),

View File

@ -9,7 +9,7 @@ use crate::{
text::paragraphs::{Paragraph, Paragraphs},
ButtonRequestExt, ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
},
};
@ -67,7 +67,10 @@ impl FlowState for RequestNumber {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings, layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -102,14 +105,17 @@ impl RequestNumber {
let number_input_dialog =
NumberInputDialog::new(min_count, max_count, count, description_cb)?;
let content_number_input = Frame::left_aligned(title, number_input_dialog)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
FrameMsg::Content(NumberInputDialogMsg(n)) => Some(FlowMsg::Choice(n as usize)),
})
.one_button_request(ButtonRequest::from_num(br_code, br_type));
let content_number_input =
Frame::left_aligned(title, SwipeContent::new(number_input_dialog))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
FrameMsg::Content(NumberInputDialogMsg(n)) => Some(FlowMsg::Choice(n as usize)),
})
.one_button_request(ButtonRequest::from_num(br_code, br_type));
let content_menu = Frame::left_aligned(
"".into(),
@ -118,6 +124,7 @@ impl RequestNumber {
.danger(theme::ICON_CANCEL, TR::backup__title_skip.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
@ -130,9 +137,10 @@ impl RequestNumber {
));
let content_info = Frame::left_aligned(
TR::backup__title_skip.into(),
SwipePage::vertical(paragraphs_info),
SwipeContent::new(paragraphs_info),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,

View File

@ -6,11 +6,13 @@ use crate::{
ui::{
button_request::ButtonRequestCode,
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, Paragraphs},
ButtonRequestExt, ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
use heapless::Vec;
@ -84,35 +86,41 @@ impl ShowShareWords {
let content_instruction = Frame::left_aligned(
title,
SwipePage::vertical(Paragraphs::new(Paragraph::new(
SwipeContent::new(Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
text_info,
))),
)
.with_subtitle(TR::words__instructions.into())
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed))
.one_button_request(ButtonRequestCode::ResetDevice.with_type("share_words"))
.with_pages(move |_| nwords + 2);
let content_words = Frame::left_aligned(title, ShareWords::new(share_words_vec))
.with_subtitle(subtitle)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_vertical_pages()
.map(|_| None);
let content_confirm =
Frame::left_aligned(text_confirm, PromptScreen::new_hold_to_confirm())
.with_footer(TR::instructions__hold_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.map(|_| Some(FlowMsg::Confirmed));
let content_check_backup_intro = Frame::left_aligned(
TR::reset__check_backup_title.into(),
SwipePage::vertical(Paragraphs::new(Paragraph::new(
SwipeContent::new(Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::reset__check_backup_instructions,
))),
)
.with_subtitle(TR::words__instructions.into())
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.map(|_| Some(FlowMsg::Confirmed));
let store = flow_store()

View File

@ -10,11 +10,13 @@ use crate::{
ui::{
component::{
base::ComponentExt,
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, VecExt},
Component,
Component, SwipeDirection,
},
flow::{FlowMsg, Swipable, SwipePage},
layout::util::ConfirmBlob,
model_mercury::component::SwipeContent,
},
};
use heapless::Vec;
@ -30,6 +32,7 @@ pub struct ConfirmBlobParams {
menu_button: bool,
chunkify: bool,
text_mono: bool,
swipe_down: bool,
}
impl ConfirmBlobParams {
@ -49,6 +52,7 @@ impl ConfirmBlobParams {
menu_button: false,
chunkify: false,
text_mono: true,
swipe_down: false,
}
}
@ -67,6 +71,11 @@ impl ConfirmBlobParams {
self
}
pub const fn with_swipe_down(mut self) -> Self {
self.swipe_down = true;
self
}
pub const fn with_footer(
mut self,
instruction: TString<'static>,
@ -89,7 +98,7 @@ impl ConfirmBlobParams {
pub fn into_layout(
self,
) -> Result<impl Component<Msg = FlowMsg> + Swipable<FlowMsg> + MaybeTrace, Error> {
) -> Result<impl Component<Msg = FlowMsg> + Swipable + MaybeTrace, Error> {
let paragraphs = ConfirmBlob {
description: self.description.unwrap_or("".into()),
extra: self.extra.unwrap_or("".into()),
@ -107,7 +116,7 @@ impl ConfirmBlobParams {
}
.into_paragraphs();
let page = SwipePage::vertical(paragraphs);
let page = SwipeContent::new(SwipePage::vertical(paragraphs));
let mut frame = Frame::left_aligned(self.title, page);
if let Some(subtitle) = self.subtitle {
frame = frame.with_subtitle(subtitle);
@ -117,7 +126,16 @@ impl ConfirmBlobParams {
}
if let Some(instruction) = self.footer_instruction {
frame = frame.with_footer(instruction, self.footer_description);
frame = frame.with_swipe(SwipeDirection::Left, SwipeSettings::default());
}
if self.swipe_down {
frame = frame.with_swipe(SwipeDirection::Down, SwipeSettings::default());
}
frame = frame.with_swipe(SwipeDirection::Up, SwipeSettings::default());
frame = frame.with_vertical_pages();
Ok(frame.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)))
}
}
@ -130,6 +148,7 @@ pub struct ShowInfoParams {
footer_instruction: Option<TString<'static>>,
footer_description: Option<TString<'static>>,
chunkify: bool,
swipe_up: bool,
items: Vec<(TString<'static>, TString<'static>), 4>,
}
@ -143,6 +162,7 @@ impl ShowInfoParams {
footer_instruction: None,
footer_description: None,
chunkify: false,
swipe_up: false,
items: Vec::new(),
}
}
@ -185,9 +205,14 @@ impl ShowInfoParams {
self
}
pub const fn with_swipe_up(mut self) -> Self {
self.swipe_up = true;
self
}
pub fn into_layout(
self,
) -> Result<impl Component<Msg = FlowMsg> + Swipable<FlowMsg> + MaybeTrace, Error> {
) -> Result<impl Component<Msg = FlowMsg> + Swipable + MaybeTrace, Error> {
let mut paragraphs = ParagraphVecShort::new();
let mut first: bool = true;
for item in self.items {
@ -212,19 +237,30 @@ impl ShowInfoParams {
let mut frame = Frame::left_aligned(
self.title,
SwipePage::vertical(paragraphs.into_paragraphs()),
SwipeContent::new(SwipePage::vertical(paragraphs.into_paragraphs())),
);
if let Some(subtitle) = self.subtitle {
frame = frame.with_subtitle(subtitle);
}
if self.cancel_button {
frame = frame.with_cancel_button();
frame = frame
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate());
} else if self.menu_button {
frame = frame.with_menu_button();
frame = frame
.with_menu_button()
.with_swipe(SwipeDirection::Left, SwipeSettings::default());
}
if let Some(instruction) = self.footer_instruction {
frame = frame.with_footer(instruction, self.footer_description);
}
if self.swipe_up {
frame = frame.with_swipe(SwipeDirection::Up, SwipeSettings::default());
}
frame = frame.with_vertical_pages();
Ok(frame.map(move |msg| {
matches!(msg, FrameMsg::Button(_)).then_some(if self.cancel_button {
FlowMsg::Cancelled

View File

@ -8,10 +8,7 @@ use crate::{
text::paragraphs::{Paragraph, ParagraphSource},
ComponentExt, SwipeDirection,
},
flow::{
base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeFlow,
SwipePage,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
},
};
@ -63,7 +60,10 @@ impl FlowState for WarningHiPrio {
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
ui::{
component::swipe_detect::SwipeSettings, layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
@ -89,10 +89,12 @@ impl WarningHiPrio {
.with_top_padding(Self::EXTRA_PADDING),
]
.into_paragraphs();
let content_message = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
let content_message = Frame::left_aligned(title, SwipeContent::new(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), Some(cancel))
.with_danger()
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
// .one_button_request(ButtonRequestCode::Warning, br_type);
@ -104,17 +106,17 @@ impl WarningHiPrio {
.danger(theme::ICON_CHEVRON_RIGHT, confirm),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
// Cancelled
let content_cancelled = IgnoreSwipe::new(
let content_cancelled =
Frame::left_aligned(done_title, StatusScreen::new_neutral_timeout())
.with_footer(TR::instructions__continue_in_app.into(), None),
)
.map(|_| Some(FlowMsg::Cancelled));
.with_footer(TR::instructions__continue_in_app.into(), None)
.map(|_| Some(FlowMsg::Cancelled));
let store = flow_store()
.add(content_message)?

View File

@ -12,11 +12,12 @@ use crate::{
ui::{
backlight::BACKLIGHT_LEVELS_OBJ,
component::{
base::ComponentExt,
base::{AttachType, ComponentExt},
connect::Connect,
image::BlendedImage,
jpeg::Jpeg,
paginated::{PageMsg, Paginate},
swipe_detect::SwipeSettings,
text::{
op::OpTextLayout,
paragraphs::{
@ -25,15 +26,16 @@ use crate::{
},
TextStyle,
},
Border, Component, Empty, FormattedText, Label, Never, Timeout,
Border, Component, Empty, FormattedText, Label, Never, SwipeDirection, Timeout,
},
flow::Swipable,
geometry,
layout::{
obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO},
util::{upy_disable_animation, ConfirmBlob, PropsList},
},
model_mercury::component::check_homescreen_format,
model_mercury::component::{check_homescreen_format, SwipeContent},
},
};
@ -209,10 +211,7 @@ impl ComponentMsgObj for PromptScreen {
}
}
impl<T> ComponentMsgObj for SwipeUpScreen<T>
where
T: Component,
{
impl<T: Component + Swipable> ComponentMsgObj for SwipeUpScreen<T> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
SwipeUpScreenMsg::Content(_) => Err(Error::TypeError),
@ -736,7 +735,8 @@ extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs:
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(TR::modify_amount__title.into(), paragraphs)
.with_cancel_button()
.with_footer(TR::instructions__swipe_up.into(), None),
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -778,7 +778,8 @@ extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *m
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, paragraphs)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None),
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -860,20 +861,21 @@ extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map)
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let allow_cancel: bool = kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into()?;
let content = SwipeUpScreen::new(Paragraphs::new([Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
description,
)]));
let content = Paragraphs::new([Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)]);
let frame = if allow_cancel {
Frame::left_aligned(title, content)
Frame::left_aligned(title, SwipeContent::new(content))
.with_cancel_button()
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
} else {
Frame::left_aligned(title, content)
Frame::left_aligned(title, SwipeContent::new(content))
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
};
let frame = SwipeUpScreen::new(frame);
let obj = LayoutObj::new(frame)?;
Ok(obj.into())
};
@ -918,15 +920,16 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
let value: TString = kwargs.get_or(Qstr::MP_QSTR_value, "".into())?;
let action: Option<TString> = kwargs.get(Qstr::MP_QSTR_button)?.try_into_option()?;
let content = SwipeUpScreen::new(Paragraphs::new([
let content = Paragraphs::new([
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description),
Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, value),
]));
let obj = LayoutObj::new(
Frame::left_aligned(title, content)
]);
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content))
.with_warning_button()
.with_footer(TR::instructions__swipe_up.into(), action),
)?;
.with_footer(TR::instructions__swipe_up.into(), action)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -936,10 +939,11 @@ extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let content = StatusScreen::new_success();
let obj = LayoutObj::new(
Frame::left_aligned(title, content)
.with_footer(TR::instructions__swipe_up.into(), None),
)?;
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content).with_normal_attach(None))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -949,14 +953,12 @@ extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let content = SwipeUpScreen::new(Paragraphs::new([Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
description,
)]));
let obj = LayoutObj::new(
Frame::left_aligned(title, content)
.with_footer(TR::instructions__swipe_up.into(), None),
)?;
let content = Paragraphs::new([Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)]);
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1070,7 +1072,8 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, paragraphs.into_paragraphs())
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), Some(button)),
.with_footer(TR::instructions__swipe_up.into(), Some(button))
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -1227,22 +1230,26 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
paragraphs.add(Paragraph::new(style, text));
}
let checklist_content = SwipeUpScreen::new(
Checklist::from_paragraphs(
theme::ICON_CHEVRON_RIGHT,
theme::ICON_BULLET_CHECKMARK,
active,
paragraphs
.into_paragraphs()
.with_spacing(theme::CHECKLIST_SPACING),
let checklist_content = Checklist::from_paragraphs(
theme::ICON_CHEVRON_RIGHT,
theme::ICON_BULLET_CHECKMARK,
active,
paragraphs
.into_paragraphs()
.with_spacing(theme::CHECKLIST_SPACING),
)
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
.with_icon_done_color(theme::GREEN);
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(
title,
SwipeContent::new(checklist_content)
.with_normal_attach(Some(AttachType::Swipe(SwipeDirection::Up))),
)
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
.with_icon_done_color(theme::GREEN),
);
let obj = LayoutObj::new(
Frame::left_aligned(title, checklist_content)
.with_footer(TR::instructions__swipe_up.into(), None),
)?;
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1264,12 +1271,12 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut
_ => TR::recovery__title.into(),
};
let content = SwipeUpScreen::new(paragraphs);
let obj = LayoutObj::new(
Frame::left_aligned(notification, content)
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(notification, SwipeContent::new(paragraphs))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_subtitle(TR::words__instructions.into()),
)?;
.with_subtitle(TR::words__instructions.into())
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }

View File

@ -63,7 +63,7 @@ impl Component for CoinJoinProgress {
// Determinate ones are receiving Event::Progress events.
if self.indeterminate {
match event {
Event::Attach => {
Event::Attach(_) => {
ctx.request_anim_frame();
}
Event::Timer(EventCtx::ANIM_FRAME_TIMER) => {

View File

@ -93,7 +93,7 @@ where
_ if animation_disabled() => {
return None;
}
Event::Attach if self.indeterminate => {
Event::Attach(_) if self.indeterminate => {
ctx.request_anim_frame();
}
Event::Timer(EventCtx::ANIM_FRAME_TIMER) => {

View File

@ -199,7 +199,7 @@ impl Component for PinKeyboard<'_> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
// Set up timer to switch off warning prompt.
Event::Attach if self.major_warning.is_some() => {
Event::Attach(_) if self.major_warning.is_some() => {
self.warning_timer = Some(ctx.request_timer(Duration::from_secs(2)));
}
// Hide warning, show major prompt.

View File

@ -81,6 +81,7 @@ def _require_confirm_change_pin(msg: ChangePin) -> Awaitable[None]:
TR.pin__title_settings,
description=TR.pin__change,
verb=TR.buttons__change,
prompt_screen=False,
)
if not msg.remove and not has_pin: # setting new pin

View File

@ -15500,9 +15500,9 @@
"T3T1_cs_test_autolock.py::test_dryrun_enter_word_slowly": "101892fa0a24f33279d07fda4477178a7b45fc04c01acbbf901833d4d024158f",
"T3T1_cs_test_autolock.py::test_dryrun_locks_at_number_of_words": "55cdac02cdd20ce5249fabd5e63ad68386a796634fa21e2ef7cb1701e98c0020",
"T3T1_cs_test_autolock.py::test_dryrun_locks_at_word_entry": "be2ba7ad30c636e7542ede4c2b39f6c65da16eed559557d559fa2a9afdb98de6",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "9baac2b705a9f38453c784d97f7db405f78c3ddad7d32e75acd6c9cfe418b69a",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "a289533058d32023964eecfaeaec3f0c5c489aa3ddacddb44c7944126a9e2c92",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "30add0f52ceec0d4aab7c8f4fbfa848b70748dc6b449fd35bb800ed65bfa6918",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "cf939a63908ea803ff46919cdf957e24fc1eaf9a830fb16033b910a1ad0b4c82",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "79241bbf8d46ac4497e668e7705152553c9c34c59658ed3b38fb52d70b613e24",
"T3T1_cs_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "d03f87e6020b3c9e47a5464a8a68752683b96746612274d91301c7ab4ffbca3e",
"T3T1_cs_test_lock.py::test_hold_to_lock": "8e99b5897e849374c94b8bf9a581971bffa050bdf39a684967be642f66fca612",
"T3T1_cs_test_passphrase_mercury.py::test_cycle_through_last_character": "bf5322dcec0e6560aec763f04e4c2f6afd3e056968644414f3acfe4203745c66",
"T3T1_cs_test_passphrase_mercury.py::test_passphrase_click_same_button_many_times": "9c272fbb29ebcf1d330827e863bdba92ef47b506843ef9108a43304c93dd9e86",
@ -15549,9 +15549,9 @@
"T3T1_de_test_autolock.py::test_dryrun_enter_word_slowly": "2f123bac78e5f12dfea18414032142e3c6efd601aa60b84a5a9e0e10a46ff85c",
"T3T1_de_test_autolock.py::test_dryrun_locks_at_number_of_words": "5c221cc9751183eaa02e2347571819469c316bb1cde0d29a5a252cf0c7a5819e",
"T3T1_de_test_autolock.py::test_dryrun_locks_at_word_entry": "5744408e0dc4ea581e209bf26abd86b29f34f89e39f8ceb125811670feee27f3",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "f0d1307d9bb9e842726e8616d4ababa60df4dcfd8e48f9777583c5fdc1d529be",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "1bf1275b1b0ae36c3df53e0df122c08ef0c1b11ad27dfda08a0e7ea4f9e13e5b",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "2d820b034627bebf99a0a054ed7ec3b81ad06f2b14803f67566e1776da943990",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "a7fcfa798f05f90df7ecf9a4e7b0f16c3c41999b995cfddd3249a890c21a50b1",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "f976c56e7f7e4302d27df9eb54f30cfce19d6dd553e5dbf42608ee579605fed0",
"T3T1_de_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "ce7fc86f644dcbaae77fbd45f6e12004c3175fef69d5cb80b7eace5b38f0195f",
"T3T1_de_test_lock.py::test_hold_to_lock": "dfecbd90b394f3b35f0cc2a0248c35b50427a67ed647499e5652ac51d6cc786c",
"T3T1_de_test_passphrase_mercury.py::test_cycle_through_last_character": "5532543c77b2c9f64c16cc9f31371decda359063067dac21143c1b318174750d",
"T3T1_de_test_passphrase_mercury.py::test_passphrase_click_same_button_many_times": "58bf45df88e43fdc94381e7d98320311daac04740851aacc8e9930d7fcf95245",
@ -15598,9 +15598,9 @@
"T3T1_en_test_autolock.py::test_dryrun_enter_word_slowly": "f6862c4fdc00878f9c14339896139094701c34a89f4f9c32d4e03767540de60e",
"T3T1_en_test_autolock.py::test_dryrun_locks_at_number_of_words": "d2423bdeb2923a2c06b59bf114a6219e4b918d7d3c061750985531b3f6ae8329",
"T3T1_en_test_autolock.py::test_dryrun_locks_at_word_entry": "5f36a5d9d88476467ba31078701529dfdcdea2344944a8952e69dbfec238598f",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "62e797bcac12d651ab64b33db19dae577a31fd002945e3f495899b3a7004e415",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "ea977ed91b155f102f66fbaf519b9c764a3839bd47f64461d41a8d1677faeb1f",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "4c47a36836400172683dd2d47b38cbdde11bbbb702d4cac72a8b0778f24e2c2c",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "1cc207d1d95529d71febcf656c29e54272aa67d4853291e7bda746b0e01234c5",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "12a6032073d94c4f1be1176f087faea05d4393e2fce5d889375b7c84891de3d3",
"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "6b80f5fe9beede5b34269bd8747d86e93c11de489758f35eac92d8a6ce09a786",
"T3T1_en_test_lock.py::test_hold_to_lock": "1665b2e4984bcc7a2f307a89789dc2d6c8db9c04a5906729f769e0e8b76da04c",
"T3T1_en_test_passphrase_mercury.py::test_cycle_through_last_character": "e0faf9f3c0c83f6762dfa00323d7a69ef4f1f71ef99160b88541e9068f04e55a",
"T3T1_en_test_passphrase_mercury.py::test_passphrase_click_same_button_many_times": "51c45f969237398ccc41494bca87694b061578042c71508c61994ea47907e033",
@ -16794,12 +16794,12 @@
"T3T1_cs_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "a004391eed80bfe3a85e716311e04bcaea4223178ce2c24dd3a478c1103b3ae0",
"T3T1_cs_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "627937945fa769ac6f8a05d1538f0fadceac61a1347ed748a7c9280e7a223b66",
"T3T1_cs_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "901d35e416e91c264ebedd47854e9eed3601cfd851cfae5ca57940c1b5c3abc0",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "df132e2ca8201b12311cd4b525e108ace38049a4b95675579eaf2c4cfc0c379f",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "bc727f137c9bfa6926f9da4645217b6003ef1b6c686bb165b1417afc15697214",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "03717e33ab843a8706c5cd373d2b979351e0408d578109f4a1cc1ddd1bfe916a",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "d64a0585937fd4c01c4771b8fff082dea3b864da5588f88637f01210bd07e3c6",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "5459bb41b69480ccf1850e3cd3edf8cce8b85457403a929f98cab17aa6d432a5",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "17db466823df48752dc7f66b86d46fc4c9242d4af92668aa282985f64e520130",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "d57198c00b1fbc8d8aff6e9ceb7a28b33e13b0a497fbde18bb5338a706509fbf",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "fffde13cd2189fd285cfba584b6257471be1703f5f3bea57d340fc80f5c4124e",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "57630bbab5d3566a14403ca11cc93e21e9742ac0de27b6c9700744682a587cb1",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "5504101b5bccffc7c99f0b7306c8e4ee0d8f030c210b6b233993e0e961321fec",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "10069d5e7c8aed9894fd038ecf70c16f0196f6c6f0c0bf3e305760efe3deab32",
"T3T1_cs_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "2643a0a963c0178087d7432eef8a1217979e7d064082dbc3dd96cd91805954de",
"T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "c61d87e076ae2251d0595371f8ab11f441d040475187da9739d6b5d469544915",
"T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "b81c0adf31a42588f04333364365ae4a2a17c41bba10f739839598768698f644",
"T3T1_cs_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "17d88765a91d0123005233bbc9f943d67a021a7eb424f161aa80af98dd64d462",
@ -17030,7 +17030,7 @@
"T3T1_cs_test_msg_changepin_t2.py::test_change_failed": "1c81b081fb05d47982bdd595e3f71d4a911530e08dadfd5fbfe118421d0a66e7",
"T3T1_cs_test_msg_changepin_t2.py::test_change_invalid_current": "e508b79f30baea0c05b6989aafcda142c1eb4751af38b3a0bfea05e30bb8d56d",
"T3T1_cs_test_msg_changepin_t2.py::test_change_pin": "7e6a3b80d28d7d3496ab9e94c392b3a1a66a618fbd12d2552a327f8d017d5586",
"T3T1_cs_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "d57405f5be43c7d0771c6153bb27e6abeca32cd50b0f048a4010d5aa15e3182a",
"T3T1_cs_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "5435aba8f069b05c13fd1570cc39fe108195256267747d4cc1143b6789394a72",
"T3T1_cs_test_msg_changepin_t2.py::test_remove_pin": "730b9ea97861bb3a2d375ab44ebeb81418949966246946ca906c9135e0cdbdf0",
"T3T1_cs_test_msg_changepin_t2.py::test_set_failed": "69928e6b1ff42739a3dbdc07f6ef6ac0aae3b6983dac3018b03b325b96462f87",
"T3T1_cs_test_msg_changepin_t2.py::test_set_pin": "ebf8a6aac47b252d84ad970de97f83ad0f8e00fe5baef5d1cf46362900b5adba",
@ -17043,7 +17043,7 @@
"T3T1_cs_test_msg_sd_protect.py::test_enable_disable": "d761f9a9ba40ca640572cbb95daaefdd8a6fb828f91547b41ae9e9cc4b071bdc",
"T3T1_cs_test_msg_sd_protect.py::test_refresh": "3ba6fe4f822ad730666aa7df1b129c49432fec58819d843368308aafbbdb4f7b",
"T3T1_cs_test_msg_sd_protect.py::test_wipe": "404eeec69de878aaad93ad8bf14c5614daa704ff84220ef288e4f5975fef2ea3",
"T3T1_cs_test_msg_wipedevice.py::test_autolock_not_retained": "35d5299b8abb4b410bbb37e808a588b3f57191739ce433a23c9c671b0230a84d",
"T3T1_cs_test_msg_wipedevice.py::test_autolock_not_retained": "1ba139c8e4297f637d67bb5338794fe012e2b4b907eadb06323e5b72e39509cc",
"T3T1_cs_test_msg_wipedevice.py::test_wipe_device": "2f03f2f391cb5aa5869185bd3ec61c6950ab1840df7d080374da4cbbc0fcd5a3",
"T3T1_cs_test_passphrase_slip39_advanced.py::test_128bit_passphrase": "a13cb96104505549e4ca94f033982cfdd70638c9b99aee3aba62bcdb77c2cd8b",
"T3T1_cs_test_passphrase_slip39_advanced.py::test_256bit_passphrase": "a13cb96104505549e4ca94f033982cfdd70638c9b99aee3aba62bcdb77c2cd8b",
@ -18179,12 +18179,12 @@
"T3T1_de_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "f0fc86ffb3f74456a9c6282789bb4f71a319fc125da5fd5616636e311fd23eae",
"T3T1_de_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "a04bf47e9b8ad1f7bc6545f6a2049fb0bb4211f66d340a501367eef323a173ab",
"T3T1_de_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "093169b644e8cc008ea7caead9d2b1c59c18805359488924c501f90e6f5cd5b6",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "ed212433613715e7aaa5a80bb0b2111dc8988487820cc83d239059b7e8cd7584",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "430a25654df0daf86c5ca74296ec129640aacfd5694810b1ca98cc97a8ed3bcc",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "eeb3d9c92d56d99254a6bf9045da0547b4461a8b7b311eae5b12fc434be19d61",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "363a46acab4a0ac5cf5fa8733848a5ffb135740e9c99a4d15a06beba2301ae6e",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "2ad7b98efd5be80e34d1f37323b2206813e294e162ac3e8b79f69cf022868821",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "fe3e24d59b677a462d31cc44549d57101f439ad380114dfc027e9ff71d8863c1",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "a56344d6a6c46392d9565ce735add8e44e8f88b7eef39a2377f89a4463e3121f",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "275d962f906bcbef3246543bcf4621debc70f0d3cd79e9a0e3d543bec4361545",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "b5f858d874b814ce5a35fc1cf50cd0aafc521962d9d91c57c342813d7146f926",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "05883449c72ccfbc18fafcf0ceab1706bcad730508d5bda3d52966d8cbef6c6d",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "ce55afaceb64b9996cbb9235f3f9ec8e61fca5a1fb3c147f32c3796931ffa395",
"T3T1_de_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "56baa91ccc304e729befbd8e086eecfaf88560babddcc8920f8f9096688dc398",
"T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "fba566a05fc63d6d2308dfad2a31bb12b35fbc77a7a8d89f252c4626234f8a32",
"T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "d8bd2de90bec49d4175cab2f5104193f754f672c99efc83a0692c7d13157a7e7",
"T3T1_de_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "8ae6882dd2413c176b79798b2ed43bec9467e0635817ea8b848f04d91a7af7e4",
@ -18415,7 +18415,7 @@
"T3T1_de_test_msg_changepin_t2.py::test_change_failed": "4cd2cca8836ebc77b09313f05f905d2b9f11c8684b2191242fbef92692433337",
"T3T1_de_test_msg_changepin_t2.py::test_change_invalid_current": "306ce6895ef75875f16e4f35b3c5f05a004bdd67967faad504402d772b2f0cef",
"T3T1_de_test_msg_changepin_t2.py::test_change_pin": "bb540789300781e12ec7b711f142ccfcf2f79d6c5e7ae98aaaba54cabf722e5d",
"T3T1_de_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "d708ac7f0bd9a5a640c68b6c0540a2f5c7414a93dbd6bcc9979bed6faac11a36",
"T3T1_de_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "8213ade8e7cfd4e496e6f28dc5a4c98b924a09be32930deb694eb1467bc5d490",
"T3T1_de_test_msg_changepin_t2.py::test_remove_pin": "367e749689b96e5b0e3fa39ff8f5378abb4fac06cce6f78abdf393a5f6587d73",
"T3T1_de_test_msg_changepin_t2.py::test_set_failed": "894b365102246efd03f8fe53dcfbaa1808d6f77b846b91ef62b5da2d66b4d60a",
"T3T1_de_test_msg_changepin_t2.py::test_set_pin": "16c1f995ca4a929e2b4ca18324039906b831f9f9e8c969b9db008351d03f92cd",
@ -18428,7 +18428,7 @@
"T3T1_de_test_msg_sd_protect.py::test_enable_disable": "fe8a1f97e89304079cc295b418d5eac75ee441ccab46e0207db549863849b477",
"T3T1_de_test_msg_sd_protect.py::test_refresh": "c643c038a11b3a301b20316fa0d2837c69841f5e074d9bdd0de0841d1f7b18d0",
"T3T1_de_test_msg_sd_protect.py::test_wipe": "619f5d025dfd71d0e15ba018c7ecfd844e6f43be7896cf612a003398993462f0",
"T3T1_de_test_msg_wipedevice.py::test_autolock_not_retained": "49e224d28671db584b0ed3524d8efb4f414bc1f03231b838172598f27886f755",
"T3T1_de_test_msg_wipedevice.py::test_autolock_not_retained": "96fd8ddab0dbb82d6d0c116aea86f7f2eeb63372a084cdadb8c898de4d1461ec",
"T3T1_de_test_msg_wipedevice.py::test_wipe_device": "92363bb8c8f23b9f81f1ce004de2e911fc517b17f63c7866047e257e414342c1",
"T3T1_de_test_passphrase_slip39_advanced.py::test_128bit_passphrase": "0807342605b4bbcb2a62d17f1bb8468f89ad7242d8d5523151b7f0f91c68663c",
"T3T1_de_test_passphrase_slip39_advanced.py::test_256bit_passphrase": "0807342605b4bbcb2a62d17f1bb8468f89ad7242d8d5523151b7f0f91c68663c",
@ -19564,12 +19564,12 @@
"T3T1_en_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "974c222954a10307ffcdcf4f2cfc458443d45f6158293ea9dfb91e613d5eaa63",
"T3T1_en_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "0dcdbc898a66346a2781b20eed15ea9d2abe4ba6d968160d808f36bda4cc17a0",
"T3T1_en_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "a805d3037b5eb7a4a87d264aa6d2a97097509f4b5d0a4d4c030321b741ad3ef3",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "e669290e2feef97631bfab5fb0d04401c5e5f00acbb2d259393c874934d1b65d",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "7e7c5cd831de5a6bb145f68bcb8b7f75e944242ba076caa1d9dc57411d5b5c56",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "d00dcc98c2ae1cfa0d36c9c6067839b88d32c29728883140d5b47fba84e21c11",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "cbc02f1465fe548c6a8d4bfbcdcbce11f84aab8331ef61780978be4f70fbf139",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "744cd1e9a722d50a1c841940f707292ad235df72ab404d72711f3eafef556df9",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "a77e280cc35809d034b7809a5556f642238b8c54b012bd1fc00e2b7525294366",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "7bed2cc8a8c0e35148b48d2323e4fcded2b2cc35cfdf3063f89410d6b4029e58",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced_Ext-10ea47d6": "66054413e53b696cdd65ac3274794bfb8943125af6b08db88f8c19a8c9533fcd",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic_Extend-5dbe8b0f": "68bfe53e51bccd3efdc1c4c6e1e14ce46756648307f336fdc3b9aa062b13601a",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "a03996ea71e63ced4ba94d24bea15a7ee63a3798dd1a145a4f7b36accc838f4a",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced_Extend-8b11c1dc": "3fec77c8c814c4dfeaeb35a8207cd6e001bdd84c3d103bf3f7b6d3c33532c260",
"T3T1_en_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic_Extendabl-cc19e908": "94b9b9477ed2c637f2356e3e27da01ebadeafa5b4d90d35d9ea3a63f91043ecc",
"T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "24e59539ea9627589fb6f660d90a5c61e1a7f91c50540cf43c360c5162dc85b6",
"T3T1_en_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "9bfefe78931ad0662da0d3a811b90986ac615b306fbab341db6a93606e703b4d",
@ -19800,7 +19800,7 @@
"T3T1_en_test_msg_changepin_t2.py::test_change_failed": "f3c671c723ce01ca2ccea0bfa4d07b442634d0fd7d5bbc649966309d8998034d",
"T3T1_en_test_msg_changepin_t2.py::test_change_invalid_current": "ea829f2c9af3d95e87fe72448b50fcc07fb2a72235811706f7394cf96f3fd2a9",
"T3T1_en_test_msg_changepin_t2.py::test_change_pin": "5293c5f59d99d5d741afafa202e4ca20f1d89c50e9d6b51adc2cf12f7ab151e3",
"T3T1_en_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "dc7a0e78034630a77c8e1a204f6df95a40df66b34dd1f764e89151174826b3ee",
"T3T1_en_test_msg_changepin_t2.py::test_pin_menu_cancel_setup": "c0501e1a2c84d9311ec207d3e819880fa8fee09de9b242e0fef1bc923b9bd1d6",
"T3T1_en_test_msg_changepin_t2.py::test_remove_pin": "f4fb7e28e42a490ac1d970eb91be9ee07faa2dd4b3b0c0d8ec70e2fb31922eb9",
"T3T1_en_test_msg_changepin_t2.py::test_set_failed": "0ebc831cb8454d21ff85175b67599c51bdc3af2018e2555ab786460d7c758bb8",
"T3T1_en_test_msg_changepin_t2.py::test_set_pin": "be7f33fa7cbe3b8601c974a1c313f8efd13a0507e6f7e4667899da8263d5a391",
@ -19813,7 +19813,7 @@
"T3T1_en_test_msg_sd_protect.py::test_enable_disable": "aca3981d378d8ed504c5a4475941af91b57eb78d919a1c9c452fd437af696f78",
"T3T1_en_test_msg_sd_protect.py::test_refresh": "b0a88717e57ee4b5af18527d282a8ee4f0f68563f70f4c0fd7165b145018f5d4",
"T3T1_en_test_msg_sd_protect.py::test_wipe": "7482be64b3596739a22d970843085e1433b510e735fca6769e95382e5f4d0a65",
"T3T1_en_test_msg_wipedevice.py::test_autolock_not_retained": "e7370d47a0d56e88fd4c59b3519a35ca7303b8915ce4e3ed16cdd7a03914592f",
"T3T1_en_test_msg_wipedevice.py::test_autolock_not_retained": "2428552c5d27675cbfe2c19f01c317fbd7891b785badb59b21e739198f4e64a0",
"T3T1_en_test_msg_wipedevice.py::test_wipe_device": "7c70b9ef9a09cec7bbdb4ee343b6d54bfe8e013dea28ab40894a72f8625ccea6",
"T3T1_en_test_passphrase_slip39_advanced.py::test_128bit_passphrase": "2e8ce270ebe538a0576f6faceda02cc1b7e558be2a945b9798c17f7c60f33e72",
"T3T1_en_test_passphrase_slip39_advanced.py::test_256bit_passphrase": "2e8ce270ebe538a0576f6faceda02cc1b7e558be2a945b9798c17f7c60f33e72",
@ -22670,7 +22670,7 @@
"T3T1_en_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.PromptTempora-b3d21f4a": "22a4b35e9ffca23d723285ccbdde8786637fb5bdc15b45d1be4677c9b0f940f6",
"T3T1_en_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.Strict-Safety-f1ff9c26": "43ece0c802dffed4f0982daf8802de8df2a29ca5ab16c5390475f697d1ac61fb",
"T3T1_en_test_shamir_persistence.py::test_abort": "699a1450e491a9059926afded4c4bac2fc20bc125adc6508e446457052015014",
"T3T1_en_test_shamir_persistence.py::test_recovery_multiple_resets": "eb7f05a015e78939669902d95f47e8312d4312a364168343e51969ed03e6dec1",
"T3T1_en_test_shamir_persistence.py::test_recovery_multiple_resets": "6da13841e94b968ebf1871dd18e20b73ff464f07eb21ebf6d510675505b22945",
"T3T1_en_test_shamir_persistence.py::test_recovery_on_old_wallet": "a4c00fba813a30af023ce02cf7f554f736852f15f68241d88e1a1085cbc23b03",
"T3T1_en_test_shamir_persistence.py::test_recovery_single_reset": "7c1d0aa16d3a4c19815262ef18bf53796d3cefa5d71f19775ddf95acc153055a",
"T3T1_en_test_wipe_code.py::test_wipe_code_activate_core": "74483206af4a9ffa769eb4ee55f44ad4a9ab9429ca76169f8e01ee3d496826b4"