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

refactor(core/ui): alternative multipage footer

This commit is contained in:
Martin Milata 2024-08-26 17:58:29 +02:00
parent 2572705d1f
commit 7db1529533
11 changed files with 376 additions and 160 deletions

View File

@ -35,6 +35,10 @@ impl<T: Component + Paginate> SwipePage<T> {
current: 0, current: 0,
} }
} }
pub fn inner(&self) -> &T {
&self.inner
}
} }
impl<T: Component + Paginate> Component for SwipePage<T> { impl<T: Component + Paginate> Component for SwipePage<T> {

View File

@ -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<F: Fn(usize) -> TString<'static>>(
Frame<SwipeContent<SwipePage<PagedVerticalMenu<F>>>>,
);
impl<F: Fn(usize) -> TString<'static>> ChooseCredential<F> {
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<F: Fn(usize) -> TString<'static>> Component for ChooseCredential<F> {
type Msg = FrameMsg<VerticalMenuChoiceMsg>;
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<Self::Msg> {
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<F: Fn(usize) -> TString<'static>> Swipable for ChooseCredential<F> {
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<F: Fn(usize) -> TString<'static>> crate::trace::Trace for ChooseCredential<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.0.trace(t)
}
}

View File

@ -22,8 +22,7 @@ use crate::{
#[derive(Clone)] #[derive(Clone)]
pub struct Footer<'a> { pub struct Footer<'a> {
area: Rect, area: Rect,
instruction: TString<'a>, content: FooterContent<'a>,
content: Option<FooterContent<'a>>,
swipe_allow_up: bool, swipe_allow_up: bool,
swipe_allow_down: bool, swipe_allow_down: bool,
progress: i16, progress: i16,
@ -32,8 +31,10 @@ pub struct Footer<'a> {
#[derive(Clone)] #[derive(Clone)]
enum FooterContent<'a> { enum FooterContent<'a> {
Description(TString<'a>), Instruction(TString<'a>),
InstructionDescription(TString<'a>, TString<'a>),
PageCounter(PageCounter), PageCounter(PageCounter),
PageHint(PageHint),
} }
impl<'a> Footer<'a> { impl<'a> Footer<'a> {
@ -45,14 +46,10 @@ impl<'a> Footer<'a> {
const STYLE_INSTRUCTION: &'static TextStyle = &theme::TEXT_SUB_GREY; const STYLE_INSTRUCTION: &'static TextStyle = &theme::TEXT_SUB_GREY;
const STYLE_DESCRIPTION: &'static TextStyle = &theme::TEXT_SUB_GREY_LIGHT; const STYLE_DESCRIPTION: &'static TextStyle = &theme::TEXT_SUB_GREY_LIGHT;
pub fn new<T: Into<TString<'a>>>( fn from_content(content: FooterContent<'a>) -> Self {
instruction: T,
description: Option<TString<'static>>,
) -> Self {
Self { Self {
area: Rect::zero(), area: Rect::zero(),
instruction: instruction.into(), content,
content: description.map(FooterContent::Description),
swipe_allow_down: false, swipe_allow_down: false,
swipe_allow_up: false, swipe_allow_up: false,
progress: 0, progress: 0,
@ -60,38 +57,79 @@ impl<'a> Footer<'a> {
} }
} }
pub fn with_page_counter(self, max_pages: u8) -> Self { pub fn new<T: Into<TString<'a>>>(
Self { instruction: T,
content: Some(FooterContent::PageCounter(PageCounter::new(max_pages))), description: Option<TString<'static>>,
..self ) -> Self {
} let instruction = instruction.into();
Self::from_content(
description
.map(|d| FooterContent::InstructionDescription(instruction, d))
.unwrap_or(FooterContent::Instruction(instruction)),
)
} }
pub fn update_instruction<T: Into<TString<'a>>>(&mut self, ctx: &mut EventCtx, s: T) { pub fn with_page_counter(max_pages: u8, instruction: TString<'static>) -> Self {
self.instruction = s.into(); Self::from_content(FooterContent::PageCounter(PageCounter::new(
max_pages,
instruction,
)))
}
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<T: Into<TString<'static>>>(&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!("not supported")
}
}
ctx.request_paint(); ctx.request_paint();
} }
pub fn update_description<T: Into<TString<'a>>>(&mut self, ctx: &mut EventCtx, s: T) { pub fn update_description<T: Into<TString<'a>>>(&mut self, ctx: &mut EventCtx, s: T) {
if let Some(ref mut content) = self.content { if let FooterContent::InstructionDescription(_i, d) = &mut self.content {
if let Some(text) = content.as_description_mut() { *d = s.into();
*text = s.into(); ctx.request_paint();
ctx.request_paint(); } else {
} else { #[cfg(feature = "ui_debug")]
#[cfg(feature = "ui_debug")] panic!("footer does not have description")
panic!("footer does not have description")
}
} }
} }
pub fn update_page_counter(&mut self, ctx: &mut EventCtx, n: u8) { pub fn update_page_counter(&mut self, ctx: &mut EventCtx, current: usize, max: Option<usize>) {
if let Some(ref mut content) = self.content { match &mut self.content {
if let Some(counter) = content.as_page_counter_mut() { FooterContent::PageCounter(counter) => {
counter.update_current_page(n); counter.update_current_page(current);
self.swipe_allow_down = counter.is_first_page(); self.swipe_allow_down = counter.is_first_page();
self.swipe_allow_up = counter.is_last_page(); self.swipe_allow_up = counter.is_last_page();
ctx.request_paint(); 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")] #[cfg(feature = "ui_debug")]
panic!("footer does not have counter") panic!("footer does not have counter")
} }
@ -99,11 +137,7 @@ impl<'a> Footer<'a> {
} }
pub fn height(&self) -> i16 { pub fn height(&self) -> i16 {
if self.content.is_some() { self.content.height()
Footer::HEIGHT_DEFAULT
} else {
Footer::HEIGHT_SIMPLE
}
} }
pub fn with_swipe(self, swipe_direction: SwipeDirection) -> Self { pub fn with_swipe(self, swipe_direction: SwipeDirection) -> Self {
@ -125,15 +159,7 @@ impl<'a> Component for Footer<'a> {
type Msg = Never; type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let h = bounds.height(); assert!(bounds.height() == self.content.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);
}
}
}
self.area = bounds; self.area = bounds;
self.area self.area
} }
@ -189,49 +215,7 @@ impl<'a> Component for Footer<'a> {
}; };
target.with_origin(offset, &|target| { target.with_origin(offset, &|target| {
// show description/counter only if there is space for it self.content.render(self.area, target);
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);
});
shape::Bar::new(self.area) shape::Bar::new(self.area)
.with_alpha(mask) .with_alpha(mask)
.with_fg(Color::black()) .with_fg(Color::black())
@ -245,89 +229,138 @@ impl<'a> Component for Footer<'a> {
impl crate::trace::Trace for Footer<'_> { impl crate::trace::Trace for Footer<'_> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Footer"); t.component("Footer");
if let Some(content) = &self.content { match &self.content {
match content { FooterContent::Instruction(i) => {
FooterContent::Description(text) => t.string("description", *text), t.string("instruction", *i);
FooterContent::PageCounter(counter) => counter.trace(t), }
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> { impl<'a> FooterContent<'a> {
pub fn as_description_mut(&mut self) -> Option<&mut TString<'a>> { fn height(&self) -> i16 {
match self { if matches!(self, FooterContent::Instruction(_)) {
FooterContent::Description(ref mut text) => Some(text), Footer::HEIGHT_SIMPLE
_ => None, } 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 { match self {
FooterContent::PageCounter(ref mut counter) => Some(counter), FooterContent::Instruction(instruction) => {
_ => None, 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 /// Helper component used within Footer instead of description for page count
/// indication, rendered e.g. as: '1 / 20'. /// indication, rendered e.g. as: '1 / 20'.
#[derive(Clone)] #[derive(Clone)]
struct PageCounter { struct PageCounter {
area: Rect, pub instruction: TString<'static>,
font: Font, font: Font,
page_curr: u8, page_curr: u8,
page_max: u8, page_max: u8,
} }
impl PageCounter { impl PageCounter {
fn new(page_max: u8) -> Self { fn new(page_max: u8, instruction: TString<'static>) -> Self {
Self { Self {
area: Rect::zero(), instruction,
page_curr: 1, page_curr: 0,
page_max, page_max,
font: Font::SUB, font: Font::SUB,
} }
} }
fn update_current_page(&mut self, new_value: u8) { fn update_current_page(&mut self, new_value: usize) {
self.page_curr = new_value.clamp(1, self.page_max); self.page_curr = (new_value as u8).clamp(0, self.page_max.saturating_sub(1));
} }
fn is_first_page(&self) -> bool { fn is_first_page(&self) -> bool {
self.page_curr == 1 self.page_curr == 0
} }
fn is_last_page(&self) -> bool { fn is_last_page(&self) -> bool {
self.page_curr == self.page_max self.page_curr + 1 == self.page_max
} }
} }
impl Component for PageCounter { impl PageCounter {
type Msg = Never; fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
let color = if self.is_last_page() {
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
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 {
theme::GREEN_LIGHT theme::GREEN_LIGHT
} else { } else {
theme::GREY_LIGHT 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); let string_max = uformat!("{}", self.page_max);
// center the whole counter "x / yz" // 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_num_max = self.font.text_width(&string_max);
let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x.x; let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x.x;
let center_x = self.area.center().x; let counter_area = area.split_top(Footer::HEIGHT_SIMPLE).0;
let counter_y = self.font.vert_center(self.area.y0, self.area.y1, "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_start_x = center_x - width_total / 2;
let counter_end_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); let base_num_curr = Point::new(counter_start_x, counter_y);
@ -359,6 +393,8 @@ impl Component for PageCounter {
.with_fg(color) .with_fg(color)
.with_font(self.font) .with_font(self.font)
.render(target); .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()); 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<usize>) {
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
}
}
}

View File

@ -196,7 +196,24 @@ where
#[inline(never)] #[inline(never)]
pub fn with_footer_counter(mut self, instruction: TString<'static>, max_value: u8) -> Self { 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 self
} }
@ -231,9 +248,14 @@ where
res 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<usize>,
) {
if let Some(footer) = &mut self.footer { if let Some(footer) = &mut self.footer {
footer.update_page_counter(ctx, new_val); footer.update_page_counter(ctx, current, max);
} }
} }

View File

@ -4,6 +4,8 @@ mod address_details;
mod binary_selection; mod binary_selection;
pub mod bl_confirm; pub mod bl_confirm;
mod button; mod button;
#[cfg(feature = "universal_fw")]
mod choose_credential;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
mod coinjoin_progress; mod coinjoin_progress;
mod fido; mod fido;
@ -46,6 +48,8 @@ pub use address_details::AddressDetails;
#[cfg(feature = "ui_overlay")] #[cfg(feature = "ui_overlay")]
pub use binary_selection::{BinarySelection, BinarySelectionMsg}; pub use binary_selection::{BinarySelection, BinarySelectionMsg};
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
#[cfg(feature = "universal_fw")]
pub use choose_credential::ChooseCredential;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
pub use coinjoin_progress::CoinJoinProgress; pub use coinjoin_progress::CoinJoinProgress;
pub use error::ErrorScreen; pub use error::ErrorScreen;

View File

@ -84,9 +84,9 @@ impl<'a> Component for ShareWords<'a> {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
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 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); let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated);
self.frame self.frame
.update_subtitle(ctx, updated_subtitle, Some(theme::TEXT_SUB_GREEN_LIME)); .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)); .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) self.frame.event(ctx, event)
} }

View File

@ -7,8 +7,7 @@ use crate::{
event::SwipeEvent, event::SwipeEvent,
geometry::{Offset, Rect}, geometry::{Offset, Rect},
lerp::Lerp, lerp::Lerp,
shape, shape::{self, Renderer},
shape::Renderer,
util::animation_disabled, util::animation_disabled,
}, },
}; };

