1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-10 07:20:56 +00:00

WIP: feat(lincoln): header component

This commit is contained in:
obrusvit 2024-12-31 13:18:46 +01:00
parent 4d193381b4
commit 1255c3ae2d
7 changed files with 231 additions and 11 deletions

View File

@ -0,0 +1,207 @@
use crate::{
strutil::TString,
time::{Duration, Stopwatch},
ui::{
component::{text::TextStyle, Component, Event, EventCtx, FlowMsg, Label},
display::{Color, Icon},
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
lerp::Lerp,
shape::{self, Renderer},
util::animation_disabled,
},
};
use super::{
button::{Button, ButtonMsg, ButtonStyleSheet},
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. Lincoln UI shows the title (can be two lines), optional
/// icon on the left, and optional button (typically for menu) on the right. Color is shared for
/// title and icon.
pub struct Header {
area: Rect,
title: Label<'static>,
button: Option<Button>,
anim: Option<AttachAnimation>,
icon: Option<Icon>,
color: Option<Color>,
title_style: TextStyle,
button_msg: FlowMsg,
}
impl Header {
pub const fn new(alignment: Alignment, title: TString<'static>) -> Self {
Self {
area: Rect::zero(),
title: Label::new(title, alignment, theme::label_title_main()).vertically_centered(),
button: None,
anim: None,
icon: None,
color: None,
title_style: theme::label_title_main(),
button_msg: FlowMsg::Cancelled,
}
}
#[inline(never)]
pub fn styled(mut self, style: TextStyle) -> Self {
self.title_style = style;
self.title = self.title.styled(style);
self
}
#[inline(never)]
pub fn update_title(&mut self, ctx: &mut EventCtx, title: TString<'static>) {
self.title.set_text(title);
ctx.request_paint();
}
#[inline(never)]
pub fn with_button(mut self, icon: Icon, enabled: bool, msg: FlowMsg) -> Self {
let touch_area = Insets::uniform(BUTTON_EXPAND_BORDER);
self.button = Some(
Button::with_icon(icon)
.with_expanded_touch_area(touch_area)
.initially_enabled(enabled)
.styled(theme::button_default()),
);
self.button_msg = msg;
self
}
#[inline(never)]
pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self {
if self.button.is_some() {
self.button = Some(self.button.unwrap().styled(style));
}
self
}
#[inline(never)]
pub fn with_result_icon(mut self, icon: Icon, color: Color) -> Self {
self.anim = Some(AttachAnimation::default());
self.icon = Some(icon);
self.color = Some(color);
let mut title_style = self.title_style;
title_style.text_color = color;
self.styled(title_style)
}
}
impl Component for Header {
type Msg = FlowMsg;
fn place(&mut self, bounds: Rect) -> Rect {
debug_assert_eq!(bounds.height(), theme::HEADER_HEIGHT);
if let Some(b) = &mut self.button {
let (_, button_area) = bounds.split_right(theme::HEADER_BUTTON_WIDTH);
b.place(button_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.button.event(ctx, event) {
return Some(self.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()
};
self.button.render(target);
target.in_clip(self.area.split_left(offset.x).0, &|target| {
if let Some(icon) = self.icon {
let color = self.color.unwrap_or(theme::GREEN);
shape::ToifImage::new(self.title.area().left_center(), icon.toif)
.with_fg(color)
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
}
});
target.with_origin(offset, &|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.button {
t.child("button", button);
}
}
}

View File

@ -1,10 +1,12 @@
mod button;
mod error;
mod header;
mod result;
mod welcome_screen;
pub use button::{ButtonStyle, ButtonStyleSheet};
pub use error::ErrorScreen;
pub use header::Header;
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;

View File

@ -59,6 +59,10 @@ include_icon!(ICON_WARNING40, "model_lincoln/res/warning40.toif");
// TODO: text styles
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, GREY_LIGHT, GREY_LIGHT);
pub const fn label_title_main() -> TextStyle {
TEXT_NORMAL
}
// TODO: button styles
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
@ -86,6 +90,9 @@ pub const fn button_default() -> ButtonStyleSheet {
}
}
pub const HEADER_HEIGHT: i16 = 96;
pub const HEADER_BUTTON_WIDTH: i16 = 80;
pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171;
pub const RESULT_FOOTER_HEIGHT: i16 = 62;

View File

@ -13,7 +13,7 @@ use crate::{
event::SwipeEvent,
geometry::{Alignment, Direction, Insets, Point, Rect},
lerp::Lerp,
model_mercury::theme::TITLE_HEIGHT,
model_mercury::theme::HEADER_HEIGHT,
shape::{self, Renderer},
},
};
@ -361,7 +361,7 @@ fn frame_place(
bounds: Rect,
margin: usize,
) -> 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
.inset(Insets::top(theme::SPACING))
.inset(Insets::top(margin as i16));

View File

@ -6,15 +6,14 @@ use crate::{
display::{Color, Icon},
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
lerp::Lerp,
model_mercury::{
component::{Button, ButtonMsg, ButtonStyleSheet},
theme::{self, TITLE_HEIGHT},
},
shape::{self, Renderer},
util::animation_disabled,
},
};
use super::super::theme::{self, HEADER_HEIGHT};
use super::button::{Button, ButtonMsg, ButtonStyleSheet};
const ANIMATION_TIME_MS: u32 = 1000;
#[derive(Default, Clone)]
@ -86,6 +85,7 @@ impl Header {
button_msg: FlowMsg::Cancelled,
}
}
#[inline(never)]
pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self {
let style = theme::TEXT_SUB_GREY;
@ -93,12 +93,14 @@ impl Header {
self.subtitle = Some(Label::new(subtitle, self.title.alignment(), style));
self
}
#[inline(never)]
pub fn styled(mut self, style: TextStyle) -> Self {
self.title_style = style;
self.title = self.title.styled(style);
self
}
#[inline(never)]
pub fn subtitle_styled(mut self, style: TextStyle) -> Self {
if let Some(subtitle) = self.subtitle.take() {
@ -106,11 +108,13 @@ impl Header {
}
self
}
#[inline(never)]
pub fn update_title(&mut self, ctx: &mut EventCtx, title: TString<'static>) {
self.title.set_text(title);
ctx.request_paint();
}
#[inline(never)]
pub fn update_subtitle(
&mut self,
@ -143,8 +147,8 @@ impl Header {
self.button_msg = msg;
self
}
#[inline(never)]
#[inline(never)]
pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self {
if self.button.is_some() {
self.button = Some(self.button.unwrap().styled(style));
@ -168,7 +172,7 @@ impl Component for Header {
fn place(&mut self, bounds: Rect) -> Rect {
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);
rest
} else {

View File

@ -22,7 +22,7 @@ use crate::{
component::Label,
constant::screen,
geometry::{Alignment, Point},
model_mercury::theme::TITLE_HEIGHT,
model_mercury::theme::HEADER_HEIGHT,
},
};
use pareen;
@ -208,7 +208,7 @@ impl Component for HoldToConfirm {
Offset::uniform(80),
Alignment2D::CENTER,
));
self.title.place(screen().split_top(TITLE_HEIGHT).0);
self.title.place(screen().split_top(HEADER_HEIGHT).0);
bounds
}

View File

@ -777,7 +777,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::SUB, GREY, BG, G
/// the header. [px]
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 BUTTON_HEIGHT: i16 = 62;
pub const BUTTON_WIDTH: i16 = 78;