mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-23 02:29:10 +00:00
feat(eckhart): improve pagination of ActionBar
- Single mode now handles pagination with the right_button shown at the last page - new PaginateOnly mode to only render navigation buttons without cancel/confirm buttons
This commit is contained in:
parent
4a92d02015
commit
6d7fe79e57
@ -171,6 +171,10 @@ impl Button {
|
||||
self.touch_expand = Some(expand);
|
||||
}
|
||||
|
||||
pub fn set_content_offset(&mut self, offset: Offset) {
|
||||
self.content_offset = offset;
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &ButtonContent {
|
||||
&self.content
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::component::{Button, ButtonContent, ButtonMsg, ButtonStyleSheet},
|
||||
super::component::{Button, ButtonMsg},
|
||||
theme, HoldToConfirmAnim,
|
||||
};
|
||||
|
||||
@ -17,20 +17,22 @@ use super::{
|
||||
pub struct ActionBar {
|
||||
/// Behavior based on `Mode`
|
||||
mode: Mode,
|
||||
/// Right or single button, can have text or icon
|
||||
right_button: Button,
|
||||
/// Optional left button, can be shorter than the right one
|
||||
/// Right or single confirm button, can have text or icon
|
||||
right_button: Option<Button>,
|
||||
/// Left cancel button, can be shorter than the right one
|
||||
left_button: Option<Button>,
|
||||
/// Area of the action bar
|
||||
area: Rect,
|
||||
/// Whether the left button is short (default: true)
|
||||
left_short: bool,
|
||||
// Storage of original button content for paginated component
|
||||
left_original: Option<(ButtonContent, ButtonStyleSheet)>,
|
||||
right_original: Option<(ButtonContent, ButtonStyleSheet)>,
|
||||
/// Hold to confirm animation
|
||||
htc_anim: Option<HoldToConfirmAnim>,
|
||||
/// Timeout
|
||||
timeout: Option<Timeout>,
|
||||
/// Pager for paginated content
|
||||
pager: Pager,
|
||||
/// Left button for paginated content
|
||||
prev_button: Button,
|
||||
/// Right button for paginated content
|
||||
next_button: Button,
|
||||
}
|
||||
|
||||
pub enum ActionBarMsg {
|
||||
@ -45,13 +47,16 @@ pub enum ActionBarMsg {
|
||||
}
|
||||
|
||||
/// Describes the behavior of the action bar
|
||||
#[derive(PartialEq)]
|
||||
enum Mode {
|
||||
/// Single confirm button taking full width
|
||||
/// Single confirm button
|
||||
Single,
|
||||
/// Cancel and confirm button; Up/Down navigation for paginated content
|
||||
Double { pager: Pager },
|
||||
/// Cancel and confirm button
|
||||
Double { left_short: bool },
|
||||
/// Automatic confirmation after a timeout
|
||||
Timeout,
|
||||
/// Only show pagination buttons, no confirm or cancel
|
||||
PaginateOnly,
|
||||
}
|
||||
|
||||
impl ActionBar {
|
||||
@ -62,16 +67,14 @@ impl ActionBar {
|
||||
const BUTTON_CONTENT_OFFSET: Offset = Offset::x(12); // [px]
|
||||
const BUTTON_EXPAND_TOUCH: Insets = Insets::top(Self::ACTION_BAR_HEIGHT);
|
||||
|
||||
const PAGINATE_LEFT_CONTENT: ButtonContent = ButtonContent::Icon(theme::ICON_CHEVRON_UP);
|
||||
const PAGINATE_RIGHT_CONTENT: ButtonContent = ButtonContent::Icon(theme::ICON_CHEVRON_DOWN);
|
||||
const PAGINATE_STYLESHEET: &'static ButtonStyleSheet = &theme::button_default();
|
||||
|
||||
/// Create action bar with single button confirming the layout
|
||||
/// Create action bar with single button confirming the layout. The
|
||||
/// component automatically shows navigation up/down buttons for
|
||||
/// paginated content.
|
||||
pub fn new_single(button: Button) -> Self {
|
||||
Self::new(
|
||||
Mode::Single,
|
||||
None,
|
||||
button.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH),
|
||||
Some(button.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
@ -81,7 +84,7 @@ impl ActionBar {
|
||||
Self::new(
|
||||
Mode::Timeout,
|
||||
None,
|
||||
button.initially_enabled(false),
|
||||
Some(button.initially_enabled(false)),
|
||||
Some(Timeout::new(timeout_ms)),
|
||||
)
|
||||
}
|
||||
@ -91,16 +94,16 @@ impl ActionBar {
|
||||
/// content.
|
||||
pub fn new_double(left: Button, right: Button) -> Self {
|
||||
Self::new(
|
||||
Mode::Double {
|
||||
pager: Pager::single_page(),
|
||||
},
|
||||
Mode::Double { left_short: true },
|
||||
Some(
|
||||
left.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)
|
||||
.with_content_offset(Self::BUTTON_CONTENT_OFFSET),
|
||||
),
|
||||
right
|
||||
.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)
|
||||
.with_content_offset(Self::BUTTON_CONTENT_OFFSET.neg()),
|
||||
Some(
|
||||
right
|
||||
.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)
|
||||
.with_content_offset(Self::BUTTON_CONTENT_OFFSET.neg()),
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
@ -112,8 +115,17 @@ impl ActionBar {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_left_short(mut self, left_short: bool) -> Self {
|
||||
self.left_short = left_short;
|
||||
/// Create action bar with only pagination buttons. The component in this
|
||||
/// mode can only return `ActionBarMsg::Prev` and `ActionBarMsg::Next`
|
||||
/// messages.
|
||||
pub fn new_paginate_only() -> Self {
|
||||
Self::new(Mode::PaginateOnly, None, None, None)
|
||||
}
|
||||
|
||||
pub fn with_left_short(mut self) -> Self {
|
||||
if let Mode::Double { ref mut left_short } = self.mode {
|
||||
*left_short = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@ -121,102 +133,82 @@ impl ActionBar {
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
btn.set_expanded_touch_area(expand);
|
||||
}
|
||||
self.right_button.set_expanded_touch_area(expand);
|
||||
if let Some(btn) = &mut self.right_button {
|
||||
btn.set_expanded_touch_area(expand);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touch_area(&self) -> Rect {
|
||||
let right_area = self.right_button.touch_area();
|
||||
let right_area = self
|
||||
.right_button
|
||||
.as_ref()
|
||||
.map_or(Rect::zero(), |right| right.touch_area());
|
||||
self.left_button
|
||||
.as_ref()
|
||||
.map_or(right_area, |left| right_area.union(left.touch_area()))
|
||||
}
|
||||
|
||||
/// Updates the pager of the component. This is used to show and process the
|
||||
/// navigation buttons.
|
||||
pub fn update(&mut self, new_pager: Pager) {
|
||||
// TODO: review `clone()` of `left_content`/`right_content`
|
||||
if let Mode::Double { pager } = &mut self.mode {
|
||||
let old_is_last = pager.is_last();
|
||||
let new_is_last = new_pager.is_last();
|
||||
*pager = new_pager;
|
||||
// Update left button - show original content/style only on first page
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
if pager.is_first() {
|
||||
let (content, style) = unwrap!(self.left_original.clone());
|
||||
btn.set_content(content);
|
||||
btn.set_stylesheet(style);
|
||||
} else {
|
||||
btn.set_content(Self::PAGINATE_LEFT_CONTENT);
|
||||
btn.set_stylesheet(*Self::PAGINATE_STYLESHEET);
|
||||
}
|
||||
}
|
||||
let old_is_last = self.pager.is_last();
|
||||
let new_is_last = new_pager.is_last();
|
||||
let old_is_first = self.pager.is_first();
|
||||
let new_is_first = new_pager.is_first();
|
||||
|
||||
// Update right button - show original content/style only on last page
|
||||
if pager.is_last() {
|
||||
let (content, style) = unwrap!(self.right_original.clone());
|
||||
self.right_button.set_content(content);
|
||||
self.right_button.set_stylesheet(style);
|
||||
} else {
|
||||
self.right_button.set_content(Self::PAGINATE_RIGHT_CONTENT);
|
||||
self.right_button.set_stylesheet(*Self::PAGINATE_STYLESHEET);
|
||||
}
|
||||
|
||||
// If we're entering or leaving the last page and left_short is true,
|
||||
// we need to update the button placement
|
||||
if self.left_short && (old_is_last != new_is_last) {
|
||||
self.place_buttons(self.area);
|
||||
}
|
||||
self.pager = new_pager;
|
||||
if (old_is_last != new_is_last) || (new_is_first != old_is_first) {
|
||||
self.place_buttons(self.area);
|
||||
}
|
||||
}
|
||||
|
||||
fn new(
|
||||
mode: Mode,
|
||||
left_button: Option<Button>,
|
||||
right_button: Button,
|
||||
right_button: Option<Button>,
|
||||
timeout: Option<Timeout>,
|
||||
) -> Self {
|
||||
let (left_original, right_original) = match mode {
|
||||
Mode::Double { .. } => (
|
||||
left_button
|
||||
.as_ref()
|
||||
.map(|b| (b.content().clone(), *b.style_sheet())),
|
||||
Some((right_button.content().clone(), *right_button.style_sheet())),
|
||||
),
|
||||
_ => (None, None),
|
||||
let htc_anim = if let Some(ref right_button) = right_button {
|
||||
right_button
|
||||
.long_press()
|
||||
.filter(|_| !animation_disabled())
|
||||
.map(|dur| {
|
||||
HoldToConfirmAnim::new()
|
||||
.with_duration(dur)
|
||||
.with_header_overlay(TR::instructions__continue_holding.into())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let htc_anim = right_button
|
||||
.long_press()
|
||||
.filter(|_| !animation_disabled())
|
||||
.map(|dur| {
|
||||
HoldToConfirmAnim::new()
|
||||
.with_duration(dur)
|
||||
.with_header_overlay(TR::instructions__continue_holding.into())
|
||||
});
|
||||
|
||||
Self {
|
||||
mode,
|
||||
right_button,
|
||||
left_button,
|
||||
area: Rect::zero(),
|
||||
left_short: true,
|
||||
left_original,
|
||||
right_original,
|
||||
htc_anim,
|
||||
timeout,
|
||||
pager: Pager::default(),
|
||||
prev_button: Button::with_icon(theme::ICON_CHEVRON_UP)
|
||||
.styled(theme::button_default())
|
||||
.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)
|
||||
.with_content_offset(Self::BUTTON_CONTENT_OFFSET),
|
||||
next_button: Button::with_icon(theme::ICON_CHEVRON_DOWN)
|
||||
.styled(theme::button_default())
|
||||
.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)
|
||||
.with_content_offset(Self::BUTTON_CONTENT_OFFSET.neg()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle event of the right button at the last page, this includes:
|
||||
/// - Single button mode
|
||||
/// - Double button mode at single page component
|
||||
/// - Double button mode at last page of paginated component
|
||||
/// Handle event of the `right_button` at the last page.
|
||||
///
|
||||
/// The function takes care about triggering the correct action to
|
||||
/// HoldToConfirm or returning the correct message out of the ActionBar.
|
||||
fn event_right_button_at_last_page(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
msg: ButtonMsg,
|
||||
) -> Option<ActionBarMsg> {
|
||||
let is_hold = self.right_button.long_press().is_some();
|
||||
fn event_right_button(&mut self, ctx: &mut EventCtx, msg: ButtonMsg) -> Option<ActionBarMsg> {
|
||||
let is_hold = self
|
||||
.right_button
|
||||
.as_ref()
|
||||
.is_some_and(|btn| btn.long_press().is_some());
|
||||
match (msg, is_hold) {
|
||||
(ButtonMsg::Pressed, true) => {
|
||||
if let Some(htc_anim) = &mut self.htc_anim {
|
||||
@ -255,22 +247,61 @@ impl ActionBar {
|
||||
|
||||
fn place_buttons(&mut self, bounds: Rect) {
|
||||
match &self.mode {
|
||||
Mode::Single | Mode::Timeout => {
|
||||
Mode::Timeout => {
|
||||
self.right_button.place(bounds);
|
||||
}
|
||||
Mode::Double { pager } => {
|
||||
let (left_area, right_area) = if self.left_short && pager.is_last() {
|
||||
// Small left button when on last page
|
||||
Mode::Single => {
|
||||
let (left_area, right_area) = if !self.pager.is_first() {
|
||||
self.next_button
|
||||
.set_content_offset(Self::BUTTON_CONTENT_OFFSET.neg());
|
||||
// Small `prev_button` when not on first page
|
||||
let (left, rest) = bounds.split_left(Self::LEFT_SMALL_BUTTON_WIDTH);
|
||||
let (_, right) = rest.split_left(Self::SPACER_WIDTH);
|
||||
(left, right)
|
||||
} else {
|
||||
// Standard equal-sized buttons
|
||||
self.next_button.set_content_offset(Offset::zero());
|
||||
(Rect::zero(), bounds)
|
||||
};
|
||||
self.right_button.place(right_area);
|
||||
self.prev_button.place(left_area);
|
||||
self.next_button.place(right_area);
|
||||
}
|
||||
Mode::Double { left_short } => {
|
||||
let (left_area, right_area) = if *left_short && self.pager.is_last() {
|
||||
// Small left button
|
||||
let (left, rest) = bounds.split_left(Self::LEFT_SMALL_BUTTON_WIDTH);
|
||||
let (_, right) = rest.split_left(Self::SPACER_WIDTH);
|
||||
(left, right)
|
||||
} else {
|
||||
// Equal-sized buttons
|
||||
let (left, _, right) = bounds.split_center(Self::SPACER_WIDTH);
|
||||
(left, right)
|
||||
};
|
||||
self.left_button.place(left_area);
|
||||
self.right_button.place(right_area);
|
||||
self.prev_button.place(left_area);
|
||||
self.next_button.place(right_area);
|
||||
}
|
||||
Mode::PaginateOnly => {
|
||||
let (left_area, right_area) = if self.pager.is_first() {
|
||||
// Only `next_button`
|
||||
self.next_button.set_content_offset(Offset::zero());
|
||||
(Rect::zero(), bounds)
|
||||
} else if self.pager.is_last() {
|
||||
// Only `prev_button`
|
||||
self.prev_button.set_content_offset(Offset::zero());
|
||||
(bounds, Rect::zero())
|
||||
} else {
|
||||
// Equal-sized `next_button` and `prev_button`
|
||||
let (left, _, right) = bounds.split_center(Self::SPACER_WIDTH);
|
||||
self.prev_button
|
||||
.set_content_offset(Self::BUTTON_CONTENT_OFFSET);
|
||||
self.next_button
|
||||
.set_content_offset(Self::BUTTON_CONTENT_OFFSET.neg());
|
||||
(left, right)
|
||||
};
|
||||
self.prev_button.place(left_area);
|
||||
self.next_button.place(right_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -300,42 +331,78 @@ impl Component for ActionBar {
|
||||
}
|
||||
}
|
||||
Mode::Single => {
|
||||
// Only handle confirm button
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.event_right_button_at_last_page(ctx, msg);
|
||||
}
|
||||
}
|
||||
Mode::Double { pager } => {
|
||||
if pager.is_single() {
|
||||
// Single page - show back and confirm
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if self.pager.is_single() {
|
||||
// Only handle confirm button
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.event_right_button_at_last_page(ctx, msg);
|
||||
return self.event_right_button(ctx, msg);
|
||||
}
|
||||
} else if pager.is_first() && !pager.is_single() {
|
||||
// First page of multiple - go back and next page
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
|
||||
} else if self.pager.is_first() && !self.pager.is_single() {
|
||||
// First page of multiple: next_button
|
||||
if let Some(ButtonMsg::Clicked) = self.next_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
} else if pager.is_last() && !pager.is_single() {
|
||||
// Last page - enable up button, show confirm
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
} else if !self.pager.is_last() && !self.pager.is_single() {
|
||||
// Middle pages: prev_button and next_button
|
||||
if let Some(ButtonMsg::Clicked) = self.prev_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.next_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
} else {
|
||||
// Last page: prev_button and right_button
|
||||
if let Some(ButtonMsg::Clicked) = self.prev_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.event_right_button_at_last_page(ctx, msg);
|
||||
return self.event_right_button(ctx, msg);
|
||||
}
|
||||
} else {
|
||||
// Middle pages - navigations up/down
|
||||
}
|
||||
}
|
||||
Mode::Double { .. } => {
|
||||
if self.pager.is_single() {
|
||||
// Single page: left_button and right_button
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.event_right_button(ctx, msg);
|
||||
}
|
||||
} else if self.pager.is_first() && !self.pager.is_single() {
|
||||
// First page of multiple: left_button and next_button
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.next_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
} else if !self.pager.is_last() && !self.pager.is_single() {
|
||||
// Middle pages: prev_button and next_button
|
||||
if let Some(ButtonMsg::Clicked) = self.prev_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
|
||||
if let Some(ButtonMsg::Clicked) = self.next_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
} else {
|
||||
// Last page: prev_button and right_button
|
||||
if let Some(ButtonMsg::Clicked) = self.prev_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.event_right_button(ctx, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::PaginateOnly => {
|
||||
// Only handle navigation, no confirm/cancel regardless of page
|
||||
if !self.pager.is_first() && !self.pager.is_single() {
|
||||
if let Some(ButtonMsg::Clicked) = self.prev_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
}
|
||||
if !self.pager.is_last() && !self.pager.is_single() {
|
||||
if let Some(ButtonMsg::Clicked) = self.next_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
}
|
||||
@ -345,16 +412,32 @@ impl Component for ActionBar {
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
if let Some(btn) = &self.left_button {
|
||||
let pos_divider = btn.area().right_center();
|
||||
let show_divider = match self.mode {
|
||||
Mode::Single => !self.pager.is_first(),
|
||||
Mode::Double { .. } => true,
|
||||
Mode::Timeout => false,
|
||||
Mode::PaginateOnly => !self.pager.is_first() && !self.pager.is_last(),
|
||||
};
|
||||
if show_divider {
|
||||
let pos_divider = self.prev_button.area().right_center();
|
||||
shape::ToifImage::new(pos_divider, theme::ICON_DASH_VERTICAL.toif)
|
||||
.with_align(Alignment2D::CENTER_LEFT)
|
||||
.with_fg(theme::GREY_EXTRA_DARK)
|
||||
.render(target);
|
||||
btn.render(target);
|
||||
}
|
||||
self.right_button.render(target);
|
||||
self.htc_anim.render(target);
|
||||
if self.pager.is_first() {
|
||||
self.left_button.render(target);
|
||||
} else {
|
||||
self.prev_button.render(target);
|
||||
}
|
||||
if self.pager.is_last() {
|
||||
self.right_button.render(target);
|
||||
} else {
|
||||
self.next_button.render(target);
|
||||
}
|
||||
if let Some(htc_anim) = &self.htc_anim {
|
||||
htc_anim.render(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,6 +448,8 @@ impl crate::trace::Trace for ActionBar {
|
||||
if let Some(btn) = &self.left_button {
|
||||
t.child("left_button", btn);
|
||||
}
|
||||
t.child("right_button", &self.right_button);
|
||||
if let Some(btn) = &self.right_button {
|
||||
t.child("right_button", btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,9 +155,9 @@ pub const fn button_default() -> ButtonStyleSheet {
|
||||
},
|
||||
disabled: &ButtonStyle {
|
||||
font: fonts::FONT_SATOSHI_MEDIUM_26,
|
||||
text_color: GREY_LIGHT,
|
||||
text_color: GREY,
|
||||
button_color: BG,
|
||||
icon_color: GREY_LIGHT,
|
||||
icon_color: GREY,
|
||||
background_color: BG,
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user