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:
parent
2572705d1f
commit
7db1529533
@ -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> {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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,29 +229,93 @@ 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::InstructionDescription(i, d) => {
|
||||||
|
t.string("description", *d);
|
||||||
|
t.string("instruction", *i);
|
||||||
|
}
|
||||||
FooterContent::PageCounter(counter) => counter.trace(t),
|
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)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,59 +323,44 @@ impl<'a> FooterContent<'a> {
|
|||||||
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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,23 +123,8 @@ 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(
|
|
||||||
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::Button(_) => Some(FlowMsg::Info),
|
||||||
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user