View File

@ -1,6 +1,6 @@
use heapless::Vec; use heapless::Vec;
use super::theme; use super::{theme, InternallySwipable};
use crate::{ use crate::{
strutil::TString, strutil::TString,
time::{Duration, Stopwatch}, time::{Duration, Stopwatch},
@ -266,7 +266,7 @@ impl Component for VerticalMenu {
let opacities = [item1_opacity, item2_opacity, item3_opacity]; let opacities = [item1_opacity, item2_opacity, item3_opacity];
target.with_origin(offset, &|target| { target.with_translate(offset, &|target| {
// render buttons separated by thin bars // render buttons separated by thin bars
for (i, button) in (&self.buttons).into_iter().take(self.n_items).enumerate() { for (i, button) in (&self.buttons).into_iter().take(self.n_items).enumerate() {
button.render(target); button.render(target);

View File

@ -11,16 +11,16 @@ use crate::{
}, },
flow::{ flow::{
base::{DecisionBuilder as _, StateChange}, base::{DecisionBuilder as _, StateChange},
FlowMsg, FlowState, SwipeFlow, SwipePage, FlowMsg, FlowState, SwipeFlow,
}, },
layout::obj::LayoutObj, layout::obj::LayoutObj,
model_mercury::component::{FidoCredential, SwipeContent},
}, },
}; };
use super::super::{ use super::super::{
component::{ component::{
Frame, FrameMsg, PagedVerticalMenu, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg, ChooseCredential, FidoCredential, Frame, FrameMsg, PromptMsg, PromptScreen, SwipeContent,
VerticalMenu, VerticalMenuChoiceMsg,
}, },
theme, theme,
}; };
@ -49,6 +49,7 @@ impl FlowState for ConfirmFido {
match (self, direction) { match (self, direction) {
(Self::Intro, SwipeDirection::Left) => Self::Menu.swipe(direction), (Self::Intro, SwipeDirection::Left) => Self::Menu.swipe(direction),
(Self::Intro, SwipeDirection::Up) => Self::ChooseCredential.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::Details, SwipeDirection::Up) => Self::Tap.swipe(direction),
(Self::Tap, SwipeDirection::Down) => Self::Details.swipe(direction), (Self::Tap, SwipeDirection::Down) => Self::Details.swipe(direction),
_ => self.do_nothing(), _ => self.do_nothing(),
@ -122,26 +123,11 @@ impl ConfirmFido {
.try_into() .try_into()
.unwrap_or_else(|_| TString::from_str("-")) .unwrap_or_else(|_| TString::from_str("-"))
}; };
let content_choose_credential = Frame::left_aligned( let content_choose_credential =
TR::fido__title_select_credential.into(), ChooseCredential::new(label_fn, num_accounts).map(|msg| match msg {
SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new( FrameMsg::Button(_) => Some(FlowMsg::Info),
num_accounts, FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
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 get_account = move || { let get_account = move || {
let current = CRED_SELECTED.load(Ordering::Relaxed); let current = CRED_SELECTED.load(Ordering::Relaxed);

View File

@ -51,6 +51,13 @@ pub trait Renderer<'a> {
inner(self); inner(self);
self.set_viewport(original); 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);
}
} }
// ========================================================================== // ==========================================================================

View File

@ -18,6 +18,7 @@ async def list_resident_credentials(
TR.fido__title_list_credentials, TR.fido__title_list_credentials,
description=TR.fido__export_credentials, description=TR.fido__export_credentials,
verb=TR.buttons__export, verb=TR.buttons__export,
prompt_screen=True,
) )
creds = [ creds = [
WebAuthnCredential( WebAuthnCredential(