1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

fix(core/mercury): adjust swipe effect direction when animating transition through python

This commit is contained in:
tychovrahe 2024-06-18 12:50:01 +02:00 committed by matejcik
parent 2de8acc141
commit a4ff76e840
16 changed files with 162 additions and 44 deletions

View File

@ -0,0 +1 @@
[T3T1] Improve swipe behavior and animations

View File

@ -242,6 +242,7 @@ static void _librust_qstrs(void) {
MP_QSTR_flow_show_share_words;
MP_QSTR_flow_warning_hi_prio;
MP_QSTR_get_language;
MP_QSTR_get_transition_out;
MP_QSTR_haptic_feedback__disable;
MP_QSTR_haptic_feedback__enable;
MP_QSTR_haptic_feedback__subtitle;

View File

@ -212,6 +212,7 @@ where
pub struct Root<T> {
inner: Option<Child<T>>,
marked_for_clear: bool,
transition_out: Option<AttachType>,
}
impl<T> Root<T> {
@ -219,6 +220,7 @@ impl<T> Root<T> {
Self {
inner: Some(Child::new(component)),
marked_for_clear: true,
transition_out: None,
}
}
@ -246,6 +248,10 @@ impl<T> Root<T> {
self.marked_for_clear = true;
}
pub fn get_transition_out(&self) -> Option<AttachType> {
self.transition_out
}
pub fn delete(&mut self) {
self.inner = None;
}
@ -270,6 +276,11 @@ where
assert!(paint_msg.is_none());
assert!(dummy_ctx.timers.is_empty());
}
if let Some(t) = ctx.get_transition_out() {
self.transition_out = Some(t);
}
msg
}
@ -528,6 +539,7 @@ pub struct EventCtx {
root_repaint_requested: bool,
swipe_disable_req: bool,
swipe_enable_req: bool,
transition_out: Option<AttachType>,
}
impl EventCtx {
@ -557,6 +569,7 @@ impl EventCtx {
root_repaint_requested: false,
swipe_disable_req: false,
swipe_enable_req: false,
transition_out: None,
}
}
@ -658,6 +671,7 @@ impl EventCtx {
self.root_repaint_requested = false;
self.swipe_disable_req = false;
self.swipe_enable_req = false;
self.transition_out = None;
}
fn register_timer(&mut self, token: TimerToken, deadline: Duration) {
@ -680,4 +694,12 @@ impl EventCtx {
.unwrap_or(Self::STARTING_TIMER_TOKEN);
token
}
pub fn set_transition_out(&mut self, attach_type: AttachType) {
self.transition_out = Some(attach_type);
}
pub fn get_transition_out(&self) -> Option<AttachType> {
self.transition_out
}
}

View File

