1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-21 22:08:08 +00:00

fix(core/ui): T3T1: remove ButtonPage, Dialog, IconDialog

[no changelog]
This commit is contained in:
Martin Milata 2024-06-05 00:27:48 +02:00 committed by matejcik
parent e268f79749
commit 4d6af487f4
13 changed files with 189 additions and 1774 deletions

View File

@ -15,7 +15,7 @@ pub struct SwipePage<T> {
current: usize,
}
impl<T: Component + Paginate + Clone> SwipePage<T> {
impl<T: Component + Paginate> SwipePage<T> {
pub fn vertical(inner: T) -> Self {
Self {
inner,
@ -37,7 +37,7 @@ impl<T: Component + Paginate + Clone> SwipePage<T> {
}
}
impl<T: Component + Paginate + Clone> Component for SwipePage<T> {
impl<T: Component + Paginate> Component for SwipePage<T> {
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {

View File

@ -1,230 +0,0 @@
use crate::{
strutil::TString,
ui::{
component::{
image::BlendedImage,
text::{
paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
TextStyle,
},
Child, Component, Event, EventCtx, Never,
},
geometry::{Insets, LinearPlacement, Rect},
shape::Renderer,
},
};
use super::theme;
pub enum DialogMsg<T, U> {
Content(T),
Controls(U),
}
pub struct Dialog<T, U> {
content: Child<T>,
controls: Child<U>,
}
impl<T, U> Dialog<T, U>
where
T: Component,
U: Component,
{
pub fn new(content: T, controls: U) -> Self {
Self {
content: Child::new(content),
controls: Child::new(controls),
}
}
pub fn inner(&self) -> &T {
self.content.inner()
}
}
impl<T, U> Component for Dialog<T, U>
where
T: Component,
U: Component,
{
type Msg = DialogMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let controls_area = self.controls.place(bounds);
let content_area = bounds
.inset(Insets::bottom(controls_area.height()))
.inset(Insets::bottom(theme::BUTTON_SPACING))
.inset(Insets::left(theme::CONTENT_BORDER));
self.content.place(content_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.content
.event(ctx, event)
.map(Self::Msg::Content)
.or_else(|| self.controls.event(ctx, event).map(Self::Msg::Controls))
}
fn paint(&mut self) {
self.content.paint();
self.controls.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target);
self.controls.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.content.bounds(sink);
self.controls.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Dialog<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Dialog");
t.child("content", &self.content);
t.child("controls", &self.controls);
}
}
pub struct IconDialog<U> {
image: Child<BlendedImage>,
paragraphs: Paragraphs<ParagraphVecShort<'static>>,
controls: Child<U>,
}
impl<U> IconDialog<U>
where
U: Component,
{
pub fn new(icon: BlendedImage, title: impl Into<TString<'static>>, controls: U) -> Self {
Self {
image: Child::new(icon),
paragraphs: Paragraphs::new(ParagraphVecShort::from_iter([Paragraph::new(
&theme::TEXT_DEMIBOLD,
title,
)
.centered()]))
.with_placement(
LinearPlacement::vertical()
.align_at_center()
.with_spacing(Self::VALUE_SPACE),
),
controls: Child::new(controls),
}
}
pub fn with_paragraph(mut self, para: Paragraph<'static>) -> Self {
if !para.content().is_empty() {
self.paragraphs.inner_mut().add(para);
}
self
}
pub fn with_text(self, style: &'static TextStyle, text: impl Into<TString<'static>>) -> Self {
self.with_paragraph(Paragraph::new(style, text).centered())
}
pub fn with_description(self, description: impl Into<TString<'static>>) -> Self {
self.with_text(&theme::TEXT_NORMAL_GREY_EXTRA_LIGHT, description)
}
pub fn with_value(self, value: impl Into<TString<'static>>) -> Self {
self.with_text(&theme::TEXT_MONO, value)
}
pub fn new_shares(lines: [impl Into<TString<'static>>; 4], controls: U) -> Self {
let [l0, l1, l2, l3] = lines;
Self {
image: Child::new(BlendedImage::new(
theme::IMAGE_BG_CIRCLE,
theme::IMAGE_FG_SUCCESS,
theme::SUCCESS_COLOR,
theme::FG,
theme::BG,
)),
paragraphs: ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_NORMAL_GREY_EXTRA_LIGHT, l0).centered(),
Paragraph::new(&theme::TEXT_DEMIBOLD, l1).centered(),
Paragraph::new(&theme::TEXT_NORMAL_GREY_EXTRA_LIGHT, l2).centered(),
Paragraph::new(&theme::TEXT_DEMIBOLD, l3).centered(),
])
.into_paragraphs()
.with_placement(LinearPlacement::vertical().align_at_center()),
controls: Child::new(controls),
}
}
pub const ICON_AREA_PADDING: i16 = 2;
pub const ICON_AREA_HEIGHT: i16 = 60;
pub const VALUE_SPACE: i16 = 5;
}
impl<U> Component for IconDialog<U>
where
U: Component,
{
type Msg = DialogMsg<Never, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let bounds = bounds
.inset(theme::borders())
.inset(Insets::top(Self::ICON_AREA_PADDING));
let controls_area = self.controls.place(bounds);
let content_area = bounds.inset(Insets::bottom(controls_area.height()));
let (image_area, content_area) = content_area.split_top(Self::ICON_AREA_HEIGHT);
self.image.place(image_area);
self.paragraphs.place(content_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.paragraphs.event(ctx, event);
self.controls.event(ctx, event).map(Self::Msg::Controls)
}
fn paint(&mut self) {
self.image.paint();
self.paragraphs.paint();
self.controls.paint();
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.image.render(target);
self.paragraphs.render(target);
self.controls.render(target);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.image.bounds(sink);
self.paragraphs.bounds(sink);
self.controls.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<U> crate::trace::Trace for IconDialog<U>
where
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("IconDialog");
t.child("image", &self.image);
t.child("content", &self.paragraphs);
t.child("controls", &self.controls);
}
}

View File

@ -4,7 +4,6 @@ pub mod bl_confirm;
mod button;
#[cfg(feature = "translations")]
mod coinjoin_progress;
mod dialog;
mod fido;
mod footer;
mod vertical_menu;
@ -12,7 +11,6 @@ mod vertical_menu;
mod fido_icons;
mod error;
mod frame;
#[cfg(feature = "translations")]
mod hold_to_confirm;
#[cfg(feature = "translations")]
@ -23,8 +21,6 @@ mod loader;
#[cfg(feature = "translations")]
mod number_input;
pub mod number_input_slider;
#[cfg(feature = "translations")]
mod page;
mod progress;
#[cfg(feature = "translations")]
mod prompt_screen;
@ -34,7 +30,6 @@ mod scroll;
mod set_brightness;
#[cfg(feature = "translations")]
mod share_words;
mod simple_page;
mod status_screen;
mod swipe_content;
#[cfg(feature = "translations")]
@ -51,7 +46,6 @@ pub use button::{
};
#[cfg(feature = "translations")]
pub use coinjoin_progress::CoinJoinProgress;
pub use dialog::{Dialog, DialogMsg, IconDialog};
pub use error::ErrorScreen;
pub use fido::{FidoConfirm, FidoMsg};
pub use footer::Footer;
@ -74,8 +68,6 @@ pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use number_input::{NumberInputDialog, NumberInputDialogMsg};
#[cfg(feature = "translations")]
pub use number_input_slider::NumberInputSliderDialog;
#[cfg(feature = "translations")]
pub use page::ButtonPage;
pub use progress::Progress;
#[cfg(feature = "translations")]
pub use prompt_screen::PromptScreen;
@ -85,7 +77,6 @@ pub use scroll::ScrollBar;
pub use set_brightness::SetBrightnessDialog;
#[cfg(feature = "translations")]
pub use share_words::ShareWords;
pub use simple_page::SimplePage;
pub use status_screen::StatusScreen;
pub use swipe_content::SwipeContent;
#[cfg(feature = "translations")]

View File

@ -1,852 +0,0 @@
use crate::{
error::Error,
strutil::TString,
time::Instant,
translations::TR,
ui::{
component::{
paginated::PageMsg, Component, ComponentExt, Event, EventCtx, Pad, Paginate, Swipe,
SwipeDirection,
},
constant,
display::{self, Color},
geometry::{Insets, Rect},
shape::Renderer,
util::animation_disabled,
},
};
use super::{
theme, Button, ButtonContent, ButtonMsg, ButtonStyleSheet, Loader, LoaderMsg, ScrollBar,
};
use core::cell::Cell;
/// Allows pagination of inner component. Shows scroll bar, confirm & cancel
/// buttons. Optionally handles hold-to-confirm with loader.
pub struct ButtonPage<T> {
/// Inner component.
content: T,
/// Cleared when page changes.
pad: Pad,
/// Swipe controller.
swipe: Swipe,
scrollbar: ScrollBar,
/// Hold-to-confirm mode whenever this is `Some(loader)`.
loader: Option<Loader>,
button_cancel: Option<Button>,
button_confirm: Button,
button_prev: Button,
button_next: Button,
/// Show cancel button instead of back button.
cancel_from_any_page: bool,
/// Whether to pass-through left swipe to parent component.
swipe_left: bool,
/// Whether to pass-through right swipe to parent component.
swipe_right: bool,
/// Fade to given backlight level on next paint().
fade: Cell<Option<u16>>,
}
impl<T> ButtonPage<T>
where
T: Paginate,
T: Component,
{
pub fn with_hold(mut self) -> Result<Self, Error> {
self.button_confirm =
Button::with_text(TR::buttons__hold_to_confirm.into()).styled(theme::button_confirm());
self.loader = Some(Loader::new());
Ok(self)
}
}
impl<T> ButtonPage<T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
Self {
content,
pad: Pad::with_background(background),
swipe: Swipe::new(),
scrollbar: ScrollBar::vertical(),
loader: None,
button_cancel: Some(Button::with_icon(theme::ICON_CANCEL)),
button_confirm: Button::with_icon(theme::ICON_CONFIRM).styled(theme::button_confirm()),
button_prev: Button::with_icon(theme::ICON_UP).initially_enabled(false),
button_next: Button::with_icon(theme::ICON_DOWN),
cancel_from_any_page: false,
swipe_left: false,
swipe_right: false,
fade: Cell::new(None),
}
}
pub fn without_cancel(mut self) -> Self {
self.button_cancel = None;
self
}
pub fn with_cancel_confirm(
mut self,
left: Option<TString<'static>>,
right: Option<TString<'static>>,
) -> Self {
let cancel = match left {
Some(verb) => verb.map(|s| match s {
"^" => Button::with_icon(theme::ICON_UP),
"<" => Button::with_icon(theme::ICON_BACK),
_ => Button::with_text(verb),
}),
_ => Button::with_icon(theme::ICON_CANCEL),
};
let confirm = match right {
Some(verb) => Button::with_text(verb).styled(theme::button_confirm()),
_ => Button::with_icon(theme::ICON_CONFIRM).styled(theme::button_confirm()),
};
self.button_cancel = Some(cancel);
self.button_confirm = confirm;
self
}
pub fn with_back_button(mut self) -> Self {
self.cancel_from_any_page = true;
self.button_prev = Button::with_icon(theme::ICON_BACK).initially_enabled(false);
self.button_cancel = Some(Button::with_icon(theme::ICON_BACK));
self
}
pub fn with_cancel_arrow(mut self) -> Self {
self.button_cancel = Some(Button::with_icon(theme::ICON_UP));
self
}
pub fn with_confirm_style(mut self, style: ButtonStyleSheet) -> Self {
self.button_confirm = self.button_confirm.styled(style);
self
}
pub fn with_swipe_left(mut self) -> Self {
self.swipe_left = true;
self
}
pub fn with_swipe_right(mut self) -> Self {
self.swipe_right = true;
self
}
fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
self.swipe.allow_left = self.swipe_left;
self.swipe.allow_right = self.swipe_right;
}
fn change_page(&mut self, ctx: &mut EventCtx, step: isize) {
// Advance scrollbar.
self.scrollbar.go_to_relative(step);
// Adjust the swipe parameters according to the scrollbar.
self.setup_swipe();
// Enable/disable prev button.
self.button_prev
.enable_if(ctx, self.scrollbar.has_previous_page());
// Change the page in the content, make sure it gets completely repainted and
// clear the background under it.
self.content.change_page(self.scrollbar.active_page);
self.content.request_complete_repaint(ctx);
self.pad.clear();
// Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint.
self.fade
.set(Some(theme::backlight::get_backlight_normal()));
}
fn is_cancel_visible(&self) -> bool {
self.cancel_from_any_page || !self.scrollbar.has_previous_page()
}
/// Area for drawing loader (and black rectangle behind it). Can be outside
/// bounds as we repaint entire UI tree after hiding the loader.
const fn loader_area() -> Rect {
constant::screen()
.inset(theme::borders())
.inset(Insets::bottom(theme::BUTTON_HEIGHT + theme::BUTTON_SPACING))
}
fn handle_swipe(
&mut self,
ctx: &mut EventCtx,
event: Event,
) -> HandleResult<<Self as Component>::Msg> {
if let Some(swipe) = self.swipe.event(ctx, event) {
match swipe {
SwipeDirection::Up => {
// Scroll down, if possible.
return HandleResult::NextPage;
}
SwipeDirection::Down => {
// Scroll up, if possible.
return HandleResult::PrevPage;
}
SwipeDirection::Left if self.swipe_left => {
return HandleResult::Return(PageMsg::SwipeLeft);
}
SwipeDirection::Right if self.swipe_right => {
return HandleResult::Return(PageMsg::SwipeRight);
}
_ => {
// Ignore other directions.
}
}
}
HandleResult::Continue
}
fn handle_button(
&mut self,
ctx: &mut EventCtx,
event: Event,
) -> HandleResult<(Option<<Self as Component>::Msg>, Option<ButtonMsg>)> {
if self.scrollbar.has_next_page() {
if let Some(ButtonMsg::Clicked) = self.button_next.event(ctx, event) {
return HandleResult::NextPage;
}
} else {
let result = self.button_confirm.event(ctx, event);
match result {
Some(ButtonMsg::Clicked) => {
return HandleResult::Return((Some(PageMsg::Confirmed), result))
}
Some(_) => return HandleResult::Return((None, result)),
None => {}
}
}
if self.is_cancel_visible() {
if let Some(ButtonMsg::Clicked) = self.button_cancel.event(ctx, event) {
return HandleResult::Return((Some(PageMsg::Cancelled), None));
}
} else if let Some(ButtonMsg::Clicked) = self.button_prev.event(ctx, event) {
return HandleResult::PrevPage;
}
HandleResult::Continue
}
fn handle_hold(
&mut self,
ctx: &mut EventCtx,
event: Event,
button_msg: &Option<ButtonMsg>,
) -> HandleResult<<Self as Component>::Msg> {
let Some(loader) = &mut self.loader else {
return HandleResult::Continue;
};
let now = Instant::now();
if let Some(LoaderMsg::ShrunkCompletely) = loader.event(ctx, event) {
// Switch it to the initial state, so we stop painting it.
loader.reset();
// Re-draw the whole content tree.
self.content.request_complete_repaint(ctx);
// Loader overpainted our bounds, repaint entire screen from scratch.
ctx.request_repaint_root()
// This can be a result of an animation frame event, we should take
// care to not short-circuit here and deliver the event to the
// content as well.
}
match button_msg {
Some(ButtonMsg::Pressed) => {
loader.start_growing(ctx, now);
loader.pad.clear(); // Clear the remnants of the content.
}
Some(ButtonMsg::Released) => {
loader.start_shrinking(ctx, now);
}
Some(ButtonMsg::Clicked) => {
if loader.is_completely_grown(now) || animation_disabled() {
return HandleResult::Return(PageMsg::Confirmed);
} else {
loader.start_shrinking(ctx, now);
}
}
_ => {}
}
HandleResult::Continue
}
}
enum HandleResult<T> {
Return(T),
PrevPage,
NextPage,
Continue,
}
impl<T> Component for ButtonPage<T>
where
T: Paginate,
T: Component,
{
type Msg = PageMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let small_left_button = match (&self.button_cancel, &self.button_confirm) {
(None, _) => true,
(Some(cancel), confirm) => match (cancel.content(), confirm.content()) {
(ButtonContent::Text(t), _) => t.len() <= 4,
(ButtonContent::Icon(_), ButtonContent::Icon(_)) => false,
_ => true,
},
};
let layout = PageLayout::new(bounds, small_left_button);
self.pad.place(bounds);
self.swipe.place(bounds);
self.button_cancel.place(layout.button_left);
self.button_confirm.place(layout.button_right);
self.button_prev.place(layout.button_left);
self.button_next.place(layout.button_right);
self.scrollbar.place(layout.scrollbar);
// Layout the content. Try to fit it on a single page first, and reduce the area
// to make space for a scrollbar if it doesn't fit.
self.content.place(layout.content_single_page);
let page_count = {
let count = self.content.page_count();
if count > 1 {
self.content.place(layout.content);
self.content.page_count() // Make sure to re-count it with the
// new size.
} else {
count // Content fits on a single page.
}
};
if page_count == 1 && self.button_cancel.is_none() {
self.button_confirm.place(layout.button_both);
}
// Now that we finally have the page count, we can setup the scrollbar and the
// swiper.
self.scrollbar.set_count_and_active_page(page_count, 0);
self.setup_swipe();
self.loader.place(Self::loader_area());
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.scrollbar.page_count);
match self.handle_swipe(ctx, event) {
HandleResult::Return(r) => return Some(r),
HandleResult::PrevPage => {
self.change_page(ctx, -1);
return None;
}
HandleResult::NextPage => {
self.change_page(ctx, 1);
return None;
}
HandleResult::Continue => {}
}
if let Some(msg) = self.content.event(ctx, event) {
return Some(PageMsg::Content(msg));
}
let mut confirm_button_msg = None;
let mut button_result = None;
match self.handle_button(ctx, event) {
HandleResult::Return((Some(r), None)) => return Some(r),
HandleResult::Return((r, m)) => {
button_result = r;
confirm_button_msg = m;
}
HandleResult::PrevPage => {
self.change_page(ctx, -1);
return None;
}
HandleResult::NextPage => {
self.change_page(ctx, 1);
return None;
}
HandleResult::Continue => {}
}
if self.loader.is_some() {
return match self.handle_hold(ctx, event, &confirm_button_msg) {
HandleResult::Return(r) => Some(r),
HandleResult::Continue => None,
_ => unreachable!(),
};
}
button_result
}
fn paint(&mut self) {
self.pad.paint();
match &self.loader {
Some(l) if l.is_animating() => self.loader.paint(),
_ => {
self.content.paint();
if self.scrollbar.has_pages() {
self.scrollbar.paint();
}
}
}
if self.button_cancel.is_some() && self.is_cancel_visible() {
self.button_cancel.paint();
} else {
self.button_prev.paint();
}
if self.scrollbar.has_next_page() {
self.button_next.paint();
} else {
self.button_confirm.paint();
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
match &self.loader {
Some(l) if l.is_animating() => self.loader.render(target),
_ => {
self.content.render(target);
if self.scrollbar.has_pages() {
self.scrollbar.render(target);
}
}
}
if self.button_cancel.is_some() && self.is_cancel_visible() {
self.button_cancel.render(target);
} else {
self.button_prev.render(target);
}
if self.scrollbar.has_next_page() {
self.button_next.render(target);
} else {
self.button_confirm.render(target);
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
self.scrollbar.bounds(sink);
self.content.bounds(sink);
self.button_cancel.bounds(sink);
self.button_confirm.bounds(sink);
self.button_prev.bounds(sink);
self.button_next.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonPage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonPage");
t.int("active_page", self.scrollbar.active_page as i64);
t.int("page_count", self.scrollbar.page_count as i64);
t.bool("hold", self.loader.is_some());
t.child("content", &self.content);
}
}
pub struct PageLayout {
/// Content when it fits on single page (no scrollbar).
pub content_single_page: Rect,
/// Content when multiple pages.
pub content: Rect,
/// Scroll bar when multiple pages.
pub scrollbar: Rect,
/// Controls displayed on last page.
pub button_left: Rect,
pub button_right: Rect,
pub button_both: Rect,
}
impl PageLayout {
const SCROLLBAR_WIDTH: i16 = 8;
const SCROLLBAR_SPACE: i16 = 5;
pub fn new(area: Rect, small_left_button: bool) -> Self {
let (area, button_both) = area.split_bottom(theme::BUTTON_HEIGHT);
let area = area.inset(Insets::bottom(theme::BUTTON_SPACING));
let (_space, content) = area.split_left(theme::CONTENT_BORDER);
let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER);
let (content, scrollbar) =
content.split_right(Self::SCROLLBAR_SPACE + Self::SCROLLBAR_WIDTH);
let (_space, scrollbar) = scrollbar.split_left(Self::SCROLLBAR_SPACE);
let width = if small_left_button {
theme::BUTTON_WIDTH
} else {
(button_both.width() - theme::BUTTON_SPACING) / 2
};
let (button_left, button_right) = button_both.split_left(width);
let button_right = button_right.inset(Insets::left(theme::BUTTON_SPACING));
Self {
content_single_page,
content,
scrollbar,
button_left,
button_right,
button_both,
}
}
}
#[cfg(test)]
mod tests {
use serde_json;
use crate::{
trace::tests::trace,
ui::{
component::text::paragraphs::{Paragraph, Paragraphs},
event::TouchEvent,
geometry::Point,
model_mercury::{constant, theme},
},
};
use super::*;
const SCREEN: Rect = constant::screen().inset(theme::borders());
fn swipe(component: &mut impl Component, points: &[(i16, i16)]) {
let last = points.len().saturating_sub(1);
let mut first = true;
let mut ctx = EventCtx::new();
for (i, &(x, y)) in points.iter().enumerate() {
let p = Point::new(x, y);
let ev = if first {
TouchEvent::TouchStart(p)
} else if i == last {
TouchEvent::TouchEnd(p)
} else {
TouchEvent::TouchMove(p)
};
component.event(&mut ctx, Event::Touch(ev));
ctx.clear();
first = false;
}
}
fn swipe_up(component: &mut impl Component) {
swipe(component, &[(20, 100), (20, 60), (20, 20)])
}
fn swipe_down(component: &mut impl Component) {
swipe(component, &[(20, 20), (20, 60), (20, 100)])
}
#[test]
fn paragraphs_empty() {
let mut page = ButtonPage::new(Paragraphs::<[Paragraph<'static>; 0]>::new([]), theme::BG);
page.place(SCREEN);
let expected = serde_json::json!({
"component": "ButtonPage",
"active_page": 0,
"page_count": 1,
"content": {
"component": "Paragraphs",
"paragraphs": [],
},
"hold": false,
});
assert_eq!(trace(&page), expected);
swipe_up(&mut page);
assert_eq!(trace(&page), expected);
swipe_down(&mut page);
assert_eq!(trace(&page), expected);
}
#[test]
fn paragraphs_single() {
let mut page = ButtonPage::new(
Paragraphs::new([
Paragraph::new(
&theme::TEXT_NORMAL,
"This is the first paragraph and it should fit on the screen entirely.",
),
Paragraph::new(
&theme::TEXT_BOLD,
"Second, bold, paragraph should also fit on the screen.",
),
]),
theme::BG,
);
page.place(SCREEN);
let expected = serde_json::json!({
"component": "ButtonPage",
"active_page": 0,
"page_count": 1,
"content": {
"component": "Paragraphs",
"paragraphs": [
["This is the first", "\n", "paragraph and it should", "\n", "fit on the screen", "\n", "entirely."],
["Second, bold,", "\n", "paragraph should also", "\n", "fit on the screen."],
],
},
"hold": false,
});
assert_eq!(trace(&page), expected);
swipe_up(&mut page);
assert_eq!(trace(&page), expected);
swipe_down(&mut page);
assert_eq!(trace(&page), expected);
}
#[test]
fn paragraphs_one_long() {
let mut page = ButtonPage::new(
Paragraphs::new(
Paragraph::new(
&theme::TEXT_BOLD,
"This is somewhat long paragraph that goes on and on and on and on and on and will definitely not fit on just a single screen. You have to swipe a bit to see all the text it contains I guess. There's just so much letters in it.",
)
),
theme::BG,
);
page.place(SCREEN);
let first_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 0,
"page_count": 2,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"This is somewhat long", "\n",
"paragraph that goes", "\n",
"on and on and on and", "\n",
"on and on and will", "\n",
"definitely not fit on", "\n",
"just a single screen.", "\n",
"You have to swipe a", "...",
],
],
},
"hold": false,
});
let second_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 1,
"page_count": 2,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"bit to see all the text it", "\n",
"contains I guess.", "\n",
"There's just so much", "\n",
"letters in it."
],
],
},
"hold": false,
});
assert_eq!(trace(&page), first_page);
swipe_down(&mut page);
assert_eq!(trace(&page), first_page);
swipe_up(&mut page);
assert_eq!(trace(&page), second_page);
swipe_up(&mut page);
assert_eq!(trace(&page), second_page);
swipe_down(&mut page);
assert_eq!(trace(&page), first_page);
}
#[test]
fn paragraphs_three_long() {
let mut page = ButtonPage::new(
Paragraphs::new([
Paragraph::new(
&theme::TEXT_BOLD,
"This paragraph is using a bold font. It doesn't need to be all that long.",
),
Paragraph::new(
&theme::TEXT_MONO,
"And this one is using MONO. Monospace is nice for numbers, they have the same width and can be scanned quickly. Even if they span several pages or something.",
),
Paragraph::new(
&theme::TEXT_BOLD,
"Let's add another one for a good measure. This one should overflow all the way to the third page with a bit of luck.",
),
]),
theme::BG,
);
page.place(SCREEN);
let first_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 0,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"This paragraph is", "\n",
"using a bold font. It", "\n",
"doesn't need to be all", "\n",
"that long.",
],
[
"And this one is u", "\n",
"sing MONO. Monosp", "\n",
"ace is nice f", "...",
],
],
},
"hold": false,
});
let second_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 1,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"...", "or numbers, t", "\n",
"hey have the same", "\n",
"width and can be", "\n",
"scanned quickly.", "\n",
"Even if they span", "\n",
"several pages or", "\n",
"something."
],
],
},
"hold": false,
});
let third_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 2,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"Let's add another one", "\n",
"for a good measure.", "\n",
"This one should", "\n",
"overflow all the way to", "\n",
"the third page with a", "\n",
"bit of luck.",
],
],
},
"hold": false,
});
assert_eq!(trace(&page), first_page);
swipe_down(&mut page);
assert_eq!(trace(&page), first_page);
swipe_up(&mut page);
assert_eq!(trace(&page), second_page);
swipe_up(&mut page);
assert_eq!(trace(&page), third_page);
swipe_up(&mut page);
assert_eq!(trace(&page), third_page);
swipe_down(&mut page);
assert_eq!(trace(&page), second_page);
swipe_down(&mut page);
assert_eq!(trace(&page), first_page);
swipe_down(&mut page);
assert_eq!(trace(&page), first_page);
}
#[test]
fn paragraphs_hard_break() {
let mut page = ButtonPage::new(
Paragraphs::new([
Paragraph::new(&theme::TEXT_NORMAL, "Short one.").break_after(),
Paragraph::new(&theme::TEXT_NORMAL, "Short two.").break_after(),
Paragraph::new(&theme::TEXT_NORMAL, "Short three.").break_after(),
]),
theme::BG,
);
page.place(SCREEN);
let first_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 0,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"Short one.",
],
],
},
"hold": false,
});
let second_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 1,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"Short two.",
],
],
},
"hold": false,
});
let third_page = serde_json::json!({
"component": "ButtonPage",
"active_page": 2,
"page_count": 3,
"content": {
"component": "Paragraphs",
"paragraphs": [
[
"Short three.",
],
],
},
"hold": false,
});
assert_eq!(trace(&page), first_page);
swipe_up(&mut page);
assert_eq!(trace(&page), second_page);
swipe_up(&mut page);
assert_eq!(trace(&page), third_page);
swipe_up(&mut page);
assert_eq!(trace(&page), third_page);
}
}

