From 7db1529533599c20e52060f3900afb9870c2b21c Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Mon, 26 Aug 2024 17:58:29 +0200 Subject: [PATCH] refactor(core/ui): alternative multipage footer --- core/embed/rust/src/ui/flow/page.rs | 4 + .../component/choose_credential.rs | 101 +++++ .../src/ui/model_mercury/component/footer.rs | 345 +++++++++++------- .../src/ui/model_mercury/component/frame.rs | 28 +- .../src/ui/model_mercury/component/mod.rs | 4 + .../ui/model_mercury/component/share_words.rs | 7 +- .../model_mercury/component/swipe_content.rs | 3 +- .../model_mercury/component/vertical_menu.rs | 4 +- .../src/ui/model_mercury/flow/confirm_fido.rs | 32 +- core/embed/rust/src/ui/shape/render.rs | 7 + .../webauthn/list_resident_credentials.py | 1 + 11 files changed, 376 insertions(+), 160 deletions(-) create mode 100644 core/embed/rust/src/ui/model_mercury/component/choose_credential.rs diff --git a/core/embed/rust/src/ui/flow/page.rs b/core/embed/rust/src/ui/flow/page.rs index 247a0399b54..b5138699c65 100644 --- a/core/embed/rust/src/ui/flow/page.rs +++ b/core/embed/rust/src/ui/flow/page.rs @@ -35,6 +35,10 @@ impl SwipePage { current: 0, } } + + pub fn inner(&self) -> &T { + &self.inner + } } impl Component for SwipePage { diff --git a/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs b/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs new file mode 100644 index 00000000000..43cc6b4bf3e --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs @@ -0,0 +1,101 @@ +use crate::{ + strutil::TString, + translations::TR, + ui::{ + component::{swipe_detect::SwipeSettings, Component, SwipeDirection}, + flow::{Swipable, SwipePage}, + }, +}; + +use super::{ + Frame, FrameMsg, InternallySwipable as _, PagedVerticalMenu, SwipeContent, + VerticalMenuChoiceMsg, +}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ConfirmFido { + Intro, + ChooseCredential, + Details, + Tap, + Menu, +} + +/// Wrapper that updates `Footer` content whenever page is changed. +pub struct ChooseCredential TString<'static>>( + Frame>>>, +); + +impl TString<'static>> ChooseCredential { + pub fn new(label_fn: F, num_accounts: usize) -> Self { + let content_choose_credential = Frame::left_aligned( + TR::fido__title_select_credential.into(), + SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new( + num_accounts, + label_fn, + ))), + ) + .with_subtitle(TR::fido__title_for_authentication.into()) + .with_menu_button() + .with_footer_page_hint( + TR::fido__more_credentials.into(), + TR::buttons__go_back.into(), + TR::instructions__swipe_up.into(), + TR::instructions__swipe_down.into(), + ) + .with_swipe(SwipeDirection::Down, SwipeSettings::default()) + .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) + .with_vertical_pages(); + + Self(content_choose_credential) + } +} + +impl TString<'static>> Component for ChooseCredential { + type Msg = FrameMsg; + + fn place(&mut self, bounds: crate::ui::geometry::Rect) -> crate::ui::geometry::Rect { + self.0.place(bounds) + } + + fn event( + &mut self, + ctx: &mut crate::ui::component::EventCtx, + event: crate::ui::component::Event, + ) -> Option { + let msg = self.0.event(ctx, event); + let current_page = self.0.inner().inner().inner().current_page(); + + self.0.update_footer_counter( + ctx, + current_page, + Some(self.0.inner().inner().inner().num_pages()), + ); + msg + } + + fn paint(&mut self) { + self.0.paint() + } + + fn render<'s>(&'s self, target: &mut impl crate::ui::shape::Renderer<'s>) { + self.0.render(target) + } +} + +impl TString<'static>> Swipable for ChooseCredential { + fn get_swipe_config(&self) -> crate::ui::component::swipe_detect::SwipeConfig { + self.0.get_swipe_config() + } + + fn get_internal_page_count(&self) -> usize { + self.0.get_internal_page_count() + } +} + +#[cfg(feature = "ui_debug")] +impl TString<'static>> crate::trace::Trace for ChooseCredential { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.0.trace(t) + } +} diff --git a/core/embed/rust/src/ui/model_mercury/component/footer.rs b/core/embed/rust/src/ui/model_mercury/component/footer.rs index 7ee6e2a08a0..fd5836027a4 100644 --- a/core/embed/rust/src/ui/model_mercury/component/footer.rs +++ b/core/embed/rust/src/ui/model_mercury/component/footer.rs @@ -22,8 +22,7 @@ use crate::{ #[derive(Clone)] pub struct Footer<'a> { area: Rect, - instruction: TString<'a>, - content: Option>, + content: FooterContent<'a>, swipe_allow_up: bool, swipe_allow_down: bool, progress: i16, @@ -32,8 +31,10 @@ pub struct Footer<'a> { #[derive(Clone)] enum FooterContent<'a> { - Description(TString<'a>), + Instruction(TString<'a>), + InstructionDescription(TString<'a>, TString<'a>), PageCounter(PageCounter), + PageHint(PageHint), } impl<'a> Footer<'a> { @@ -45,14 +46,10 @@ impl<'a> Footer<'a> { const STYLE_INSTRUCTION: &'static TextStyle = &theme::TEXT_SUB_GREY; const STYLE_DESCRIPTION: &'static TextStyle = &theme::TEXT_SUB_GREY_LIGHT; - pub fn new>>( - instruction: T, - description: Option>, - ) -> Self { + fn from_content(content: FooterContent<'a>) -> Self { Self { area: Rect::zero(), - instruction: instruction.into(), - content: description.map(FooterContent::Description), + content, swipe_allow_down: false, swipe_allow_up: false, progress: 0, @@ -60,38 +57,79 @@ impl<'a> Footer<'a> { } } - pub fn with_page_counter(self, max_pages: u8) -> Self { - Self { - content: Some(FooterContent::PageCounter(PageCounter::new(max_pages))), - ..self - } + pub fn new>>( + instruction: T, + description: Option>, + ) -> Self { + let instruction = instruction.into(); + Self::from_content( + description + .map(|d| FooterContent::InstructionDescription(instruction, d)) + .unwrap_or(FooterContent::Instruction(instruction)), + ) } - pub fn update_instruction>>(&mut self, ctx: &mut EventCtx, s: T) { - self.instruction = s.into(); - ctx.request_paint(); + pub fn with_page_counter(max_pages: u8, instruction: TString<'static>) -> Self { + Self::from_content(FooterContent::PageCounter(PageCounter::new( + max_pages, + instruction, + ))) } - pub fn update_description>>(&mut self, ctx: &mut EventCtx, s: T) { - if let Some(ref mut content) = self.content { - if let Some(text) = content.as_description_mut() { - *text = s.into(); - ctx.request_paint(); - } else { + pub fn with_page_hint( + description: TString<'static>, + description_last: TString<'static>, + instruction: TString<'static>, + instruction_last: TString<'static>, + ) -> Self { + Self::from_content(FooterContent::PageHint(PageHint { + description, + description_last, + instruction, + instruction_last, + page_curr: 0, + page_num: 1, + })) + } + + pub fn update_instruction>>(&mut self, ctx: &mut EventCtx, s: T) { + match &mut self.content { + FooterContent::Instruction(i) => *i = s.into(), + FooterContent::InstructionDescription(i, _d) => *i = s.into(), + FooterContent::PageCounter(page_counter) => page_counter.instruction = s.into(), + _ => { #[cfg(feature = "ui_debug")] - panic!("footer does not have description") + panic!("not supported") } } + ctx.request_paint(); + } + + pub fn update_description>>(&mut self, ctx: &mut EventCtx, s: T) { + if let FooterContent::InstructionDescription(_i, d) = &mut self.content { + *d = s.into(); + ctx.request_paint(); + } else { + #[cfg(feature = "ui_debug")] + panic!("footer does not have description") + } } - pub fn update_page_counter(&mut self, ctx: &mut EventCtx, n: u8) { - if let Some(ref mut content) = self.content { - if let Some(counter) = content.as_page_counter_mut() { - counter.update_current_page(n); + pub fn update_page_counter(&mut self, ctx: &mut EventCtx, current: usize, max: Option) { + match &mut self.content { + FooterContent::PageCounter(counter) => { + counter.update_current_page(current); self.swipe_allow_down = counter.is_first_page(); self.swipe_allow_up = counter.is_last_page(); ctx.request_paint(); - } else { + } + FooterContent::PageHint(page_hint) => { + page_hint.update_current_page(current, max); + self.swipe_allow_down = page_hint.is_first_page(); + self.swipe_allow_up = page_hint.is_last_page(); + ctx.request_paint(); + } + _ => { #[cfg(feature = "ui_debug")] panic!("footer does not have counter") } @@ -99,11 +137,7 @@ impl<'a> Footer<'a> { } pub fn height(&self) -> i16 { - if self.content.is_some() { - Footer::HEIGHT_DEFAULT - } else { - Footer::HEIGHT_SIMPLE - } + self.content.height() } pub fn with_swipe(self, swipe_direction: SwipeDirection) -> Self { @@ -125,15 +159,7 @@ impl<'a> Component for Footer<'a> { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { - let h = bounds.height(); - assert!(h == Footer::HEIGHT_SIMPLE || h == Footer::HEIGHT_DEFAULT); - if let Some(content) = &mut self.content { - if let Some(counter) = content.as_page_counter_mut() { - if h == Footer::HEIGHT_DEFAULT { - counter.place(bounds.split_top(Footer::HEIGHT_SIMPLE).0); - } - } - } + assert!(bounds.height() == self.content.height()); self.area = bounds; self.area } @@ -189,49 +215,7 @@ impl<'a> Component for Footer<'a> { }; target.with_origin(offset, &|target| { - // show description/counter only if there is space for it - if self.area.height() == Footer::HEIGHT_DEFAULT { - if let Some(content) = &self.content { - match content { - FooterContent::Description(text) => { - let area_description = self.area.split_top(Footer::HEIGHT_SIMPLE).0; - let text_description_font_descent = Footer::STYLE_DESCRIPTION - .text_font - .visible_text_height_ex("Ay") - .1; - let text_description_baseline = area_description.bottom_center() - - Offset::y(text_description_font_descent); - - text.map(|t| { - Text::new(text_description_baseline, t) - .with_font(Footer::STYLE_DESCRIPTION.text_font) - .with_fg(Footer::STYLE_DESCRIPTION.text_color) - .with_align(Alignment::Center) - .render(target); - }); - } - FooterContent::PageCounter(counter) => { - counter.render(target); - } - } - } - } - - let area_instruction = self.area.split_bottom(Footer::HEIGHT_SIMPLE).1; - let text_instruction_font_descent = Footer::STYLE_INSTRUCTION - .text_font - .visible_text_height_ex("Ay") - .1; - let text_instruction_baseline = - area_instruction.bottom_center() - Offset::y(text_instruction_font_descent); - self.instruction.map(|t| { - Text::new(text_instruction_baseline, t) - .with_font(Footer::STYLE_INSTRUCTION.text_font) - .with_fg(Footer::STYLE_INSTRUCTION.text_color) - .with_align(Alignment::Center) - .render(target); - }); - + self.content.render(self.area, target); shape::Bar::new(self.area) .with_alpha(mask) .with_fg(Color::black()) @@ -245,89 +229,138 @@ impl<'a> Component for Footer<'a> { impl crate::trace::Trace for Footer<'_> { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("Footer"); - if let Some(content) = &self.content { - match content { - FooterContent::Description(text) => t.string("description", *text), - FooterContent::PageCounter(counter) => counter.trace(t), + match &self.content { + FooterContent::Instruction(i) => { + t.string("instruction", *i); + } + FooterContent::InstructionDescription(i, d) => { + t.string("description", *d); + t.string("instruction", *i); + } + FooterContent::PageCounter(counter) => counter.trace(t), + FooterContent::PageHint(page_hint) => { + t.string("description", page_hint.description()); + t.string("instruction", page_hint.instruction()); } } - t.string("instruction", self.instruction); } } impl<'a> FooterContent<'a> { - pub fn as_description_mut(&mut self) -> Option<&mut TString<'a>> { - match self { - FooterContent::Description(ref mut text) => Some(text), - _ => None, + fn height(&self) -> i16 { + if matches!(self, FooterContent::Instruction(_)) { + Footer::HEIGHT_SIMPLE + } else { + Footer::HEIGHT_DEFAULT } } - pub fn as_page_counter_mut(&mut self) -> Option<&mut PageCounter> { + fn render<'s>(&'s self, area: Rect, target: &mut impl Renderer<'s>) + where + 's: 'a, + { match self { - FooterContent::PageCounter(ref mut counter) => Some(counter), - _ => None, + FooterContent::Instruction(instruction) => { + Self::render_instruction(target, area, instruction); + } + FooterContent::InstructionDescription(instruction, description) => { + Self::render_description(target, area, description); + Self::render_instruction(target, area, instruction); + } + FooterContent::PageCounter(page_counter) => page_counter.render(target, area), + FooterContent::PageHint(page_hint) => { + Self::render_description(target, area, &page_hint.description()); + Self::render_instruction(target, area, &page_hint.instruction()); + } } } + + fn render_description<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + description: &TString<'a>, + ) { + let area_description = area.split_top(Footer::HEIGHT_SIMPLE).0; + let text_description_font_descent = Footer::STYLE_DESCRIPTION + .text_font + .visible_text_height_ex("Ay") + .1; + let text_description_baseline = + area_description.bottom_center() - Offset::y(text_description_font_descent); + + description.map(|t| { + Text::new(text_description_baseline, t) + .with_font(Footer::STYLE_DESCRIPTION.text_font) + .with_fg(Footer::STYLE_DESCRIPTION.text_color) + .with_align(Alignment::Center) + .render(target) + }); + } + + fn render_instruction<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + instruction: &TString<'a>, + ) { + let area_instruction = area.split_bottom(Footer::HEIGHT_SIMPLE).1; + let text_instruction_font_descent = Footer::STYLE_INSTRUCTION + .text_font + .visible_text_height_ex("Ay") + .1; + let text_instruction_baseline = + area_instruction.bottom_center() - Offset::y(text_instruction_font_descent); + instruction.map(|t| { + Text::new(text_instruction_baseline, t) + .with_font(Footer::STYLE_INSTRUCTION.text_font) + .with_fg(Footer::STYLE_INSTRUCTION.text_color) + .with_align(Alignment::Center) + .render(target) + }); + } } /// Helper component used within Footer instead of description for page count /// indication, rendered e.g. as: '1 / 20'. #[derive(Clone)] struct PageCounter { - area: Rect, + pub instruction: TString<'static>, font: Font, page_curr: u8, page_max: u8, } impl PageCounter { - fn new(page_max: u8) -> Self { + fn new(page_max: u8, instruction: TString<'static>) -> Self { Self { - area: Rect::zero(), - page_curr: 1, + instruction, + page_curr: 0, page_max, font: Font::SUB, } } - fn update_current_page(&mut self, new_value: u8) { - self.page_curr = new_value.clamp(1, self.page_max); + fn update_current_page(&mut self, new_value: usize) { + self.page_curr = (new_value as u8).clamp(0, self.page_max.saturating_sub(1)); } fn is_first_page(&self) -> bool { - self.page_curr == 1 + self.page_curr == 0 } fn is_last_page(&self) -> bool { - self.page_curr == self.page_max + self.page_curr + 1 == self.page_max } } -impl Component for PageCounter { - type Msg = Never; - - fn place(&mut self, bounds: Rect) -> Rect { - self.area = bounds; - self.area - } - - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { - None - } - - fn paint(&mut self) { - todo!("remove when ui-t3t1 done") - } - - fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let color = if self.page_curr == self.page_max { +impl PageCounter { + fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) { + let color = if self.is_last_page() { theme::GREEN_LIGHT } else { theme::GREY_LIGHT }; - let string_curr = uformat!("{}", self.page_curr); + let string_curr = uformat!("{}", self.page_curr + 1); let string_max = uformat!("{}", self.page_max); // center the whole counter "x / yz" @@ -337,8 +370,9 @@ impl Component for PageCounter { let width_num_max = self.font.text_width(&string_max); let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x.x; - let center_x = self.area.center().x; - let counter_y = self.font.vert_center(self.area.y0, self.area.y1, "0"); + let counter_area = area.split_top(Footer::HEIGHT_SIMPLE).0; + let center_x = counter_area.center().x; + let counter_y = self.font.vert_center(counter_area.y0, counter_area.y1, "0"); let counter_start_x = center_x - width_total / 2; let counter_end_x = center_x + width_total / 2; let base_num_curr = Point::new(counter_start_x, counter_y); @@ -359,6 +393,8 @@ impl Component for PageCounter { .with_fg(color) .with_font(self.font) .render(target); + + FooterContent::render_instruction(target, area, &self.instruction); } } @@ -370,3 +406,58 @@ impl crate::trace::Trace for PageCounter { t.int("page max", self.page_max.into()); } } + +#[derive(Clone)] +struct PageHint { + pub description: TString<'static>, + pub description_last: TString<'static>, + pub instruction: TString<'static>, + pub instruction_last: TString<'static>, + pub page_curr: u8, + pub page_num: u8, +} + +impl PageHint { + fn update_current_page(&mut self, current: usize, max: Option) { + self.page_curr = (current as u8).clamp(0, self.page_num.saturating_sub(1)); + if let Some(max) = max { + self.page_num = max as u8; + } + } + + fn update_max_page(&mut self, max: usize) { + self.page_num = max as u8; + } + + fn is_single_page(&self) -> bool { + self.page_num <= 1 + } + + fn is_first_page(&self) -> bool { + self.page_curr == 0 + } + + fn is_last_page(&self) -> bool { + self.page_curr + 1 == self.page_num + } + + fn description(&self) -> TString<'static> { + if self.is_single_page() { + TString::empty() + } else if self.is_last_page() { + self.description_last + } else { + self.description + } + } + + fn instruction(&self) -> TString<'static> { + if self.is_single_page() { + TString::empty() + } else if self.is_last_page() { + self.instruction_last + } else { + self.instruction + } + } +} diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index 1a70134da65..410c04b94b9 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -196,7 +196,24 @@ where #[inline(never)] pub fn with_footer_counter(mut self, instruction: TString<'static>, max_value: u8) -> Self { - self.footer = Some(Footer::new(instruction, None).with_page_counter(max_value)); + self.footer = Some(Footer::with_page_counter(max_value, instruction)); + self + } + + #[inline(never)] + pub fn with_footer_page_hint( + mut self, + description: TString<'static>, + description_last: TString<'static>, + instruction: TString<'static>, + instruction_last: TString<'static>, + ) -> Self { + self.footer = Some(Footer::with_page_hint( + description, + description_last, + instruction, + instruction_last, + )); self } @@ -231,9 +248,14 @@ where res } - pub fn update_footer_counter(&mut self, ctx: &mut EventCtx, new_val: u8) { + pub fn update_footer_counter( + &mut self, + ctx: &mut EventCtx, + current: usize, + max: Option, + ) { if let Some(footer) = &mut self.footer { - footer.update_page_counter(ctx, new_val); + footer.update_page_counter(ctx, current, max); } } diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index 113ab70d779..48f3a35131a 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -4,6 +4,8 @@ mod address_details; mod binary_selection; pub mod bl_confirm; mod button; +#[cfg(feature = "universal_fw")] +mod choose_credential; #[cfg(feature = "translations")] mod coinjoin_progress; mod fido; @@ -46,6 +48,8 @@ pub use address_details::AddressDetails; #[cfg(feature = "ui_overlay")] pub use binary_selection::{BinarySelection, BinarySelectionMsg}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText}; +#[cfg(feature = "universal_fw")] +pub use choose_credential::ChooseCredential; #[cfg(feature = "translations")] pub use coinjoin_progress::CoinJoinProgress; pub use error::ErrorScreen; diff --git a/core/embed/rust/src/ui/model_mercury/component/share_words.rs b/core/embed/rust/src/ui/model_mercury/component/share_words.rs index 396200fae61..46562c79bfa 100644 --- a/core/embed/rust/src/ui/model_mercury/component/share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/component/share_words.rs @@ -84,9 +84,9 @@ impl<'a> Component for ShareWords<'a> { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - let page_index = self.frame.inner().inner().page_index as u8; + let page_index = self.frame.inner().inner().page_index; if let Some(repeated_indices) = &self.repeated_indices { - if repeated_indices.contains(&page_index) { + if repeated_indices.contains(&(page_index as u8)) { let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated); self.frame .update_subtitle(ctx, updated_subtitle, Some(theme::TEXT_SUB_GREEN_LIME)); @@ -95,7 +95,8 @@ impl<'a> Component for ShareWords<'a> { .update_subtitle(ctx, self.subtitle, Some(theme::TEXT_SUB_GREY)); } } - self.frame.update_footer_counter(ctx, page_index + 1); + self.frame + .update_footer_counter(ctx, page_index as usize, None); self.frame.event(ctx, event) } diff --git a/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs b/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs index fb42a6c37d2..8e484f084db 100644 --- a/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs +++ b/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs @@ -7,8 +7,7 @@ use crate::{ event::SwipeEvent, geometry::{Offset, Rect}, lerp::Lerp, - shape, - shape::Renderer, + shape::{self, Renderer}, util::animation_disabled, }, }; diff --git a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs index bb7744c538d..11c3ae18c2a 100644 --- a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs +++ b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs @@ -1,6 +1,6 @@ use heapless::Vec; -use super::theme; +use super::{theme, InternallySwipable}; use crate::{ strutil::TString, time::{Duration, Stopwatch}, @@ -266,7 +266,7 @@ impl Component for VerticalMenu { let opacities = [item1_opacity, item2_opacity, item3_opacity]; - target.with_origin(offset, &|target| { + target.with_translate(offset, &|target| { // render buttons separated by thin bars for (i, button) in (&self.buttons).into_iter().take(self.n_items).enumerate() { button.render(target); diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs index a13bb161841..3961c4114d2 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs @@ -11,16 +11,16 @@ use crate::{ }, flow::{ base::{DecisionBuilder as _, StateChange}, - FlowMsg, FlowState, SwipeFlow, SwipePage, + FlowMsg, FlowState, SwipeFlow, }, layout::obj::LayoutObj, - model_mercury::component::{FidoCredential, SwipeContent}, }, }; use super::super::{ component::{ - Frame, FrameMsg, PagedVerticalMenu, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg, + ChooseCredential, FidoCredential, Frame, FrameMsg, PromptMsg, PromptScreen, SwipeContent, + VerticalMenu, VerticalMenuChoiceMsg, }, theme, }; @@ -49,6 +49,7 @@ impl FlowState for ConfirmFido { match (self, direction) { (Self::Intro, SwipeDirection::Left) => Self::Menu.swipe(direction), (Self::Intro, SwipeDirection::Up) => Self::ChooseCredential.swipe(direction), + (Self::ChooseCredential, SwipeDirection::Down) => Self::Intro.swipe(direction), (Self::Details, SwipeDirection::Up) => Self::Tap.swipe(direction), (Self::Tap, SwipeDirection::Down) => Self::Details.swipe(direction), _ => self.do_nothing(), @@ -122,26 +123,11 @@ impl ConfirmFido { .try_into() .unwrap_or_else(|_| TString::from_str("-")) }; - let content_choose_credential = Frame::left_aligned( - TR::fido__title_select_credential.into(), - SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new( - num_accounts, - label_fn, - ))), - ) - .with_subtitle(TR::fido__title_for_authentication.into()) - .with_menu_button() - .with_footer( - TR::instructions__swipe_up.into(), - (num_accounts > 2).then_some(TR::fido__more_credentials.into()), - ) - .with_swipe(SwipeDirection::Down, SwipeSettings::default()) - .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) - .with_vertical_pages() - .map(|msg| match msg { - FrameMsg::Button(_) => Some(FlowMsg::Info), - FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), - }); + let content_choose_credential = + ChooseCredential::new(label_fn, num_accounts).map(|msg| match msg { + FrameMsg::Button(_) => Some(FlowMsg::Info), + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + }); let get_account = move || { let current = CRED_SELECTED.load(Ordering::Relaxed); diff --git a/core/embed/rust/src/ui/shape/render.rs b/core/embed/rust/src/ui/shape/render.rs index 9a59a996a0b..2caaa382ea9 100644 --- a/core/embed/rust/src/ui/shape/render.rs +++ b/core/embed/rust/src/ui/shape/render.rs @@ -51,6 +51,13 @@ pub trait Renderer<'a> { inner(self); self.set_viewport(original); } + + fn with_translate(&mut self, offset: Offset, inner: &dyn Fn(&mut Self)) { + let original = self.viewport(); + self.set_viewport(self.viewport().translate(offset)); + inner(self); + self.set_viewport(original); + } } // ========================================================================== diff --git a/core/src/apps/webauthn/list_resident_credentials.py b/core/src/apps/webauthn/list_resident_credentials.py index 0c5e92dbe7d..1151cfc708e 100644 --- a/core/src/apps/webauthn/list_resident_credentials.py +++ b/core/src/apps/webauthn/list_resident_credentials.py @@ -18,6 +18,7 @@ async def list_resident_credentials( TR.fido__title_list_credentials, description=TR.fido__export_credentials, verb=TR.buttons__export, + prompt_screen=True, ) creds = [ WebAuthnCredential(