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

WIP: feat(lincoln): header component

This commit is contained in:
obrusvit 2024-12-31 13:18:46 +01:00
parent 40d9682117
commit 6da4eaabad
7 changed files with 264 additions and 7 deletions

View File

@ -0,0 +1,253 @@
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::super::theme::{self, HEADER_HEIGHT};
use super::button::{Button, ButtonMsg, ButtonStyleSheet};
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;
pub struct Header {
area: Rect,
title: Label<'static>,
subtitle: Option<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(),
subtitle: None,
button: None,
anim: None,
icon: None,
color: None,
title_style: theme::label_title_main(),
button_msg: FlowMsg::Cancelled,
}
}
#[inline(never)]
pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self {
let style = theme::TEXT_SUB_GREY;
self.title = self.title.top_aligned();
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() {
self.subtitle = Some(subtitle.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 update_subtitle(
&mut self,
ctx: &mut EventCtx,
new_subtitle: TString<'static>,
new_style: Option<TextStyle>,
) {
let style = new_style.unwrap_or(theme::TEXT_SUB_GREY);
match &mut self.subtitle {
Some(subtitle) => {
subtitle.set_style(style);
subtitle.set_text(new_subtitle);
}
None => {
self.subtitle = Some(Label::new(new_subtitle, self.title.alignment(), style));
}
}
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 {
let header_area = if let Some(b) = &mut self.button {
let (rest, button_area) = bounds.split_right(HEADER_HEIGHT);
b.place(button_area);
rest
} else {
bounds
};
if self.subtitle.is_some() {
let title_area = self.title.place(header_area);
let remaining = header_area.inset(Insets::top(title_area.height()));
let _subtitle_area = self.subtitle.place(remaining);
} else {
self.title.place(header_area);
}
self.area = bounds;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.subtitle.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);
self.subtitle.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(subtitle) = &self.subtitle {
t.child("subtitle", subtitle);
}
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

@ -86,6 +86,8 @@ pub const fn button_default() -> ButtonStyleSheet {
}
}
pub const HEADER_HEIGHT: i16 = 96;
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

@ -8,7 +8,7 @@ use crate::{
lerp::Lerp,
model_mercury::{
component::{Button, ButtonMsg, ButtonStyleSheet},
theme::{self, TITLE_HEIGHT},
theme::{self, HEADER_HEIGHT},
},
shape::{self, Renderer},
util::animation_disabled,
@ -168,7 +168,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;