1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-28 15:22:14 +00:00

feat(eckhart): header component

This commit is contained in:
obrusvit 2025-01-15 15:44:08 +01:00
parent a3dcfacf77
commit 389940e2fb
7 changed files with 319 additions and 9 deletions

View File

@ -20,7 +20,7 @@ use crate::{
}, },
}; };
use super::super::theme::TITLE_HEIGHT; use super::super::theme::HEADER_HEIGHT;
#[derive(Clone)] #[derive(Clone)]
pub struct HorizontalSwipe { pub struct HorizontalSwipe {
@ -399,7 +399,7 @@ fn frame_place(
bounds: Rect, bounds: Rect,
margin: usize, margin: usize,
) -> Rect { ) -> Rect {
let (mut header_area, mut content_area) = bounds.split_top(TITLE_HEIGHT); let (mut header_area, mut content_area) = bounds.split_top(HEADER_HEIGHT);
content_area = content_area content_area = content_area
.inset(Insets::top(theme::SPACING)) .inset(Insets::top(theme::SPACING))
.inset(Insets::top(margin as i16)); .inset(Insets::top(margin as i16));

View File

@ -13,7 +13,7 @@ use crate::{
use super::super::{ use super::super::{
component::{Button, ButtonMsg, ButtonStyleSheet}, component::{Button, ButtonMsg, ButtonStyleSheet},
theme::{self, TITLE_HEIGHT}, theme::{self, HEADER_HEIGHT},
}; };
const ANIMATION_TIME_MS: u32 = 1000; const ANIMATION_TIME_MS: u32 = 1000;
@ -103,6 +103,7 @@ impl Header {
button_msg: HeaderMsg::Cancelled, button_msg: HeaderMsg::Cancelled,
} }
} }
#[inline(never)] #[inline(never)]
pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self { pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self {
let style = theme::TEXT_SUB_GREY; let style = theme::TEXT_SUB_GREY;
@ -110,12 +111,14 @@ impl Header {
self.subtitle = Some(Label::new(subtitle, self.title.alignment(), style)); self.subtitle = Some(Label::new(subtitle, self.title.alignment(), style));
self self
} }
#[inline(never)] #[inline(never)]
pub fn styled(mut self, style: TextStyle) -> Self { pub fn styled(mut self, style: TextStyle) -> Self {
self.title_style = style; self.title_style = style;
self.title = self.title.styled(style); self.title = self.title.styled(style);
self self
} }
#[inline(never)] #[inline(never)]
pub fn subtitle_styled(mut self, style: TextStyle) -> Self { pub fn subtitle_styled(mut self, style: TextStyle) -> Self {
if let Some(subtitle) = self.subtitle.take() { if let Some(subtitle) = self.subtitle.take() {
@ -123,11 +126,13 @@ impl Header {
} }
self self
} }
#[inline(never)] #[inline(never)]
pub fn update_title(&mut self, ctx: &mut EventCtx, title: TString<'static>) { pub fn update_title(&mut self, ctx: &mut EventCtx, title: TString<'static>) {
self.title.set_text(title); self.title.set_text(title);
ctx.request_paint(); ctx.request_paint();
} }
#[inline(never)] #[inline(never)]
pub fn update_subtitle( pub fn update_subtitle(
&mut self, &mut self,
@ -160,8 +165,8 @@ impl Header {
self.button_msg = msg; self.button_msg = msg;
self self
} }
#[inline(never)]
#[inline(never)]
pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self { pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self {
if self.button.is_some() { if self.button.is_some() {
self.button = Some(self.button.unwrap().styled(style)); self.button = Some(self.button.unwrap().styled(style));
@ -185,7 +190,7 @@ impl Component for Header {
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let header_area = if let Some(b) = &mut self.button { let header_area = if let Some(b) = &mut self.button {
let (rest, button_area) = bounds.split_right(TITLE_HEIGHT); let (rest, button_area) = bounds.split_right(HEADER_HEIGHT);
b.place(button_area); b.place(button_area);
rest rest
} else { } else {

View File

@ -13,7 +13,7 @@ use crate::{
}; };
use super::{ use super::{
theme::{self, TITLE_HEIGHT}, theme::{self, HEADER_HEIGHT},
Button, ButtonContent, ButtonMsg, Button, ButtonContent, ButtonMsg,
}; };
@ -210,7 +210,7 @@ impl Component for HoldToConfirm {
Offset::uniform(80), Offset::uniform(80),
Alignment2D::CENTER, Alignment2D::CENTER,
)); ));
self.title.place(screen().split_top(TITLE_HEIGHT).0); self.title.place(screen().split_top(HEADER_HEIGHT).0);
bounds bounds
} }

View File

@ -795,7 +795,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(fonts::FONT_SUB, GREY,
/// the header. [px] /// the header. [px]
pub const SPACING: i16 = 2; pub const SPACING: i16 = 2;
pub const TITLE_HEIGHT: i16 = 42; pub const HEADER_HEIGHT: i16 = 42;
pub const CONTENT_BORDER: i16 = 0; pub const CONTENT_BORDER: i16 = 0;
pub const BUTTON_HEIGHT: i16 = 62; pub const BUTTON_HEIGHT: i16 = 62;
pub const BUTTON_WIDTH: i16 = 78; pub const BUTTON_WIDTH: i16 = 78;

View File

@ -0,0 +1,272 @@
use crate::{
strutil::TString,
time::{Duration, Stopwatch},
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Label},
display::{Color, Icon},
geometry::{Alignment2D, Insets, Offset, Rect},
layout_eckhart::constant,
lerp::Lerp,
shape::{self, Renderer},
util::animation_disabled,
},
};
use super::{
button::{Button, ButtonContent, ButtonMsg},
theme,
};
const ANIMATION_TIME_MS: u32 = 1000;
#[derive(Default, Clone)]
struct AttachAnimation {
pub timer: Stopwatch,
}
impl AttachAnimation {
pub fn is_active(&self) -> bool {
if animation_disabled() {
return false;
}
self.timer
.is_running_within(Duration::from_millis(ANIMATION_TIME_MS))
}
pub fn eval(&self) -> f32 {
if animation_disabled() {
return ANIMATION_TIME_MS as f32 / 1000.0;
}
self.timer.elapsed().to_millis() as f32 / 1000.0
}
pub fn get_title_offset(&self, t: f32) -> i16 {
let fnc = pareen::constant(0.0).seq_ease_in_out(
0.8,
easer::functions::Cubic,
0.2,
pareen::constant(1.0),
);
i16::lerp(0, 25, fnc.eval(t))
}
pub fn start(&mut self) {
self.timer.start();
}
pub fn reset(&mut self) {
self.timer = Stopwatch::new_stopped();
}
}
const BUTTON_EXPAND_BORDER: i16 = 32;
/// Component for the header of a screen. Eckhart UI shows the title (can be two
/// lines), optional icon button on the left, and optional icon button
/// (typically for menu) on the right.
pub struct Header {
area: Rect,
title: Label<'static>,
title_style: TextStyle,
/// button in the top-right corner
right_button: Option<Button>,
/// button in the top-left corner
left_button: Option<Button>,
right_button_msg: HeaderMsg,
left_button_msg: HeaderMsg,
/// icon in the top-left corner (used instead of left button)
icon: Option<Icon>,
icon_color: Option<Color>,
/// animation
anim: Option<AttachAnimation>,
}
#[derive(Copy, Clone)]
pub enum HeaderMsg {
Cancelled,
Menu,
}
impl Header {
pub const HEADER_HEIGHT: i16 = 96; // [px]
pub const HEADER_BUTTON_WIDTH: i16 = 56; // [px]
const HEADER_INSETS: Insets = Insets::sides(24); // [px]
pub const fn new(title: TString<'static>) -> Self {
Self {
area: Rect::zero(),
title: Label::left_aligned(title, theme::label_title_main()).vertically_centered(),
title_style: theme::label_title_main(),
right_button: None,
left_button: None,
right_button_msg: HeaderMsg::Cancelled,
left_button_msg: HeaderMsg::Cancelled,
icon: None,
icon_color: None,
anim: None,
}
}
#[inline(never)]
pub fn with_text_style(mut self, style: TextStyle) -> Self {
self.title_style = style;
self.title = self.title.styled(style);
self
}
#[inline(never)]
pub fn with_right_button(self, button: Button, msg: HeaderMsg) -> Self {
debug_assert!(matches!(button.content(), ButtonContent::Icon(_)));
let touch_area = Insets::uniform(BUTTON_EXPAND_BORDER);
Self {
right_button: Some(button.with_expanded_touch_area(touch_area)),
right_button_msg: msg,
..self
}
}
#[inline(never)]
pub fn with_left_button(self, button: Button, msg: HeaderMsg) -> Self {
debug_assert!(matches!(button.content(), ButtonContent::Icon(_)));
let touch_area = Insets::uniform(BUTTON_EXPAND_BORDER);
Self {
icon: None,
left_button: Some(button.with_expanded_touch_area(touch_area)),
left_button_msg: msg,
..self
}
}
#[inline(never)]
pub fn with_menu_button(self) -> Self {
self.with_right_button(
Button::with_icon(theme::ICON_MENU).styled(theme::button_header()),
HeaderMsg::Menu,
)
}
#[inline(never)]
pub fn with_close_button(self) -> Self {
self.with_right_button(
Button::with_icon(theme::ICON_CLOSE).styled(theme::button_header()),
HeaderMsg::Cancelled,
)
}
#[inline(never)]
pub fn with_icon(self, icon: Icon, color: Color) -> Self {
Self {
left_button: None,
icon: Some(icon),
icon_color: Some(color),
..self
}
}
#[inline(never)]
pub fn update_title(&mut self, ctx: &mut EventCtx, title: TString<'static>) {
self.title.set_text(title);
ctx.request_paint();
}
/// Calculates the width needed for the left icon, be it a button with icon or just icon
fn left_icon_width(&self) -> i16 {
let margin_right: i16 = 16; // [px]
if let Some(b) = &self.left_button {
match b.content() {
ButtonContent::Icon(icon) => icon.toif.width() + margin_right,
_ => 0,
}
} else if let Some(icon) = self.icon {
icon.toif.width() + margin_right
} else {
0
}
}
}
impl Component for Header {
type Msg = HeaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
debug_assert_eq!(bounds.width(), constant::screen().width());
debug_assert_eq!(bounds.height(), Self::HEADER_HEIGHT);
let bounds = bounds.inset(Self::HEADER_INSETS);
let rest = if let Some(b) = &mut self.right_button {
let (rest, button_area) = bounds.split_right(Self::HEADER_BUTTON_WIDTH);
b.place(button_area);
rest
} else {
bounds
};
let icon_width = self.left_icon_width();
let (rest, title_area) = rest.split_left(icon_width);
self.left_button.place(rest);
self.title.place(title_area);
self.area = bounds;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
if let Some(anim) = &mut self.anim {
if let Event::Attach(_) = event {
anim.start();
ctx.request_paint();
ctx.request_anim_frame();
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if anim.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
}
}
}
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
return Some(self.left_button_msg.clone());
};
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
return Some(self.right_button_msg.clone());
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let offset = if let Some(anim) = &self.anim {
Offset::x(anim.get_title_offset(anim.eval()))
} else {
Offset::zero()
};
// TODO: correct animation
target.with_origin(offset, &|target| {
self.right_button.render(target);
self.left_button.render(target);
if let Some(icon) = self.icon {
shape::ToifImage::new(self.area.left_center(), icon.toif)
.with_fg(self.icon_color.unwrap_or(theme::GREY_LIGHT))
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
}
self.title.render(target);
});
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Header {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Header");
t.child("title", &self.title);
if let Some(button) = &self.right_button {
t.child("button", button);
}
}
}

View File

@ -1,11 +1,13 @@
pub mod bl_confirm; pub mod bl_confirm;
mod button; mod button;
mod error; mod error;
mod header;
mod result; mod result;
mod welcome_screen; mod welcome_screen;
pub use button::{Button, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText}; pub use button::{Button, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
pub use error::ErrorScreen; pub use error::ErrorScreen;
pub use header::{Header, HeaderMsg};
pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen; pub use welcome_screen::WelcomeScreen;

View File

@ -136,7 +136,11 @@ pub const TEXT_MONO_LIGHT: TextStyle = TextStyle::new(
GREY_EXTRA_LIGHT, GREY_EXTRA_LIGHT,
); );
// TODO: button styles pub const fn label_title_main() -> TextStyle {
TEXT_SMALL
}
// Button styles
pub const fn button_default() -> ButtonStyleSheet { pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet { ButtonStyleSheet {
normal: &ButtonStyle { normal: &ButtonStyle {
@ -164,6 +168,33 @@ pub const fn button_default() -> ButtonStyleSheet {
} }
pub const fn button_header() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: BG,
icon_color: GREY_LIGHT,
background_color: BG,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: GREY_SUPER_DARK,
icon_color: GREY_LIGHT,
background_color: BG,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: BG,
icon_color: GREY_LIGHT,
background_color: BG,
},
}
}
// Result constants
pub const RESULT_PADDING: i16 = 6; pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171; pub const RESULT_FOOTER_START: i16 = 171;
pub const RESULT_FOOTER_HEIGHT: i16 = 62; pub const RESULT_FOOTER_HEIGHT: i16 = 62;