@ -5,7 +5,7 @@ use crate::ui::{
shape::Renderer,
};
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq, ToPrimitive, FromPrimitive)]
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
pub enum SwipeDirection {
Up,

View File

@ -5,7 +5,7 @@ use crate::{
base::AttachType, swipe_detect::SwipeSettings, Component, Event, EventCtx, SwipeDetect,
SwipeDetectMsg, SwipeDirection,
},
event::SwipeEvent,
event::{SwipeEvent, TouchEvent},
flow::{base::Decision, FlowMsg, FlowState, FlowStore},
geometry::Rect,
shape::Renderer,
@ -107,6 +107,7 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let mut decision: Decision<Q> = Decision::Nothing;
let mut return_transition: AttachType = AttachType::Initial;
let mut attach = false;
@ -139,6 +140,8 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
decision = self.handle_swipe_child(ctx, dir);
}
return_transition = AttachType::Swipe(dir);
let states_num = self.internal_pages;
if states_num > 0 {
if config.has_horizontal_pages() {
@ -230,6 +233,7 @@ impl<Q: FlowState, S: FlowStore> Component for SwipeFlow<Q, S> {
None
}
Decision::Return(msg) => {
ctx.set_transition_out(return_transition);
self.swipe.reset();
self.allow_swipe = true;
Some(msg)

View File

@ -2,6 +2,7 @@ use core::{
cell::RefCell,
convert::{TryFrom, TryInto},
};
use num_traits::{FromPrimitive, ToPrimitive};
use crate::{
error::Error,
@ -9,7 +10,7 @@ use crate::{
micropython::{
buffer::StrBuffer,
gc::Gc,
macros::{obj_dict, obj_fn_1, obj_fn_2, obj_fn_var, obj_map, obj_type},
macros::{obj_dict, obj_fn_1, obj_fn_2, obj_fn_3, obj_fn_var, obj_map, obj_type},
map::Map,
obj::{Obj, ObjBase},
qstr::Qstr,
@ -32,9 +33,33 @@ use crate::ui::{display::Color, shape::render_on_display};
#[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::{component::SwipeDirection, event::TouchEvent};
impl AttachType {
fn to_obj(self) -> Obj {
match self {
Self::Initial => Obj::const_none(),
#[cfg(feature = "touch")]
Self::Swipe(dir) => dir.to_u8().into(),
}
}
fn try_from_obj(obj: Obj) -> Result<Self, Error> {
if obj == Obj::const_none() {
return Ok(Self::Initial);
}
#[cfg(feature = "touch")]
{
let dir: u8 = obj.try_into()?;
return Ok(AttachType::Swipe(
SwipeDirection::from_u8(dir).ok_or(Error::TypeError)?,
));
}
#[allow(unreachable_code)]
Err(Error::TypeError)
}
}
/// Conversion trait implemented by components that know how to convert their
/// message values into MicroPython `Obj`s.
@ -57,6 +82,7 @@ pub trait ObjComponent: MaybeTrace {
fn obj_skip_paint(&mut self) {}
fn obj_request_clear(&mut self) {}
fn obj_delete(&mut self) {}
fn obj_get_transition_out(&self) -> Result<Obj, Error>;
}
impl<T> ObjComponent for Root<T>
@ -112,6 +138,14 @@ where
fn obj_delete(&mut self) {
self.delete()
}
fn obj_get_transition_out(&self) -> Result<Obj, Error> {
if let Some(msg) = self.get_transition_out() {
Ok(msg.to_obj())
} else {
Ok(Obj::const_none())
}
}
}
/// `LayoutObj` is a GC-allocated object exported to MicroPython, with type
@ -278,6 +312,14 @@ impl LayoutObj {
}
}
fn obj_get_transition_out(&self) -> Result<Obj, Error> {
let inner = &mut *self.inner.borrow_mut();
// Get transition out result
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
unsafe { Gc::as_mut(&mut inner.root) }.obj_get_transition_out()
}
#[cfg(feature = "ui_debug")]
fn obj_bounds(&self) {
use crate::ui::display;
@ -299,7 +341,7 @@ impl LayoutObj {
static TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_LayoutObj,
locals: &obj_dict!(obj_map! {
Qstr::MP_QSTR_attach_timer_fn => obj_fn_2!(ui_layout_attach_timer_fn).as_obj(),
Qstr::MP_QSTR_attach_timer_fn => obj_fn_3!(ui_layout_attach_timer_fn).as_obj(),
Qstr::MP_QSTR_touch_event => obj_fn_var!(4, 4, ui_layout_touch_event).as_obj(),
Qstr::MP_QSTR_button_event => obj_fn_var!(3, 3, ui_layout_button_event).as_obj(),
Qstr::MP_QSTR_progress_event => obj_fn_var!(3, 3, ui_layout_progress_event).as_obj(),
@ -312,6 +354,7 @@ impl LayoutObj {
Qstr::MP_QSTR___del__ => obj_fn_1!(ui_layout_delete).as_obj(),
Qstr::MP_QSTR_page_count => obj_fn_1!(ui_layout_page_count).as_obj(),
Qstr::MP_QSTR_button_request => obj_fn_1!(ui_layout_button_request).as_obj(),
Qstr::MP_QSTR_get_transition_out => obj_fn_1!(ui_layout_get_transition_out).as_obj(),
}),
};
&TYPE
@ -378,11 +421,12 @@ impl TryFrom<Never> for Obj {
}
}
extern "C" fn ui_layout_attach_timer_fn(this: Obj, timer_fn: Obj) -> Obj {
extern "C" fn ui_layout_attach_timer_fn(this: Obj, timer_fn: Obj, attach_type: 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(AttachType::Initial))?;
let msg = this.obj_event(Event::Attach(AttachType::try_from_obj(attach_type)?))?;
assert!(msg == Obj::const_none());
Ok(Obj::const_none())
};
@ -510,6 +554,14 @@ extern "C" fn ui_layout_button_request(this: Obj) -> Obj {
unsafe { util::try_or_raise(block) }
}
extern "C" fn ui_layout_get_transition_out(this: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;
this.obj_get_transition_out()
};
unsafe { util::try_or_raise(block) }
}
#[cfg(feature = "ui_debug")]
#[no_mangle]
pub extern "C" fn ui_debug_layout_type() -> &'static Type {

View File

@ -45,6 +45,9 @@ impl AttachAnimation {
);
match attach_type {
Some(AttachType::Initial) => {
Offset::lerp(Offset::new(0, -20), Offset::zero(), value.eval(t))
}
Some(AttachType::Swipe(dir)) => match dir {
SwipeDirection::Up => {
Offset::lerp(Offset::new(0, 20), Offset::zero(), value.eval(t))
@ -66,7 +69,8 @@ impl AttachAnimation {
pareen::constant(1.0),
);
match attach_type {
Some(AttachType::Swipe(SwipeDirection::Up))
Some(AttachType::Initial)
| Some(AttachType::Swipe(SwipeDirection::Up))
| Some(AttachType::Swipe(SwipeDirection::Down)) => {}
_ => {
return 255;
@ -89,9 +93,9 @@ pub struct SwipeContent<T> {
bounds: Rect,
progress: i16,
dir: SwipeDirection,
normal: Option<AttachType>,
attach_animation: AttachAnimation,
attach_type: Option<AttachType>,
show_attach_anim: bool,
}
impl<T: Component> SwipeContent<T> {
@ -101,17 +105,15 @@ impl<T: Component> SwipeContent<T> {
bounds: Rect::zero(),
progress: 0,
dir: SwipeDirection::Up,
normal: Some(AttachType::Swipe(SwipeDirection::Down)),
attach_animation: AttachAnimation::default(),
attach_type: None,
show_attach_anim: true,
}
}
pub fn with_normal_attach(self, attach_type: Option<AttachType>) -> Self {
Self {
normal: attach_type,
..self
}
pub fn with_no_attach_anim(mut self) -> Self {
self.show_attach_anim = false;
self
}
pub fn inner(&self) -> &T {
@ -130,9 +132,7 @@ impl<T: Component> Component for SwipeContent<T> {
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 {
if self.show_attach_anim {
self.attach_type = Some(attach_type);
}
self.attach_animation.reset();

View File

@ -1,6 +1,6 @@
use crate::ui::{
component::{Component, Event, EventCtx, SwipeDetect, SwipeDetectMsg},
event::SwipeEvent,
component::{base::AttachType, Component, Event, EventCtx, SwipeDetect, SwipeDetectMsg},
event::{SwipeEvent},
flow::Swipable,
geometry::Rect,
shape::Renderer,
@ -49,7 +49,8 @@ impl<T: Swipable + Component> Component for SwipeUpScreen<T> {
.swipe
.event(ctx, event, self.content.get_swipe_config())
{
Some(SwipeDetectMsg::Trigger(_)) => {
Some(SwipeDetectMsg::Trigger(dir)) => {
ctx.set_transition_out(AttachType::Swipe(dir));
return Some(SwipeUpScreenMsg::Swiped);
}
Some(SwipeDetectMsg::Move(dir, progress)) => {

View File

@ -20,7 +20,7 @@ use crate::{
ui::{
backlight::BACKLIGHT_LEVELS_OBJ,
component::{
base::{AttachType, ComponentExt},
base::ComponentExt,
connect::Connect,
swipe_detect::SwipeSettings,
text::{
@ -811,7 +811,7 @@ extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map
let content = StatusScreen::new_success();
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content).with_normal_attach(None))
Frame::left_aligned(title, SwipeContent::new(content).with_no_attach_anim())
.with_footer(TR::instructions__swipe_up.into(), description)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
@ -1070,11 +1070,7 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
.with_done_offset(theme::CHECKLIST_DONE_OFFSET);
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(
title,
SwipeContent::new(checklist_content)
.with_normal_attach(Some(AttachType::Swipe(SwipeDirection::Up))),
)
Frame::left_aligned(title, SwipeContent::new(checklist_content))
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
@ -1317,12 +1313,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
///
/// T = TypeVar("T")
///
/// class AttachType:
/// ...
///
/// class LayoutObj(Generic[T]):
/// """Representation of a Rust-based layout object.
/// see `trezor::ui::layout::obj::LayoutObj`.
/// """
///
/// def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
/// def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None:
/// """Attach a timer setter function.
///
/// The layout object can call the timer setter with two arguments,
@ -1379,6 +1378,9 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def page_count(self) -> int:
/// """Return the number of pages in the layout object."""
///
/// def get_transition_out(self) -> AttachType:
/// """Return the transition type."""
///
/// def __del__(self) -> None:
/// """Calls drop on contents of the root component."""
///

View File

@ -1625,12 +1625,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
///
/// T = TypeVar("T")
///
/// class AttachType:
/// ...
///
/// class LayoutObj(Generic[T]):
/// """Representation of a Rust-based layout object.
/// see `trezor::ui::layout::obj::LayoutObj`.
/// """
///
/// def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
/// def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None:
/// """Attach a timer setter function.
///
/// The layout object can call the timer setter with two arguments,
@ -1690,6 +1693,9 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def button_request(self) -> tuple[int, str] | None:
/// """Return (code, type) of button request made during the last event or timer pass."""
///
/// def get_transition_out(self) -> AttachType:
/// """Return the transition type."""
///
/// def __del__(self) -> None:
/// """Calls drop on contents of the root component."""
///

View File

@ -3,12 +3,17 @@ from trezor import utils
T = TypeVar("T")
# rust/src/ui/model_mercury/layout.rs
class AttachType:
...
# rust/src/ui/model_mercury/layout.rs
class LayoutObj(Generic[T]):
"""Representation of a Rust-based layout object.
see `trezor::ui::layout::obj::LayoutObj`.
"""
def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None:
"""Attach a timer setter function.
The layout object can call the timer setter with two arguments,
`token` and `deadline`. When `deadline` is reached, the layout object
@ -49,6 +54,8 @@ class LayoutObj(Generic[T]):
"""Paint bounds of individual components on screen."""
def page_count(self) -> int:
"""Return the number of pages in the layout object."""
def get_transition_out(self) -> AttachType:
"""Return the transition type."""
def __del__(self) -> None:
"""Calls drop on contents of the root component."""
@ -1083,12 +1090,17 @@ from trezor import utils
T = TypeVar("T")
# rust/src/ui/model_tt/layout.rs
class AttachType:
...
# rust/src/ui/model_tt/layout.rs
class LayoutObj(Generic[T]):
"""Representation of a Rust-based layout object.
see `trezor::ui::layout::obj::LayoutObj`.
"""
def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None:
"""Attach a timer setter function.
The layout object can call the timer setter with two arguments,
`token` and `deadline`. When `deadline` is reached, the layout object
@ -1131,6 +1143,8 @@ class LayoutObj(Generic[T]):
"""Return the number of pages in the layout object."""
def button_request(self) -> tuple[int, str] | None:
"""Return (code, type) of button request made during the last event or timer pass."""
def get_transition_out(self) -> AttachType:
"""Return the transition type."""
def __del__(self) -> None:
"""Calls drop on contents of the root component."""

View File

@ -9,7 +9,7 @@ from trezorui2 import BacklightLevels
if TYPE_CHECKING:
from typing import Generic, TypeVar
from trezorui2 import UiResult # noqa: F401
from trezorui2 import AttachType, UiResult # noqa: F401
T = TypeVar("T")
@ -34,6 +34,9 @@ layout_chan = loop.chan()
# allow only one alert at a time to avoid alerts overlapping
_alert_in_progress = False
# storing last transition type, so that next layout can continue nicely
LAST_TRANSITION_OUT: AttachType | None = None
# in debug mode, display an indicator in top right corner
if __debug__:
@ -131,6 +134,12 @@ class Layout(Generic[T]):
raised, usually from some of the child components.
"""
def finalize(self) -> None:
"""
Called when the layout is done. Usually overridden to allow cleanup or storing context.
"""
pass
async def __iter__(self) -> T:
"""
Run the layout and wait until it completes. Returns the result value.
@ -159,6 +168,8 @@ class Layout(Generic[T]):
except Result as result:
# Result exception was raised, this means this layout is complete.
value = result.value
finally:
self.finalize()
return value
if TYPE_CHECKING:

View File

@ -36,7 +36,7 @@ class RustLayout(ui.Layout):
self.br_chan = loop.chan()
self.layout = layout
self.timer = loop.Timer()
self.layout.attach_timer_fn(self.set_timer)
self.layout.attach_timer_fn(self.set_timer, ui.LAST_TRANSITION_OUT)
self._send_button_request()
self.backlight_level = ui.BacklightLevels.NORMAL
@ -191,6 +191,7 @@ class RustLayout(ui.Layout):
)
def _first_paint(self) -> None:
ui.backlight_fade(ui.BacklightLevels.NONE)
self._paint()
@ -213,7 +214,7 @@ class RustLayout(ui.Layout):
notify_layout_change(self, event_id)
# Turn the brightness on again.
# Fade brightness to desired level
ui.backlight_fade(self.backlight_level)
def handle_input_and_rendering(self) -> loop.Task:
@ -260,13 +261,16 @@ class RustLayout(ui.Layout):
br_code, br_type = res
self.br_chan.publish((br_code, br_type, self.layout.page_count()))
def finalize(self):
ui.LAST_TRANSITION_OUT = self.layout.get_transition_out()
def draw_simple(layout: Any) -> None:
# Simple drawing not supported for layouts that set timers.
def dummy_set_timer(token: int, deadline: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer)
layout.attach_timer_fn(dummy_set_timer, None)
ui.backlight_fade(ui.BacklightLevels.DIM)
layout.paint()
ui.refresh()

View File

@ -35,7 +35,7 @@ class RustProgress:
):
self.layout = layout
ui.backlight_fade(ui.BacklightLevels.DIM)
self.layout.attach_timer_fn(self.set_timer)
self.layout.attach_timer_fn(self.set_timer, None)
if self.layout.paint():
ui.refresh()
ui.backlight_fade(ui.BacklightLevels.NORMAL)

View File

@ -41,7 +41,7 @@ class RustLayout(LayoutParentType[T]):
self.br_chan = loop.chan()
self.layout = layout
self.timer = loop.Timer()
self.layout.attach_timer_fn(self.set_timer)
self.layout.attach_timer_fn(self.set_timer, None)
self._send_button_request()
def __del__(self):
@ -309,7 +309,7 @@ def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
def dummy_set_timer(token: int, deadline: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer)
layout.attach_timer_fn(dummy_set_timer, None)
layout.paint()
ui.refresh()

View File

@ -42,7 +42,7 @@ class RustLayout(LayoutParentType[T]):
self.br_chan = loop.chan()
self.layout = layout
self.timer = loop.Timer()
self.layout.attach_timer_fn(self.set_timer)
self.layout.attach_timer_fn(self.set_timer, None)
self._send_button_request()
self.backlight_level = ui.BacklightLevels.NORMAL
@ -272,7 +272,7 @@ def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
def dummy_set_timer(token: int, deadline: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer)
layout.attach_timer_fn(dummy_set_timer, None)
ui.backlight_fade(ui.BacklightLevels.DIM)
layout.paint()
ui.refresh()