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:
parent
4d193381b4
commit
1255c3ae2d
207
core/embed/rust/src/ui/model_lincoln/component/header.rs
Normal file
207
core/embed/rust/src/ui/model_lincoln/component/header.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
mod button;
|
mod button;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod header;
|
||||||
mod result;
|
mod result;
|
||||||
mod welcome_screen;
|
mod welcome_screen;
|
||||||
|
|
||||||
pub use button::{ButtonStyle, ButtonStyleSheet};
|
pub use button::{ButtonStyle, ButtonStyleSheet};
|
||||||
pub use error::ErrorScreen;
|
pub use error::ErrorScreen;
|
||||||
|
pub use header::Header;
|
||||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||||
pub use welcome_screen::WelcomeScreen;
|
pub use welcome_screen::WelcomeScreen;
|
||||||
|
|
||||||
|
@ -59,6 +59,10 @@ include_icon!(ICON_WARNING40, "model_lincoln/res/warning40.toif");
|
|||||||
// TODO: text styles
|
// TODO: text styles
|
||||||
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, GREY_LIGHT, GREY_LIGHT);
|
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
|
// TODO: button styles
|
||||||
pub const fn button_default() -> ButtonStyleSheet {
|
pub const fn button_default() -> ButtonStyleSheet {
|
||||||
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_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;
|
||||||
|
@ -13,7 +13,7 @@ use crate::{
|
|||||||
event::SwipeEvent,
|
event::SwipeEvent,
|
||||||
geometry::{Alignment, Direction, Insets, Point, Rect},
|
geometry::{Alignment, Direction, Insets, Point, Rect},
|
||||||
lerp::Lerp,
|
lerp::Lerp,
|
||||||
model_mercury::theme::TITLE_HEIGHT,
|
model_mercury::theme::HEADER_HEIGHT,
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -361,7 +361,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));
|
||||||
|
@ -6,15 +6,14 @@ use crate::{
|
|||||||
display::{Color, Icon},
|
display::{Color, Icon},
|
||||||
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
||||||
lerp::Lerp,
|
lerp::Lerp,
|
||||||
model_mercury::{
|
|
||||||
component::{Button, ButtonMsg, ButtonStyleSheet},
|
|
||||||
theme::{self, TITLE_HEIGHT},
|
|
||||||
},
|
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::super::theme::{self, HEADER_HEIGHT};
|
||||||
|
use super::button::{Button, ButtonMsg, ButtonStyleSheet};
|
||||||
|
|
||||||
const ANIMATION_TIME_MS: u32 = 1000;
|
const ANIMATION_TIME_MS: u32 = 1000;
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@ -86,6 +85,7 @@ impl Header {
|
|||||||
button_msg: FlowMsg::Cancelled,
|
button_msg: FlowMsg::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;
|
||||||
@ -93,12 +93,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() {
|
||||||
@ -106,11 +108,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,
|
||||||
@ -143,8 +147,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));
|
||||||
@ -168,7 +172,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 {
|
||||||
|
@ -22,7 +22,7 @@ use crate::{
|
|||||||
component::Label,
|
component::Label,
|
||||||
constant::screen,
|
constant::screen,
|
||||||
geometry::{Alignment, Point},
|
geometry::{Alignment, Point},
|
||||||
model_mercury::theme::TITLE_HEIGHT,
|
model_mercury::theme::HEADER_HEIGHT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use pareen;
|
use pareen;
|
||||||
@ -208,7 +208,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,7 +777,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::SUB, GREY, BG, G
|
|||||||
/// 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;
|
||||||
|
Loading…
Reference in New Issue
Block a user