View File

@ -1,204 +0,0 @@
use crate::ui::{
component::{
base::ComponentExt, Component, Event, EventCtx, Pad, PageMsg, Paginate, Swipe,
SwipeDirection,
},
display::{self, Color},
geometry::{Axis, Insets, Rect},
shape::Renderer,
};
use super::{theme, ScrollBar};
use core::cell::Cell;
const SCROLLBAR_HEIGHT: i16 = 18;
const SCROLLBAR_BORDER: i16 = 4;
pub struct SimplePage<T> {
content: T,
pad: Pad,
swipe: Swipe,
scrollbar: ScrollBar,
axis: Axis,
swipe_right_to_go_back: bool,
fade: Cell<Option<u16>>,
}
impl<T> SimplePage<T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, axis: Axis, background: Color) -> Self {
Self {
content,
swipe: Swipe::new(),
pad: Pad::with_background(background),
scrollbar: ScrollBar::new(axis),
axis,
swipe_right_to_go_back: false,
fade: Cell::new(None),
}
}
pub fn horizontal(content: T, background: Color) -> Self {
Self::new(content, Axis::Horizontal, background)
}
pub fn vertical(content: T, background: Color) -> Self {
Self::new(content, Axis::Vertical, background)
}
pub fn with_swipe_right_to_go_back(mut self) -> Self {
self.swipe_right_to_go_back = true;
self
}
pub fn inner(&self) -> &T {
&self.content
}
fn setup_swipe(&mut self) {
if self.is_horizontal() {
self.swipe.allow_left = self.scrollbar.has_next_page();
self.swipe.allow_right =
self.scrollbar.has_previous_page() || self.swipe_right_to_go_back;
} else {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
self.swipe.allow_right = self.swipe_right_to_go_back;
}
}
fn change_page(&mut self, ctx: &mut EventCtx, step: isize) {
// Advance scrollbar.
self.scrollbar.go_to_relative(step);
// Adjust the swipe parameters according to the scrollbar.
self.setup_swipe();
// Change the page in the content, make sure it gets completely repainted and
// clear the background under it.
self.content.change_page(self.scrollbar.active_page);
self.content.request_complete_repaint(ctx);
self.pad.clear();
// Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint.
self.fade
.set(Some(theme::backlight::get_backlight_normal()));
}
fn is_horizontal(&self) -> bool {
matches!(self.axis, Axis::Horizontal)
}
}
impl<T> Component for SimplePage<T>
where
T: Paginate,
T: Component,
{
type Msg = PageMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.swipe.place(bounds);
let (content, scrollbar) = if self.is_horizontal() {
bounds.split_bottom(SCROLLBAR_HEIGHT + SCROLLBAR_BORDER)
} else {
bounds.split_right(SCROLLBAR_HEIGHT + SCROLLBAR_BORDER)
};
self.content.place(bounds);
if self.content.page_count() > 1 {
self.pad.place(content);
self.content.place(content);
} else {
self.pad.place(bounds);
}
if self.is_horizontal() {
self.scrollbar
.place(scrollbar.inset(Insets::bottom(SCROLLBAR_BORDER)));
} else {
self.scrollbar
.place(scrollbar.inset(Insets::right(SCROLLBAR_BORDER)));
}
self.scrollbar
.set_count_and_active_page(self.content.page_count(), 0);
self.setup_swipe();
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.scrollbar.page_count);
if let Some(swipe) = self.swipe.event(ctx, event) {
match (swipe, self.axis) {
(SwipeDirection::Left, Axis::Horizontal) | (SwipeDirection::Up, Axis::Vertical) => {
self.change_page(ctx, 1);
return None;
}
(SwipeDirection::Right, _)
if self.swipe_right_to_go_back && self.scrollbar.active_page == 0 =>
{
return Some(PageMsg::Cancelled);
}
(SwipeDirection::Right, Axis::Horizontal)
| (SwipeDirection::Down, Axis::Vertical) => {
self.change_page(ctx, -1);
return None;
}
_ => {
// Ignore other directions.
}
}
}
self.content.event(ctx, event).map(PageMsg::Content)
}
fn paint(&mut self) {
self.pad.paint();
self.content.paint();
if self.scrollbar.has_pages() {
self.scrollbar.paint();
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target);
self.content.render(target);
if self.scrollbar.has_pages() {
self.scrollbar.render(target);
}
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
self.scrollbar.bounds(sink);
self.content.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SimplePage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("SimplePage");
t.int("active_page", self.scrollbar.active_page as i64);
t.int("page_count", self.scrollbar.page_count as i64);
t.child("content", &self.content);
}
}

