1
0
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:
obrusvit 2025-03-29 13:35:00 +01:00 committed by M1nd3r
parent 4a92d02015
commit 6d7fe79e57
3 changed files with 218 additions and 129 deletions

View File

@ -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
}

View File

@ -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);
}
}
}

View File

@ -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,
},
}