View File

@ -236,7 +236,7 @@ fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Err
}
}
pub fn new_confirm_action_simple<T: Component + Paginate + Clone + MaybeTrace + 'static>(
pub fn new_confirm_action_simple<T: Component + Paginate + MaybeTrace + 'static>(
content: T,
title: TString<'static>,
subtitle: Option<TString<'static>>,

View File

@ -14,9 +14,7 @@ use crate::{
component::{
base::{AttachType, ComponentExt},
connect::Connect,
image::BlendedImage,
jpeg::Jpeg,
paginated::{PageMsg, Paginate},
swipe_detect::SwipeSettings,
text::{
op::OpTextLayout,
@ -26,7 +24,7 @@ use crate::{
},
TextStyle,
},
Border, Component, Empty, FormattedText, Label, Never, SwipeDirection, Timeout,
Border, Component, FormattedText, Label, Never, SwipeDirection, Timeout,
},
flow::Swipable,
geometry,
@ -41,13 +39,12 @@ use crate::{
use super::{
component::{
AddressDetails, Bip39Input, Button, ButtonMsg, ButtonPage, ButtonStyleSheet,
CancelConfirmMsg, CancelInfoConfirmMsg, CoinJoinProgress, Dialog, DialogMsg, FidoConfirm,
FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput,
MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg,
PinKeyboard, PinKeyboardMsg, Progress, PromptScreen, SelectWordCount, SelectWordCountMsg,
SetBrightnessDialog, SimplePage, Slip39Input, StatusScreen, SwipeUpScreen,
SwipeUpScreenMsg, VerticalMenu, VerticalMenuChoiceMsg,
AddressDetails, Bip39Input, Button, CancelConfirmMsg, CancelInfoConfirmMsg,
CoinJoinProgress, FidoConfirm, FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg,
Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard,
PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, PromptScreen,
SelectWordCount, SelectWordCountMsg, SetBrightnessDialog, Slip39Input, StatusScreen,
SwipeUpScreen, SwipeUpScreenMsg, VerticalMenu, VerticalMenuChoiceMsg,
},
flow, theme,
};
@ -108,33 +105,6 @@ where
}
}
impl<T, U> ComponentMsgObj for Dialog<T, U>
where
T: ComponentMsgObj,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Content(c) => Ok(self.inner().msg_try_into_obj(c)?),
DialogMsg::Controls(msg) => msg.try_into(),
}
}
}
impl<U> ComponentMsgObj for IconDialog<U>
where
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Controls(msg) => msg.try_into(),
_ => unreachable!(),
}
}
}
impl ComponentMsgObj for PinKeyboard<'_> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -220,21 +190,6 @@ impl<T: Component + Swipable> ComponentMsgObj for SwipeUpScreen<T> {
}
}
impl<T> ComponentMsgObj for ButtonPage<T>
where
T: Component + Paginate,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Confirmed => Ok(CONFIRMED.as_obj()),
PageMsg::Cancelled => Ok(CANCELLED.as_obj()),
PageMsg::SwipeLeft => Ok(INFO.as_obj()),
PageMsg::SwipeRight => Ok(CANCELLED.as_obj()),
}
}
}
// Clippy/compiler complains about conflicting implementations
// TODO move the common impls to a common module
#[cfg(not(feature = "clippy"))]
@ -289,19 +244,6 @@ where
}
}
impl<T> ComponentMsgObj for SimplePage<T>
where
T: ComponentMsgObj + Paginate,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PageMsg::Content(inner_msg) => Ok(self.inner().msg_try_into_obj(inner_msg)?),
PageMsg::Cancelled => Ok(CANCELLED.as_obj()),
_ => Err(Error::TypeError),
}
}
}
impl ComponentMsgObj for AddressDetails {
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
Ok(CANCELLED.as_obj())
@ -350,12 +292,13 @@ extern "C" fn new_confirm_emphasized(n_args: usize, args: *const Obj, kwargs: *m
}
}
let obj = LayoutObj::new(Frame::left_aligned(
flow::new_confirm_action_simple(
FormattedText::new(ops).vertically_centered(),
title,
ButtonPage::new(FormattedText::new(ops).vertically_centered(), theme::BG)
.with_cancel_confirm(None, verb),
))?;
Ok(obj.into())
None,
verb,
None,
)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -423,42 +366,6 @@ impl ConfirmBlobParams {
self
}
fn into_layout(self) -> Result<Obj, Error> {
let paragraphs = ConfirmBlob {
description: self.description.unwrap_or("".into()),
extra: self.extra.unwrap_or("".into()),
data: self.data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_DEMIBOLD,
data_font: if self.chunkify {
let data: TString = self.data.try_into()?;
theme::get_chunkified_text_style(data.len())
} else if self.text_mono {
&theme::TEXT_MONO
} else {
&theme::TEXT_NORMAL
},
}
.into_paragraphs();
let mut page = ButtonPage::new(paragraphs, theme::BG);
if let Some(verb) = self.verb {
page = page.with_cancel_confirm(self.verb_cancel, Some(verb))
}
if self.hold {
page = page.with_hold()?
}
let mut frame = Frame::left_aligned(self.title, page);
if let Some(subtitle) = self.subtitle {
frame = frame.with_subtitle(subtitle);
}
if self.info_button {
frame = frame.with_menu_button();
}
let obj = LayoutObj::new(frame)?;
Ok(obj.into())
}
fn into_flow(self) -> Result<Obj, Error> {
let paragraphs = ConfirmBlob {
description: self.description.unwrap_or("".into()),
@ -521,7 +428,6 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: Option<TString> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let verb: TString = kwargs.get_or(Qstr::MP_QSTR_verb, TR::buttons__confirm.into())?;
let extra: Option<TString> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
@ -543,16 +449,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
}
.into_paragraphs();
let obj = LayoutObj::new(
Frame::left_aligned(
title,
ButtonPage::new(paragraphs, theme::BG)
.with_swipe_left()
.with_cancel_confirm(None, Some(verb)),
)
.with_menu_button(),
)?;
Ok(obj.into())
flow::new_confirm_action_simple(paragraphs, title, None, None, None)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -560,7 +457,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let _hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; // FIXME
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let paragraphs = PropsList::new(
@ -569,14 +466,14 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
&theme::TEXT_MONO,
&theme::TEXT_MONO,
)?;
let page = if hold {
ButtonPage::new(paragraphs.into_paragraphs(), theme::BG).with_hold()?
} else {
ButtonPage::new(paragraphs.into_paragraphs(), theme::BG)
.with_cancel_confirm(None, Some(TR::buttons__confirm.into()))
};
let obj = LayoutObj::new(Frame::left_aligned(title, page))?;
Ok(obj.into())
flow::new_confirm_action_simple(
paragraphs.into_paragraphs(),
title,
None,
Some(TR::buttons__confirm.into()),
None,
)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -588,34 +485,41 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m
let jpeg: BinaryData = image.try_into()?;
if jpeg.is_empty() {
let obj = if jpeg.is_empty() {
// Incoming data may be empty, meaning we should
// display default homescreen message.
let buttons = Button::cancel_confirm_text(None, Some(TR::buttons__change.into()));
let obj = LayoutObj::new(Frame::centered(
LayoutObj::new(SwipeUpScreen::new(
Frame::centered(
title,
Dialog::new(
Paragraphs::new([Paragraph::new(
SwipeContent::new(Paragraphs::new([Paragraph::new(
&theme::TEXT_DEMIBOLD,
TR::homescreen__set_default,
)
.centered()]),
buttons,
),
))?;
Ok(obj.into())
.centered()])),
)
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::buttons__change.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))
} else {
if !check_homescreen_format(jpeg) {
return Err(value_error!("Invalid image."));
};
let buttons = Button::cancel_confirm_text(None, Some(TR::buttons__change.into()));
let obj = LayoutObj::new(Frame::centered(
title,
Dialog::new(Jpeg::new(jpeg, 1), buttons),
))?;
Ok(obj.into())
}
LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, Jpeg::new(jpeg, 1))
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::buttons__change.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))
};
Ok(obj?.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -625,7 +529,7 @@ extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs:
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let horizontal: bool = kwargs.get_or(Qstr::MP_QSTR_horizontal, false)?;
let _horizontal: bool = kwargs.get_or(Qstr::MP_QSTR_horizontal, false)?; // FIXME
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let mut paragraphs = ParagraphVecShort::new();
@ -645,19 +549,10 @@ extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs:
}
}
let axis = match horizontal {
true => geometry::Axis::Horizontal,
_ => geometry::Axis::Vertical,
};
let obj = LayoutObj::new(
Frame::left_aligned(
title,
SimplePage::new(paragraphs.into_paragraphs(), axis, theme::BG)
.with_swipe_right_to_go_back(),
)
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(paragraphs.into_paragraphs()))
.with_cancel_button(),
)?;
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -698,8 +593,6 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false)?;
let cancel_arrow: bool = kwargs.get_or(Qstr::MP_QSTR_cancel_arrow, false)?;
let mut paragraphs = ParagraphVecShort::new();
@ -708,18 +601,14 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, label).no_break());
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value));
}
let mut page = ButtonPage::new(paragraphs.into_paragraphs(), theme::BG).with_hold()?;
if cancel_arrow {
page = page.with_cancel_arrow()
}
if info_button {
page = page.with_swipe_left();
}
let mut frame = Frame::left_aligned(title, page);
if info_button {
frame = frame.with_menu_button();
}
let obj = LayoutObj::new(frame)?;
// FIXME: hold
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(paragraphs.into_paragraphs()))
.with_menu_button()
.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) }
@ -798,82 +687,13 @@ extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *m
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
fn new_show_modal(
kwargs: &Map,
icon: BlendedImage,
button_style: ButtonStyleSheet,
) -> Result<Obj, Error> {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let value: TString = kwargs.get_or(Qstr::MP_QSTR_value, "".into())?;
let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?;
let button: TString = kwargs.get_or(Qstr::MP_QSTR_button, TR::buttons__continue.into())?;
let allow_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_allow_cancel, true)?;
let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?;
let no_buttons = button.is_empty();
let obj = if no_buttons && time_ms == 0 {
// No buttons and no timer, used when we only want to draw the dialog once and
// then throw away the layout object.
LayoutObj::new(
IconDialog::new(icon, title, Empty)
.with_value(value)
.with_description(description),
)?
.into()
} else if no_buttons && time_ms > 0 {
// Timeout, no buttons.
LayoutObj::new(
IconDialog::new(
icon,
title,
Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)),
)
.with_value(value)
.with_description(description),
)?
.into()
} else if allow_cancel {
// Two buttons.
LayoutObj::new(
IconDialog::new(
icon,
title,
Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text(button).styled(button_style),
false,
),
)
.with_value(value)
.with_description(description),
)?
.into()
} else {
// Single button.
LayoutObj::new(
IconDialog::new(
icon,
title,
theme::button_bar(Button::with_text(button).styled(button_style).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
})),
)
.with_value(value)
.with_description(description),
)?
.into()
};
Ok(obj)
}
extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
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 allow_cancel: bool = kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into()?;
let content = 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, SwipeContent::new(content))
.with_cancel_button()
@ -965,7 +785,7 @@ 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 = Paragraphs::new([Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)]);
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)
@ -983,33 +803,17 @@ extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Ma
let url: TString = TR::addr_mismatch__support_url.into();
let button: TString = TR::buttons__quit.into();
let icon = BlendedImage::new(
theme::IMAGE_BG_OCTAGON,
theme::IMAGE_FG_WARN,
theme::WARN_COLOR,
theme::FG,
theme::BG,
);
let obj = LayoutObj::new(
IconDialog::new(
icon,
title,
Button::cancel_confirm(
Button::with_icon(theme::ICON_BACK),
Button::with_text(button).styled(theme::button_default()),
true,
),
)
.with_paragraph(
Paragraph::new(&theme::TEXT_NORMAL, description)
.centered()
.with_bottom_padding(
theme::TEXT_NORMAL.text_font.text_height()
- theme::TEXT_DEMIBOLD.text_font.text_height(),
),
)
.with_text(&theme::TEXT_DEMIBOLD, url),
)?;
let paragraphs = Paragraphs::new([
Paragraph::new(&theme::TEXT_NORMAL, description).centered(),
Paragraph::new(&theme::TEXT_DEMIBOLD, url).centered(),
]);
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(paragraphs))
.with_cancel_button()
.with_footer(TR::instructions__swipe_up.into(), Some(button))
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -1018,24 +822,14 @@ extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Ma
extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: Option<TString> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?;
let obj = if let Some(t) = title {
LayoutObj::new(Frame::left_aligned(
t,
Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, description)),
))?
.into()
} else {
LayoutObj::new(Border::new(
let obj = LayoutObj::new(Border::new(
theme::borders(),
Paragraphs::new(Paragraph::new(&theme::TEXT_DEMIBOLD, description)),
))?
.into()
};
))?;
Ok(obj)
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -1044,7 +838,6 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let button: TString = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let info_button: TString = kwargs.get(Qstr::MP_QSTR_info_button)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecShort::new();
@ -1060,7 +853,7 @@ 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())
Frame::left_aligned(title, SwipeContent::new(paragraphs.into_paragraphs()))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), Some(button))
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
@ -1073,7 +866,6 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let button: TString = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecLong::new();
@ -1085,12 +877,11 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map
paragraphs.add(Paragraph::new(style, text));
}
let obj = LayoutObj::new(Frame::left_aligned(
title,
ButtonPage::new(paragraphs.into_paragraphs(), theme::BG)
.with_cancel_confirm(None, Some(button))
.with_confirm_style(theme::button_default())
.with_back_button(),
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(paragraphs.into_paragraphs()))
.with_cancel_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -1109,11 +900,8 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut
Paragraph::new(&theme::TEXT_MONO, max_feerate),
]);
let obj = LayoutObj::new(Frame::left_aligned(
TR::coinjoin__title.into(),
ButtonPage::new(paragraphs, theme::BG).with_hold()?,
))?;
Ok(obj.into())
// FIXME: hold
flow::new_confirm_action_simple(paragraphs, TR::coinjoin__title.into(), None, None, None)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -1253,7 +1041,7 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut
let recovery_type: u32 = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
let _info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false)?;
let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]);
let paragraphs = Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, description));
let notification = match recovery_type {
RECOVERY_TYPE_DRY_RUN => TR::recovery__title_dry_run.into(),
@ -1292,12 +1080,21 @@ extern "C" fn new_show_group_share_success(
let lines_iterable: Obj = kwargs.get(Qstr::MP_QSTR_lines)?;
let lines: [TString; 4] = util::iter_into_array(lines_iterable)?;
let obj = LayoutObj::new(IconDialog::new_shares(
lines,
theme::button_bar(Button::with_text(TR::buttons__continue.into()).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
})),
let paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_NORMAL_GREY_EXTRA_LIGHT, lines[0]).centered(),
Paragraph::new(&theme::TEXT_DEMIBOLD, lines[1]).centered(),
Paragraph::new(&theme::TEXT_NORMAL_GREY_EXTRA_LIGHT, lines[2]).centered(),
Paragraph::new(&theme::TEXT_DEMIBOLD, lines[3]).centered(),
])
.into_paragraphs()
.with_placement(geometry::LinearPlacement::vertical().align_at_center());
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned("".into(), SwipeContent::new(paragraphs))
.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) }
@ -1315,12 +1112,13 @@ extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs:
.add(Paragraph::new(&theme::TEXT_NORMAL, description).break_after());
}
let obj = LayoutObj::new(Frame::left_aligned(
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(
TR::recovery__title_remaining_shares.into(),
ButtonPage::new(paragraphs.into_paragraphs(), theme::BG)
.with_cancel_confirm(None, Some(TR::buttons__continue.into()))
.with_confirm_style(theme::button_default())
.without_cancel(),
SwipeContent::new(paragraphs.into_paragraphs()),
)
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
@ -2046,59 +1844,3 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// mock:global
Qstr::MP_QSTR_BacklightLevels => BACKLIGHT_LEVELS_OBJ.as_obj(),
};
#[cfg(test)]
mod tests {
use serde_json;
use crate::{
trace::tests::trace,
ui::{component::text::op::OpTextLayout, geometry::Rect, model_mercury::constant},
};
use super::*;
const SCREEN: Rect = constant::screen().inset(theme::borders());
#[test]
fn trace_example_layout() {
let buttons = Button::cancel_confirm(
Button::with_text("Left".into()),
Button::with_text("Right".into()),
false,
);
let ops = OpTextLayout::new(theme::TEXT_NORMAL)
.text_normal("Testing text layout, with some text, and some more text. And ")
.text_bold("parameters!");
let formatted = FormattedText::new(ops);
let mut layout = Dialog::new(formatted, buttons);
layout.place(SCREEN);
let expected = serde_json::json!({
"component": "Dialog",
"content": {
"component": "FormattedText",
"text": ["Testing text layout, with", "\n", "some text, and some", "\n",
"more text. And ", "parame", "-", "\n", "ters!"],
"fits": true,
},
"controls": {
"component": "FixedHeightBar",
"inner": {
"component": "Split",
"first": {
"component": "Button",
"text": "Left",
},
"second": {
"component": "Button",
"text": "Right",
},
},
},
});
assert_eq!(trace(&layout), expected);
}
}

View File

@ -99,7 +99,6 @@ include_icon!(
ICON_LOCKSCREEN_FILTER,
"model_mercury/res/lockscreen_filter.toif"
);
include_icon!(ICON_CENTRAL_CIRCLE, "model_mercury/res/central_circle.toif");
// Scrollbar/PIN dots - taken from model T
include_icon!(DOT_ACTIVE, "model_mercury/res/scroll-active.toif");
@ -127,14 +126,6 @@ include_icon!(
// Icon for "next keyboard layout" for special characters
include_icon!(ICON_ASTERISK, "model_mercury/res/asterisk16.toif");
// Large, three-color icons.
pub const WARN_COLOR: Color = ORANGE_LIGHT;
pub const INFO_COLOR: Color = GREY_LIGHT;
pub const SUCCESS_COLOR: Color = GREEN;
pub const ERROR_COLOR: Color = ORANGE_DIMMED;
include_icon!(IMAGE_FG_SUCCESS, "model_mercury/res/fg-check48.toif");
include_icon!(IMAGE_BG_CIRCLE, "model_mercury/res/circle48.toif");
// Welcome screen.
include_icon!(ICON_LOGO, "model_mercury/res/lock_full.toif");
@ -160,12 +151,6 @@ include_icon!(ICON_LOCK_BIG, "model_tt/res/lock24.toif");
include_icon!(ICON_COINJOIN, "model_tt/res/coinjoin16.toif");
include_icon!(ICON_MAGIC, "model_tt/res/magic.toif");
include_icon!(IMAGE_FG_WARN, "model_tt/res/fg-warning48.toif");
include_icon!(IMAGE_FG_ERROR, "model_tt/res/fg-error48.toif");
include_icon!(IMAGE_FG_INFO, "model_tt/res/fg-info48.toif");
include_icon!(IMAGE_FG_USER, "model_tt/res/fg-user48.toif");
include_icon!(IMAGE_BG_OCTAGON, "model_tt/res/octagon48.toif");
// Non-square button backgrounds.
include_icon!(IMAGE_BG_BACK_BTN, "model_tt/res/bg-back40.toif");
include_icon!(IMAGE_BG_BACK_BTN_TALL, "model_tt/res/bg-back52.toif");

View File

@ -316,6 +316,7 @@ def test_signmessage(
@pytest.mark.skip_t1b1
@pytest.mark.skip_t2b1
@pytest.mark.skip_t3t1(reason="Not yet implemented in new UI")
@pytest.mark.parametrize(
"coin_name, path, script_type, no_script_type, address, message, signature", VECTORS
)

View File

@ -430,6 +430,7 @@ HEXDATA = "0123456789abcd000023456789abcd010003456789abcd020000456789abcd0300000
@pytest.mark.parametrize(
"flow", (input_flow_data_skip, input_flow_data_scroll_down, input_flow_data_go_back)
)
@pytest.mark.skip_t3t1(reason="Not yet implemented in new UI")
@pytest.mark.skip_t1b1
def test_signtx_data_pagination(client: Client, flow):
def _sign_tx_call():

View File

@ -18514,23 +18514,23 @@
"T3T1_en_binance-test_get_address.py::test_binance_get_address_chunkify_details[m-44h-714h-0h-0-0-bn-59d4996f": "16bec6ab85f7ccec2eed3f2d9f273ab561fa4edc2779a8333e96e325e39a87ca",
"T3T1_en_binance-test_get_address.py::test_binance_get_address_chunkify_details[m-44h-714h-0h-0-1-bn-c9025900": "ef31054cf45938b5cba8bf2b7d8f5b691f74a436ee7fa417cf678d7517856255",
"T3T1_en_binance-test_get_public_key.py::test_binance_get_public_key": "69fcdf948ddc30dcc97f6901b82fe80376c7b0c0428099010490521ca37d788a",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[False-message0-expected_response0]": "01f8fd01fc6dacca738224f7eec1ff8dc79acc7f5c5e172aef6ccd460592c6b2",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[False-message1-expected_response1]": "7adbbc301984f9ea5c78dc9e6dfc1af774869e4d87d81363ad14346b94c12b03",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[False-message0-expected_response0]": "89b890924880b128afeaad4f93de40be28cbbcfeeab46e491b2936d81274752d",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[False-message1-expected_response1]": "0d928a4dc6873a2a79441bb1964491f4f22f683e14ff47bbc68d4204db85754c",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[False-message2-expected_response2]": "0b46d6e740b98bfa899618291cad1bb90efa9384b6755b361b45673cd95f3639",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[True-message0-expected_response0]": "01f8fd01fc6dacca738224f7eec1ff8dc79acc7f5c5e172aef6ccd460592c6b2",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[True-message1-expected_response1]": "7adbbc301984f9ea5c78dc9e6dfc1af774869e4d87d81363ad14346b94c12b03",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[True-message0-expected_response0]": "89b890924880b128afeaad4f93de40be28cbbcfeeab46e491b2936d81274752d",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[True-message1-expected_response1]": "0d928a4dc6873a2a79441bb1964491f4f22f683e14ff47bbc68d4204db85754c",
"T3T1_en_binance-test_sign_tx.py::test_binance_sign_message[True-message2-expected_response2]": "0b46d6e740b98bfa899618291cad1bb90efa9384b6755b361b45673cd95f3639",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "5fb0f48e4136f400eea4c1958c9a28b0cd205f0a78a6ed2142e1567703b7e242",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "e08e582ace0266765662772d8676497b2f540166fa784420016c14c35f27789f",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_get_address": "67ef3bfb498805795a5c111437e9e31e9996dc2b2a3d7ed410b820a76879737a",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "1a34efcfdc19f56e11bd7a5a888644efd3e17b1d122c0fbc1e8cb7933da07870",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "15cdff9ad1bc9e2d1fd11bed1c1ecdea556d2abc277f33b70034a99821cee76b",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx[False]": "0ca4bcbaa1a549cb84429dbc77d607ab2cba4c6ce3174699a675251891e8b7f9",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx[True]": "0ca4bcbaa1a549cb84429dbc77d607ab2cba4c6ce3174699a675251891e8b7f9",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx_large": "7500e2783981dbb877e46acf18016f79675cfc89fbc32bccfcff9b2e32f89599",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "70b9d19990ec21925a41d0a20778698dc7a3648e4de15f0f9a8d5252c4fb8712",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx[False]": "5e68efc63caa84a88f9490f00adaca1c7a97d92ae35911778b3818ace34b6be4",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx[True]": "5e68efc63caa84a88f9490f00adaca1c7a97d92ae35911778b3818ace34b6be4",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx_large": "507bee28bfee7cc86a6f12f812f8f52bed2c78d64390514a4d3392a8c2707ad3",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx_migration": "6c35025a5a714e4b7bb147186b755672b66c29f72bafaabab02da3d901d60e21",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "8b7dcef01953de33d6a52a40951134b9a2b8daec4066df5ffa2e8b238f4e07a4",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_wrong_account_type": "8a9aa98a0099a826ee682dc6983059d38c3abbe9dfc2caf8937e3b6e99090e6e",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "8a9aa98a0099a826ee682dc6983059d38c3abbe9dfc2caf8937e3b6e99090e6e",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_wrong_account_type": "2a30f5df79cd7071056cf8111c858f58881225c19513fb1cdb85d92e687eefd1",
"T3T1_en_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "2a30f5df79cd7071056cf8111c858f58881225c19513fb1cdb85d92e687eefd1",
"T3T1_en_bitcoin-test_bcash.py::test_attack_change_input": "3fe9761bcc0b60cc685d9c0f7e557a3cb2ec18046de63b499670cca7040bdd40",
"T3T1_en_bitcoin-test_bcash.py::test_send_bch_change": "3fe9761bcc0b60cc685d9c0f7e557a3cb2ec18046de63b499670cca7040bdd40",
"T3T1_en_bitcoin-test_bcash.py::test_send_bch_external_presigned": "2863996beef75967773099e62aaea6d7d5494856dd98e936ef664163b56b5601",
@ -18748,11 +18748,11 @@
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-3h-100h-4-255-script_types1]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-4-255-script_types0]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-49-0-63-0-255-script_types4]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-6-255-script_types3]": "728660776f62f628e3bc48034dc19bc92560f7e1eb3b228e3e086e512240bd11",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-script_types2]": "06c3732134ec930d8f1b50c5f4c21ac15106ae5adda319835c6d89d34e7aa09c",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-3h-100h-4-255-script_types1]": "f18f3f737c6610f5a6c1da1f96cad6050aed20b4c9db5745160675eefd08b924",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-4-255-script_types0]": "82206cbfa43366c0562ab93109da5e5ef680b230e3ac3befbe4f2e5623c23118",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-49-0-63-0-255-script_types4]": "bb9daa1fda9dd7b5329d15c43dd587dca5678433f551d96d38e919cc307e613c",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-6-255-script_types3]": "02fbd513872b7b00f65e6373d6c6d5b98b6bcf828a29678aa0031b6802bcf145",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-script_types2]": "a360e6195514444bf22144a7b8790eacaab32535c6c39dbb04345cf617ff5fa6",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-3h-100h-4-255-script_types1]": "b69dbb3b01109c9ed8c7a47a4a595287d04c2c80751dec5dc186eb4708ae8b73",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-4-255-script_types0]": "dcb6bd72011dbcdccf5e833c9a15d228af212430838894c5bd358e385064411a",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signmessage[m-49-0-63-0-255-script_types4]": "5956c3900f013afb69b450ccb1b8c95398b0f29862f0d37f35d240fc3da8f65c",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signtx[m-1195487518-6-255-script_types3]": "884923a572f62240cb5472321cff4b7bc66b168d7afe022beb64d2474631a5d7",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signtx[m-1195487518-script_types2]": "095604e9b8497113a4845d928ca72798f3adbf3d7a89b68b6de846dfba8298d0",
"T3T1_en_bitcoin-test_nonstandard_paths.py::test_signtx[m-3h-100h-4-255-script_types1]": "4127933c0fd4d1708aa666dc912b17fb92576f83519e17f11fdefc7f85b8516b",
@ -18772,52 +18772,33 @@
"T3T1_en_bitcoin-test_peercoin.py::test_timestamp_included": "2df967ba9a788011f8dc1f73dc8dd818027a10c245ec00d24958150ff9a55484",
"T3T1_en_bitcoin-test_peercoin.py::test_timestamp_missing": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_bitcoin-test_peercoin.py::test_timestamp_missing_prevtx": "53f7cffed7f0140f04d33a4d7b0981a4e82eff6e9c6d671203edfb37fed54574",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[NFC message]": "e9f3d5df4d1bd04be1a6f6ddbd761d31699316b1a19c3bfc92ad52a394a90287",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[NFKD message]": "e9f3d5df4d1bd04be1a6f6ddbd761d31699316b1a19c3bfc92ad52a394a90287",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[bcash]": "f4adb0934b1f3df13e2c649936319bda77cc5e9bd91fa167bee225c7af6265cf",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-p2pkh]": "05b3d84d111da58af6482f53d764e6931842c6f93b6e98ec9c0b0e30cc6df616",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-native]": "6720b09e96f003adafb45042c6f79d456891de8a0d238963870fef7aac61cbb9",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-p2sh]": "b23e559e7612a09ef187d721b217acbbdd31593c8d32add8a05d25815a6a4d3d",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh long message]": "67f67220227c6945b3d202693ecdfad497e03f4c14428db9a2881e4188aaa3ac",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh0]": "b6c475a7e2cf0149f78a803b15bd0bbc3a40704da9fd89f06802668b2719123e",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh1]": "b6c475a7e2cf0149f78a803b15bd0bbc3a40704da9fd89f06802668b2719123e",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh2]": "a7b84107f82a334ce4233c5d848314a42fcb8517d86ddc5dbbcd9083af66ed65",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native long message]": "6c65e6a24dd82d066574ffe9f6ca739d1afb8a34e1dd0735cd7e82248c115189",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native0]": "799646e7845719d4aad141f7875eb4e924c08743498d69adf91284359d5f8871",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native1]": "799646e7845719d4aad141f7875eb4e924c08743498d69adf91284359d5f8871",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native2]": "374321a97826ef612d94d191ac4d3530842b2026780fd53cba251a7cca64e34d",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh long message]": "3d06e0814afd67a805d00d7b75a438cc5517a61a794c883b3b982c664a2b0299",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh0]": "57313215dcf9c3dac621ade2419b5e101a160a6165551f9aef75b3ecfb382aae",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh1]": "57313215dcf9c3dac621ade2419b5e101a160a6165551f9aef75b3ecfb382aae",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh2]": "3ab152ef1bd8cb7ffd91c26b8b8e22d371d6d04057aa832300af6bc6c762e192",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[t1 firmware path]": "28ff073d1c6b6e1ade0c569e86987578bbc8bb0f2a516d57c011964c2b6d8acc",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[NFC message]": "bab75ef4bcfc8757a610befce5077796c5f07a4521c4da2caf3a3e688355fcd5",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[NFKD message]": "bab75ef4bcfc8757a610befce5077796c5f07a4521c4da2caf3a3e688355fcd5",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[bcash]": "005fce2151d5d00041216b8df4c7a1b7c44b3d1fea44e2c53d2ac93eded687ec",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[grs-p2pkh]": "63b80d24787d21a3734ef875f9cbc506830b57511f6a41832988939a063a39f9",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[grs-segwit-native]": "25715b14066b081975e3e82e47c0d73dbec6885d1672bbc7ad1b3bf68941d7e4",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[grs-segwit-p2sh]": "d8f8bc5393264b0d717f9a760cb8ba8cbdda82aea0d077b30ce1070094a99466",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[p2pkh long message]": "227985471ce8341a82962a2a634c3786a5f7feb18addc95f92393f4c60c1beba",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[p2pkh0]": "ab00467d9e4fdc9c3e63afc53926072a50ce89e9c140c26f636b65a6e205ce72",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[p2pkh1]": "ab00467d9e4fdc9c3e63afc53926072a50ce89e9c140c26f636b65a6e205ce72",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[p2pkh2]": "e5cc85d2e4803c96aeefae36b4638f02e2c0f674d0a038fb8fe75832b5eb8d43",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-native long message]": "33bf48ac4364c7557daa97a5c13e1208604f85bbd45077d3b9364fde921b1f47",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-native0]": "283acfdd4526d7775c15c6de32b69f5de0b32611ea7db8cf910a32c25c931b57",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-native1]": "283acfdd4526d7775c15c6de32b69f5de0b32611ea7db8cf910a32c25c931b57",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-native2]": "cfff484653d935cb3e294a5ea168ae1174194c4cce408100c96156d8c26445df",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-p2sh long message]": "757ebd325ab076d705f7ac805f03f8cdc964fd1b3b6d3e6f30d30a4007e77900",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-p2sh0]": "8c0eaf50ecc37645397cb4bf74418e12cbd6a1fac9cd187cdfe32077bc4a375a",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-p2sh1]": "8c0eaf50ecc37645397cb4bf74418e12cbd6a1fac9cd187cdfe32077bc4a375a",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[segwit-p2sh2]": "815ff0f02676e74d04e527de234a7739d58d04d455ac9978614d698022ad3979",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_info[t1 firmware path]": "28fa7d38bc1252c346b57361aad936f399fbf73555d25b64987b019f55129523",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[long_words]": "341394d80b5d0d6b9d65f3bebf184ef81ada6fc6af3d2bfe50445dc4e421cc52",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[newlines]": "29fa0814bcca57c8df1d3475cf7df720a739ab54fd41343ac61ed645d3216513",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[no_spaces]": "bb613b0198d0cd8a1823db142019b79b9b6a0d6a6d2f1ca7023c4fa4b64d5684",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[normal_text]": "3ab717646032afcce058e08d9fbde406c3101df7e309334a648761f925d3560f",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[single_line_over]": "6a56d56bbbfab1f910b49fda5059ed4ec84815687a7dc9b0de1b2586cb1211ce",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_nospace]": "35c95744c026a93759f795953851beff19cbab1883423d2535db610fe91de8d4",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_text]": "cba6c516d1699aa0134a0a05e514681295870cfd56a5f806d1cebff0a232d643",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_path_warning": "969b3c76206be865afeb09fd7b0d25d08f0a026061b1717988b9c2b65f4c517c",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[NFC message]": "16e4ca277fa5920f39772c5a9e183b78507ba9c43c5bf18eb01ca948305f1ecd",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[NFKD message]": "16e4ca277fa5920f39772c5a9e183b78507ba9c43c5bf18eb01ca948305f1ecd",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[bcash]": "4d83b12408caf5d986b00e4353096e33dab7c40e301b191b475aab7f1b0815f9",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-p2pkh]": "8c9b98de0aa76a35a383e098613cde939fb6f2af4b789ce664377838d2d0c8ac",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-native]": "62f2b3c454c9f2e3b292a1ad814cfff6e7afc6900232a950d36b35f6ddf0a485",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-p2sh]": "9dfba44e949bbcf3cd4960e1bdbe0bbdc17393d6273417af878faf9e38e5904d",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh long message]": "37a07035fd875bd3988323c6fd2ccc324f0f574d6fa39ed5842105487ba74bab",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh0]": "027f3af245cab497f274e0b4856722b30a90691cab36e12f9ce32adf45f840eb",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh1]": "027f3af245cab497f274e0b4856722b30a90691cab36e12f9ce32adf45f840eb",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[p2pkh2]": "77f8a0e4bcc8ebd74b689ecf6920963dfa5489c57d8fe354215def74293757bf",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native long message]": "6abee9ee8b76c0a7a750bc6a0e5f78f86ba298312ab554b809f285984eb0114b",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native0]": "3e2fc00ceaa1a0e5719164a069b9172c3e8829db1cfa2852400661a141aa3e08",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native1]": "3e2fc00ceaa1a0e5719164a069b9172c3e8829db1cfa2852400661a141aa3e08",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-native2]": "84b55eee88be0a06e6445f7fb3a0d922e63bffef1e1544857415916b40e0fbd1",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh long message]": "25b6ac2fb4e1bf9c9603ba2892bc499c28f2a0e0962f6c4b3340517dd2638238",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh0]": "128354a0f8d2c2f4e45001d0b0dbe5c88f1af680fe5e894cdecf8dbee964536c",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh1]": "128354a0f8d2c2f4e45001d0b0dbe5c88f1af680fe5e894cdecf8dbee964536c",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh2]": "76c8b7803e444a17a5c22916126706ede2fd494876e8701e88cbb8ae655ddaea",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage[t1 firmware path]": "3d9031a4bcb61c4d0fdf9f02371bed19ad57942cf1bce3f8ef399d456c0e52a3",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[long_words]": "139a959ffc1a87e6c9709b5a80fa731851054e8b4cac0a626544aefafff46eb2",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[newlines]": "86242ecc0b5c04d410448c90843dd3be267ee410b57526c73b3680df3e82994d",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[no_spaces]": "bcb4cd069c4ae56051792831a36385722fc95ea63e0f8a78417f9cf479dd7389",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[normal_text]": "22567b74d504a0a3b9e6676888224dfac10eb2509bf1bebec05ffb7c8f3c54cc",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[single_line_over]": "56d6ad0fa3828981feed2211dc236e502eb0c37e7c18b75fb26417ee6e6fd34f",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_nospace]": "85e814ffda72370f0e7f054713811a8cf1a765cba7905001ca64dc1a2f794f97",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_text]": "9fcb1d0d499c190314bb4cf9d161a4a20994d867572f9b3c9e523b0b6fec5108",
"T3T1_en_bitcoin-test_signmessage.py::test_signmessage_path_warning": "520e8e8518c199461e835a7d84635f013e4b665f2a3dc290ea0cdfc8ffeb33dc",
"T3T1_en_bitcoin-test_signtx.py::test_attack_change_input_address": "62f927a78bcb0b89e65ded4e2d349143e687f3a947a5827e936dcbefa337a8ae",
"T3T1_en_bitcoin-test_signtx.py::test_attack_change_outputs": "8ac53a90eafa8b8d392cf36ab42c797577928258c9205fd1847a1fff64d2be2b",
"T3T1_en_bitcoin-test_signtx.py::test_attack_modify_change_address": "1f6941265d2cf09bfbf79db2526242c8caa44a187e24f01a848ff02e9e84f244",
@ -18959,21 +18940,21 @@
"T3T1_en_bitcoin-test_signtx_taproot.py::test_send_p2tr[False]": "53511d2b2ccfa47ccc114861cc6c62a8cbd9aa5a1cda65d17488fed9d03c478f",
"T3T1_en_bitcoin-test_signtx_taproot.py::test_send_p2tr[True]": "ca34314cd4ed59516fe95514ca020017739374b19d315c622e901b2a173d4baf",
"T3T1_en_bitcoin-test_signtx_taproot.py::test_send_two_with_change": "2091037ff45d876fc3f2c47cbaf4fe67882fb97cb1b07afafbd6e7d7db116278",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_grs": "8774222aa7ffaca6d767fa0d323c39ffbf53cdd113bdaa68070b2b0a01375fc2",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_long": "63976f9d8e033d1d72c2a5567f5de2828aadecaa561bca7a797330e29743d773",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_testnet": "c87d4f66667ee698d8fc1a2f25642e4efd71a8f4a1f6176ce7a9e6067150384a",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_verify": "49b82a5d4384741da8cc06d844adfe64c4e9710dc805cfa50f7a31dcfcfdf3b9",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_verify_bcash": "66cad602f6a8aa960fe0a7c040baaaf2a9be29a8b35663b24e4c6de6ee6fa33b",
"T3T1_en_bitcoin-test_verifymessage.py::test_verify_bitcoind": "3fe7a318c6c5f6b729151274fc045ef7613bb8a788b31407f4bd1f89f981315d",
"T3T1_en_bitcoin-test_verifymessage.py::test_verify_utf": "2e61cd8165e9367a2235fc4e1c0699ddf8e928719efd7777069ee5f94c949a2a",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_long": "adfc1326cc4b4df2f316932ec847b261d444f967d959c08e4ba4ef07cac3c2ee",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_testnet": "8b0d1ac04237051807cc3741007ed7f81a8a6964ff1d7d12ce9da07dd1c51ef8",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_verify": "511b3abdc5312161852b65b919d2c533f67cb6f7833f5604e635886bbd640f32",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_verify_utf": "4561c7a72fe2512d6db042348f91e41f17df8cf60f41ec530684bf5c7bcd1ef9",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_long": "0a771818ba81808bcf0691ab1e7e60493f460141b4eb3056cd797f9636be66d9",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_testnet": "1f3ffb6945ea54f4abff98e2531dfb03f17cce71b4278c75859d1df65d52e345",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_verify": "c0d9b5df36bd7b52e9cbb369736023c6b9ffd3158d6138ac7f4640d68b6e2697",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_verify_utf": "457e4a8083428bcd5085900c2f571067cc75e968ceeddca98ecc44622a2723b0",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_grs": "6d4a8d4604f601f80be82f0f038d6caef866c82054949846e4f115e9774faef0",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_long": "770a0b7933a14b5a3b9911bdd25ea5c96e271c9849a29175ee77b96498777862",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_testnet": "92d844b2ec51ead7b5013343a4ea7a8ef683f8cba3b505938e8ddb7debe465f6",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_verify": "6e071435d62a5beba33278d1036957b499309cd989a05ab0a9f28f005520f02e",
"T3T1_en_bitcoin-test_verifymessage.py::test_message_verify_bcash": "5607540bd05406c5ac9526ee02377b92f30c410db113de3e1fa220c7bf5e1c1e",
"T3T1_en_bitcoin-test_verifymessage.py::test_verify_bitcoind": "4ca1c66ba5403e14c7141e80505f622f5db40bd1bc38cad4224a74fdfe665d91",
"T3T1_en_bitcoin-test_verifymessage.py::test_verify_utf": "8b2a3b5e55dfa774d83e8d21142a035330b437e4e94d8beb0878b4ef7cf74b2c",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_long": "0efff3f4184c7488064ccfdddb05a330bbd049137a45def0235be3dbda2e3219",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_testnet": "96e66eb12925d1c4b31ab1fa7c4afb688e8203edb09ab5624d9a8628a3487a7d",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_message_verify": "c183e117bdbfd04081265528c103136bc22af9854faa0a63b601193c19774c08",
"T3T1_en_bitcoin-test_verifymessage_segwit.py::test_verify_utf": "c08c7e64e166dbf3c122ed6c7181899b3882bc6e03ef6c2a708cee85b9e99450",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_long": "b6dc72bd8a8e9f6d27a2142329a9ccae258221921a4ca5d52deb0dc9063717bd",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_testnet": "b067955e593bdca92c2fde93ee342a285a63dd14ea1ea36afab87085dd5a02a3",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_message_verify": "846acdbfaa7973769a82ba4b5fbd88d3bc61c02916438f6b1c517b013cbfaef2",
"T3T1_en_bitcoin-test_verifymessage_segwit_native.py::test_verify_utf": "127c596ccf2bcaabffd7f7942eae69b8ec3ab6691132c846dc8dd9ff32759271",
"T3T1_en_bitcoin-test_zcash.py::test_external_presigned": "e516a868683d108e916edee64ae6b6de68cb3bfc6a018aade8483202f7b7c26a",
"T3T1_en_bitcoin-test_zcash.py::test_one_one_fee_sapling": "5a87239937a388a1a3ba4e371142e0e488e8270d13b3f4ffde9b3970b9575767",
"T3T1_en_bitcoin-test_zcash.py::test_spend_old_versions": "7464fc1bb6d19d9a3670ff58b04490684f1163ad03426f578654f3817f1554fb",
@ -19317,10 +19298,10 @@
"T3T1_en_ethereum-test_definitions.py::test_chain_id_allowed": "7daae0a73f578b588d7051e8da25b2121ddc971a550fb2a65a288f770ba23d07",
"T3T1_en_ethereum-test_definitions.py::test_chain_id_mismatch": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_definition_does_not_override_builtin": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_token_mismatch": "5594f33aad9731a6575ed40bf8362b03d1a2d39411e88e2b11f7749eb11b9d80",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_token_ok": "4b512d783707893aee5e52e1709fbacf286e14aaa0ac85e7c4786d3952a00af2",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_without_token": "2d3e0f7eeacf34df053401319a02b9f91da70f8c6a07ad933ea537dc83e11ce9",
"T3T1_en_ethereum-test_definitions.py::test_external_token": "f48184a187991499473244d4872ae4ef5bd7b80adcf71f7a577ab95d8922aa46",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_token_mismatch": "cbf597551b5c08a3c5d8e777ab3a9df39ec0652813f8c3f2f137d457c7aeb838",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_token_ok": "d7d82df450b05a26c35d639b2dc527fa4a356fa3fb3da8c9c3ab8e1b12ec7bbc",
"T3T1_en_ethereum-test_definitions.py::test_external_chain_without_token": "78f90c45fc4919f6a2e5da233be493e90b9691e908ccc983c9340850a645cde8",
"T3T1_en_ethereum-test_definitions.py::test_external_token": "e1449f004dd877f4d96ed2636cd0f96b3f154a6240f3f37d4e396060d79839b4",
"T3T1_en_ethereum-test_definitions.py::test_method_builtin[_call_getaddress]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_method_builtin[_call_sign_typed_data]": "eab5a0fb53c4503b745f12fdd2db6f320caf7fd359d6f0bf14b393e0adef9af0",
"T3T1_en_ethereum-test_definitions.py::test_method_builtin[_call_signmessage]": "8777026a6234d2a6b8d898a15c60901bb6b81630786f31cff975712e7424b1f3",
@ -19334,7 +19315,7 @@
"T3T1_en_ethereum-test_definitions.py::test_method_external_mismatch[_call_sign_typed_data]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_method_external_mismatch[_call_signmessage]": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_slip44_disallowed": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions.py::test_slip44_external": "4458d1110986ddcb4fd7aed32c018978cc3221f29151321f7b174f89cc2f273d",
"T3T1_en_ethereum-test_definitions.py::test_slip44_external": "3a3abfd3700491806152e6589c095f8ff509b0e1fc94e25db12c577f00913572",
"T3T1_en_ethereum-test_definitions.py::test_slip44_external_disallowed": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions_bad.py::test_bad_prefix": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_ethereum-test_definitions_bad.py::test_bad_proof": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
@ -19541,14 +19522,14 @@
"T3T1_en_reset_recovery-test_recovery_bip39_t2.py::test_tt_nopin_nopassphrase": "335a5a9a1b71d445f5528aefcf664689baa2cbc30a7350c5f97a949f4813dda7",
"T3T1_en_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "d91852e23f8a089b9f3c38f20fa5bd24a27365f7a47284ea059e6174cf5f0bd2",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "587a9d7ebe9f38edb69ed2ef3ebce72274f13ece62ac77566d5d106d3112b878",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "f50972880eacfadb02252ddd77cf95f75b9bfef68e8d0a5d14da5b6f765d3a31",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "1e10bf06c3f6ee2322a5b18407afcd56ae00cd1050f8d478797f71b0f01c15bb",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "d9069164bffce9ffcd768986bd9f6b9091d1a93b42d1031531755f8559accded",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "251a0f02d357a1634ece78931d3d0a5ea5e2ed1db59709420170da3c44b24cfc",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "38d2728275c00a63b2f42d868fc51ff7067268418f123ef62b3fee229c15d489",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "22368230917b79e6ddd9c05c94fb9707829ca3ea344972e71ea9167fdbab512d",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "8b7f0f8a29a71df1561aa347caf57410dbb80c4fc5eb15f2c1b08fa8ccaea3f2",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "c19ef735f3a4f28b1d151230548f60aabeef4e99a053d1883971fcf1bd2c83f5",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "8fa4c189c7a584755be4e7cd02ffb954040065c00eb9ff15a93c2408ba656ec3",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "9ab79f7aa8130f98fa24cc653c883a9a341bcac08ea2de6483bccf28d031907c",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "1cd935cab0ba2407a31ccd7105c5e44883ee293d76a68437c3f617d9e2da7cc3",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "b746b024274f7008bcb15d5cc1b9df818165b95da8d0e236c21cb7c1c0886e61",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "dedd702fdfd3d7131088bf8b1c2ef803a822f779315fad0a5a75ae7f7b313ff6",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "071494931a255961687a551e6575ec911a532402fff59aaeb5a564837ddd2ba7",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "e28016b19934e6c465a342111432fc16dd4edd8b4ad584d502ce74aa34afa170",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "04284464e108c182d2def58391cbd72815ef5747a7baf0564145522b49025061",
"T3T1_en_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "01238e49c12e7419cbddf33b535a57e1d1283cd60f308462b7644e95a03d13d0",
"T3T1_en_reset_recovery-test_recovery_slip39_basic.py::test_abort": "587a9d7ebe9f38edb69ed2ef3ebce72274f13ece62ac77566d5d106d3112b878",
"T3T1_en_reset_recovery-test_recovery_slip39_basic.py::test_invalid_mnemonic_first_share": "3c8d138bd4f0485864a6d3c1786a5bcb2267ce85d48e7e6ca1a9425b2b76b748",
@ -19731,7 +19712,7 @@
"T3T1_en_test_autolock.py::test_apply_auto_lock_delay_valid[60]": "d5dacbfada3fd814942e9ecf45f949b9471533ee2e8f0abc120afca3eb94a2b9",
"T3T1_en_test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "72cfddf432bd7f4a21f7e5598b6670b44e37ffcdd4b8d7e766f7efe2524ee3de",
"T3T1_en_test_autolock.py::test_autolock_cancels_ui": "107fef3042f43e6426bdb3fa75e5796c3c543062a63ca1d7800b83ded3c080f3",
"T3T1_en_test_autolock.py::test_autolock_default_value": "cea810ad632407b1e8fbfdac17ccc89b1789895d113997657a42562f06717fc8",
"T3T1_en_test_autolock.py::test_autolock_default_value": "ce7bdbdc4938e4725951ebbc00cfc8f242a3fa430c2206a7905e528acc3b4048",
"T3T1_en_test_autolock.py::test_autolock_ignores_getaddress": "b2c6e6edbc2a19419193502dc652cd8c0aeeac08c9126905d655e7431422b9b9",
"T3T1_en_test_autolock.py::test_autolock_ignores_initialize": "b2c6e6edbc2a19419193502dc652cd8c0aeeac08c9126905d655e7431422b9b9",
"T3T1_en_test_basic.py::test_capabilities": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
@ -19771,15 +19752,15 @@
"T3T1_en_test_language.py::test_switch_from_english_not_silent": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_test_language.py::test_switch_language": "f0e2703955ee26c7d9918a510b6b2744a8d6df0f83e0dcc8031be465dd1b77b4",
"T3T1_en_test_language.py::test_translations_renders_on_screen": "386d167297cc9f3f421fd44e816f5643adf63b74bef472730bd2d84a3858fb25",
"T3T1_en_test_msg_applysettings.py::test_apply_homescreen_jpeg": "6d357a26a304a5626d93418ca9fc40de0acc4d98dc78d7cc16b913be680a3fe4",
"T3T1_en_test_msg_applysettings.py::test_apply_homescreen_jpeg": "51232e5df87a3709e68602d4a223cd4e910ad3a78114febd53b7e7c1db98b25d",
"T3T1_en_test_msg_applysettings.py::test_apply_homescreen_jpeg_progressive": "8c7c77033a5713b5711bfadffa4571699a57e45a7fe706bce1124aac9d388b04",
"T3T1_en_test_msg_applysettings.py::test_apply_homescreen_jpeg_wrong_size": "8c7c77033a5713b5711bfadffa4571699a57e45a7fe706bce1124aac9d388b04",
"T3T1_en_test_msg_applysettings.py::test_apply_homescreen_toif": "8c7c77033a5713b5711bfadffa4571699a57e45a7fe706bce1124aac9d388b04",
"T3T1_en_test_msg_applysettings.py::test_apply_settings": "32ab92c6244ed354fbe9eacac290fc387cf4cd69ff0321f85b253e4fbb6dbccc",
"T3T1_en_test_msg_applysettings.py::test_apply_settings": "cd172368ef0a87f66c26666a8984c1b3b4abe8a740dc588968400587d19cf83b",
"T3T1_en_test_msg_applysettings.py::test_apply_settings_passphrase": "f94f6a29ef26d7f65da8b654eb2af8fd62d980317f76236fe8fcff757e610c32",
"T3T1_en_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "34dd7ad1f185a666c2383500eac5825887286b307839995cd8514d2f3aa1349a",
"T3T1_en_test_msg_applysettings.py::test_apply_settings_rotation": "51675a9ea00ff4b3978e8f9d3f162ea6b83c806af17f710d4c0c4dc98941c9a5",
"T3T1_en_test_msg_applysettings.py::test_experimental_features": "5c7dfd9d9149551760fc232353de0e85d855f2b0825e7b74c50156bc4c7c2615",
"T3T1_en_test_msg_applysettings.py::test_experimental_features": "a9dc0aad862b5d5d3443272a3e7fa5ebc414e7836c4a4d70266e900185af4b0f",
"T3T1_en_test_msg_applysettings.py::test_label_too_long": "0373a0f99ec1445924dc60f071ca11f626176d9b7e40ba4e0b0afad195ad31e0",
"T3T1_en_test_msg_applysettings.py::test_safety_checks": "2fa4cfafc8252551e45da6e53ff029ddf3661639e3108d79920cb2300f85ffea",
"T3T1_en_test_msg_backup_device.py::test_backup_bip39": "9a1005431cbe80dff773b55dd1daeaae135dda1e450539442b825b113e4f2dd6",