mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-06-25 09:22:33 +00:00
feat(eckhart): fuel gauge UI
- fuel gauge (battery state) with icons for charging/discharging state and soc percentage - on attach on Homescreen ActionBar - permanently on the first DeviceMenu subscreen - temporarily in the Header on charging state update [no changelog]
This commit is contained in:
parent
17d6dd4a55
commit
f0865e3b10
@ -179,7 +179,7 @@ test: ## run unit tests
|
|||||||
test_rust: ## run rs unit tests
|
test_rust: ## run rs unit tests
|
||||||
export BUILD_DIR=$(abspath $(UNIX_BUILD_DIR)) ; \
|
export BUILD_DIR=$(abspath $(UNIX_BUILD_DIR)) ; \
|
||||||
cd embed/rust ; cargo test $(TESTOPTS) --target=$(RUST_TARGET) \
|
cd embed/rust ; cargo test $(TESTOPTS) --target=$(RUST_TARGET) \
|
||||||
--no-default-features --features $(LAYOUT_FEATURE),test \
|
--no-default-features --features $(LAYOUT_FEATURE),power_manager,test \
|
||||||
-- --test-threads=1 --nocapture
|
-- --test-threads=1 --nocapture
|
||||||
|
|
||||||
test_emu: ## run selected device tests from python-trezor
|
test_emu: ## run selected device tests from python-trezor
|
||||||
|
@ -3,6 +3,7 @@ use super::ffi;
|
|||||||
#[cfg(feature = "ui")]
|
#[cfg(feature = "ui")]
|
||||||
use crate::ui::event::PMEvent;
|
use crate::ui::event::PMEvent;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||||
pub enum ChargingState {
|
pub enum ChargingState {
|
||||||
Discharging,
|
Discharging,
|
||||||
Charging,
|
Charging,
|
||||||
|
@ -424,7 +424,7 @@ impl Timer {
|
|||||||
self.token().0 == Timer::INVALID_TOKEN_VALUE
|
self.token().0 == Timer::INVALID_TOKEN_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn is_running(&self) -> bool {
|
pub const fn is_running(&self) -> bool {
|
||||||
self.0 & Timer::IS_RUNNING_BITMASK != 0
|
self.0 & Timer::IS_RUNNING_BITMASK != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::super::theme;
|
||||||
|
|
||||||
#[cfg(feature = "bootloader")]
|
#[cfg(feature = "bootloader")]
|
||||||
use super::super::fonts;
|
use super::super::fonts;
|
||||||
|
|
||||||
use super::super::theme;
|
|
||||||
|
|
||||||
pub enum ButtonMsg {
|
pub enum ButtonMsg {
|
||||||
Pressed,
|
Pressed,
|
||||||
Released,
|
Released,
|
||||||
@ -236,10 +236,8 @@ impl Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_content(&mut self, content: ButtonContent) {
|
pub fn set_content(&mut self, content: ButtonContent) {
|
||||||
if self.content != content {
|
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_expanded_touch_area(&mut self, expand: Insets) {
|
pub fn set_expanded_touch_area(&mut self, expand: Insets) {
|
||||||
self.touch_expand = expand;
|
self.touch_expand = expand;
|
||||||
@ -253,6 +251,10 @@ impl Button {
|
|||||||
&self.content
|
&self.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn content_mut(&mut self) -> &mut ButtonContent {
|
||||||
|
&mut self.content
|
||||||
|
}
|
||||||
|
|
||||||
pub fn content_offset(&self) -> Offset {
|
pub fn content_offset(&self) -> Offset {
|
||||||
self.content_offset
|
self.content_offset
|
||||||
}
|
}
|
||||||
@ -301,7 +303,7 @@ impl Button {
|
|||||||
text.map(|t| self.text_height(t, false, width) + self.baseline_subtext_height())
|
text.map(|t| self.text_height(t, false, width) + self.baseline_subtext_height())
|
||||||
}
|
}
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
ButtonContent::HomeBar(_) => theme::ACTION_BAR_HEIGHT,
|
ButtonContent::HomeBar(..) => theme::ACTION_BAR_HEIGHT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,7 +409,7 @@ impl Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_content<'s>(
|
fn render_content<'s>(
|
||||||
&self,
|
&'s self,
|
||||||
target: &mut impl Renderer<'s>,
|
target: &mut impl Renderer<'s>,
|
||||||
stylesheet: &ButtonStyle,
|
stylesheet: &ButtonStyle,
|
||||||
alpha: u8,
|
alpha: u8,
|
||||||
@ -558,7 +560,7 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
|
pub fn render_with_alpha<'s>(&'s self, target: &mut impl Renderer<'s>, alpha: u8) {
|
||||||
let style = self.style();
|
let style = self.style();
|
||||||
self.render_background(target, style, alpha);
|
self.render_background(target, style, alpha);
|
||||||
self.render_content(target, style, alpha);
|
self.render_content(target, style, alpha);
|
||||||
@ -690,7 +692,9 @@ impl crate::trace::Trace for Button {
|
|||||||
t.string("text", *text);
|
t.string("text", *text);
|
||||||
}
|
}
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
ButtonContent::HomeBar(text) => t.string("text", text.unwrap_or(TString::empty())),
|
ButtonContent::HomeBar(text) => {
|
||||||
|
t.string("text", text.unwrap_or(TString::empty()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,7 +715,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ButtonContent {
|
pub enum ButtonContent {
|
||||||
Empty,
|
Empty,
|
||||||
Text {
|
Text {
|
||||||
|
@ -177,6 +177,21 @@ impl ActionBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn left_button(&self) -> Option<&Button> {
|
||||||
|
self.left_button.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left_button_mut(&mut self) -> Option<&mut Button> {
|
||||||
|
self.left_button.as_mut()
|
||||||
|
}
|
||||||
|
pub fn right_button(&self) -> Option<&Button> {
|
||||||
|
self.right_button.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_button_mut(&mut self) -> Option<&mut Button> {
|
||||||
|
self.right_button.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
left_button: Option<Button>,
|
left_button: Option<Button>,
|
||||||
|
@ -14,19 +14,21 @@ use crate::{
|
|||||||
Component, Event, EventCtx,
|
Component, Event, EventCtx,
|
||||||
},
|
},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
layout_eckhart::{
|
|
||||||
component::{Button, ButtonStyleSheet},
|
|
||||||
constant::SCREEN,
|
|
||||||
firmware::{
|
|
||||||
Header, HeaderMsg, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
|
|
||||||
VerticalMenuScreenMsg, SHORT_MENU_ITEMS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{theme, ShortMenuVec};
|
use super::{
|
||||||
|
super::{
|
||||||
|
component::{Button, ButtonStyleSheet},
|
||||||
|
constant::SCREEN,
|
||||||
|
firmware::{
|
||||||
|
FuelGauge, Header, HeaderMsg, TextScreen, TextScreenMsg, VerticalMenu,
|
||||||
|
VerticalMenuScreen, VerticalMenuScreenMsg, SHORT_MENU_ITEMS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme, ShortMenuVec,
|
||||||
|
};
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
const MAX_DEPTH: usize = 3;
|
const MAX_DEPTH: usize = 3;
|
||||||
@ -37,10 +39,9 @@ const DISCONNECT_DEVICE_MENU_INDEX: usize = 1;
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Action {
|
enum Action {
|
||||||
// Go to another registered subscreen
|
/// Go to another registered subscreen
|
||||||
GoTo(usize),
|
GoTo(usize),
|
||||||
|
/// Return a DeviceMenuMsg to the caller
|
||||||
// Return a DeviceMenuMsg to the caller
|
|
||||||
Return(DeviceMenuMsg),
|
Return(DeviceMenuMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,7 +382,7 @@ impl<'a> DeviceMenuScreen<'a> {
|
|||||||
}
|
}
|
||||||
let mut header = Header::new(submenu.header_text).with_close_button();
|
let mut header = Header::new(submenu.header_text).with_close_button();
|
||||||
if submenu.show_battery {
|
if submenu.show_battery {
|
||||||
// TODO: add battery
|
header = header.with_fuel_gauge(Some(FuelGauge::always()));
|
||||||
} else {
|
} else {
|
||||||
header = header.with_left_button(
|
header = header.with_left_button(
|
||||||
Button::with_icon(theme::ICON_CHEVRON_LEFT),
|
Button::with_icon(theme::ICON_CHEVRON_LEFT),
|
||||||
|
220
core/embed/rust/src/ui/layout_eckhart/firmware/fuel_gauge.rs
Normal file
220
core/embed/rust/src/ui/layout_eckhart/firmware/fuel_gauge.rs
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
use crate::{
|
||||||
|
trezorhal::power_manager::{self, ChargingState},
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx, Never, Timer},
|
||||||
|
display::{Color, Font, Icon},
|
||||||
|
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||||
|
shape::{self, Renderer},
|
||||||
|
util::animation_disabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
fonts,
|
||||||
|
theme::{
|
||||||
|
firmware::FUEL_GAUGE_DURATION, GREY_LIGHT, ICON_BATTERY_EMPTY, ICON_BATTERY_FULL,
|
||||||
|
ICON_BATTERY_LOW, ICON_BATTERY_MID, ICON_BATTERY_ZAP, RED, YELLOW,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Component for showing a small fuel gauge (battery status) consisting of:
|
||||||
|
/// - icon indicating charging or discharging state
|
||||||
|
/// - percentage
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FuelGauge {
|
||||||
|
/// Area where the fuel gauge is rendered
|
||||||
|
area: Rect,
|
||||||
|
/// Alignment of the fuel gauge within its area
|
||||||
|
alignment: Alignment,
|
||||||
|
/// Mode of the fuel gauge (Always or OnChrgStatusChange)
|
||||||
|
mode: FuelGaugeMode,
|
||||||
|
/// State of battery charging
|
||||||
|
charging_state: ChargingState,
|
||||||
|
/// State of charge (0-100) [%]
|
||||||
|
soc: Option<u8>,
|
||||||
|
/// Timer to track temporary battery status showcase
|
||||||
|
timer: Timer,
|
||||||
|
/// Font used for the soc percentage
|
||||||
|
font: Font,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum FuelGaugeMode {
|
||||||
|
/// Always show the fuel gauge
|
||||||
|
Always,
|
||||||
|
/// Show the fuel gauge only when charging state changes
|
||||||
|
OnChargingChange,
|
||||||
|
/// Show the fuel gauge when charging state changes or when attached
|
||||||
|
OnChargingChangeOrAttach,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FuelGauge {
|
||||||
|
pub const fn always() -> Self {
|
||||||
|
Self::new(FuelGaugeMode::Always)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn on_charging_change() -> Self {
|
||||||
|
Self::new(FuelGaugeMode::OnChargingChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn on_charging_change_or_attach() -> Self {
|
||||||
|
Self::new(FuelGaugeMode::OnChargingChangeOrAttach)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
|
||||||
|
self.alignment = alignment;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn with_font(mut self, font: Font) -> Self {
|
||||||
|
self.font = font;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_pm_state(&mut self) {
|
||||||
|
self.soc = Some(power_manager::soc());
|
||||||
|
self.charging_state = power_manager::charging_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_be_shown(&self) -> bool {
|
||||||
|
match self.mode {
|
||||||
|
FuelGaugeMode::Always => true,
|
||||||
|
FuelGaugeMode::OnChargingChange | FuelGaugeMode::OnChargingChangeOrAttach => {
|
||||||
|
self.timer.is_running()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn new(mode: FuelGaugeMode) -> Self {
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
alignment: Alignment::Start,
|
||||||
|
mode,
|
||||||
|
charging_state: ChargingState::Idle,
|
||||||
|
soc: None,
|
||||||
|
timer: Timer::new(),
|
||||||
|
font: fonts::FONT_SATOSHI_REGULAR_22,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the icon, color for the icon, and color for the text based on
|
||||||
|
/// the charging state and state of charge (soc).
|
||||||
|
fn battery_indication(&self, charging_state: ChargingState, soc: u8) -> (Icon, Color, Color) {
|
||||||
|
const SOC_THRESHOLD_FULL: u8 = 80;
|
||||||
|
const SOC_THRESHOLD_MID: u8 = 25;
|
||||||
|
const SOC_THRESHOLD_LOW: u8 = 9;
|
||||||
|
match charging_state {
|
||||||
|
ChargingState::Charging => (ICON_BATTERY_ZAP, YELLOW, GREY_LIGHT),
|
||||||
|
ChargingState::Discharging | ChargingState::Idle => {
|
||||||
|
if soc > SOC_THRESHOLD_FULL {
|
||||||
|
(ICON_BATTERY_FULL, GREY_LIGHT, GREY_LIGHT)
|
||||||
|
} else if soc > SOC_THRESHOLD_MID {
|
||||||
|
(ICON_BATTERY_MID, GREY_LIGHT, GREY_LIGHT)
|
||||||
|
} else if soc > SOC_THRESHOLD_LOW {
|
||||||
|
(ICON_BATTERY_LOW, YELLOW, GREY_LIGHT)
|
||||||
|
} else {
|
||||||
|
(ICON_BATTERY_EMPTY, RED, RED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for FuelGauge {
|
||||||
|
type Msg = Never;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
self.area = bounds;
|
||||||
|
self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
match event {
|
||||||
|
Event::Attach(_) => {
|
||||||
|
if self.soc.is_none() {
|
||||||
|
self.update_pm_state();
|
||||||
|
}
|
||||||
|
if !animation_disabled() && self.mode == FuelGaugeMode::OnChargingChangeOrAttach {
|
||||||
|
self.timer.start(ctx, FUEL_GAUGE_DURATION.into());
|
||||||
|
}
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::PM(e) => {
|
||||||
|
self.update_pm_state();
|
||||||
|
match self.mode {
|
||||||
|
FuelGaugeMode::Always => {
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
FuelGaugeMode::OnChargingChange | FuelGaugeMode::OnChargingChangeOrAttach => {
|
||||||
|
if e.charging_status_changed {
|
||||||
|
self.timer.start(ctx, FUEL_GAUGE_DURATION.into());
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Timer(_) => {
|
||||||
|
if self.timer.expire(event) {
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
const ICON_PERCENT_GAP: i16 = 16;
|
||||||
|
|
||||||
|
let soc = self.soc.unwrap_or(0);
|
||||||
|
let (icon, color_icon, color_text) = self.battery_indication(self.charging_state, soc);
|
||||||
|
let soc_percent_fmt = if self.soc.is_none() {
|
||||||
|
uformat!("--")
|
||||||
|
} else {
|
||||||
|
uformat!("{} %", soc)
|
||||||
|
};
|
||||||
|
let text_width = self.font.text_width(&soc_percent_fmt);
|
||||||
|
let text_height = self.font.text_height();
|
||||||
|
let icon_width = icon.toif.width();
|
||||||
|
let icon_height = icon.toif.height();
|
||||||
|
|
||||||
|
let (point, alignment) = match self.alignment {
|
||||||
|
Alignment::Start => (self.area.left_center(), Alignment2D::CENTER_LEFT),
|
||||||
|
Alignment::End => (self.area.right_center(), Alignment2D::CENTER_RIGHT),
|
||||||
|
Alignment::Center => (self.area.center(), Alignment2D::CENTER),
|
||||||
|
};
|
||||||
|
|
||||||
|
let area = Rect::snap(
|
||||||
|
point,
|
||||||
|
Offset::new(
|
||||||
|
icon_width + ICON_PERCENT_GAP + text_width,
|
||||||
|
text_height.max(icon_height),
|
||||||
|
),
|
||||||
|
alignment,
|
||||||
|
);
|
||||||
|
let text_y_coord = self.font.vert_center(area.y0, area.y1, &soc_percent_fmt);
|
||||||
|
|
||||||
|
shape::ToifImage::new(area.left_center(), icon.toif)
|
||||||
|
.with_fg(color_icon)
|
||||||
|
.with_align(Alignment2D::CENTER_LEFT)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(
|
||||||
|
Point::new(area.x1, text_y_coord),
|
||||||
|
&soc_percent_fmt,
|
||||||
|
self.font,
|
||||||
|
)
|
||||||
|
.with_fg(color_text)
|
||||||
|
.with_align(Alignment::End)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for FuelGauge {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("FuelGauge");
|
||||||
|
t.int("soc", self.soc.unwrap_or(0) as i64);
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
super::component::{Button, ButtonContent, ButtonMsg},
|
super::{
|
||||||
|
component::{Button, ButtonContent, ButtonMsg},
|
||||||
|
firmware::FuelGauge,
|
||||||
|
},
|
||||||
constant, theme,
|
constant, theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,6 +34,8 @@ pub struct Header {
|
|||||||
/// icon in the top-left corner (used instead of left button)
|
/// icon in the top-left corner (used instead of left button)
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
icon_color: Option<Color>,
|
icon_color: Option<Color>,
|
||||||
|
/// Battery status indicator
|
||||||
|
fuel_gauge: Option<FuelGauge>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@ -56,6 +61,7 @@ impl Header {
|
|||||||
left_button_msg: HeaderMsg::Cancelled,
|
left_button_msg: HeaderMsg::Cancelled,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
fuel_gauge: Some(FuelGauge::on_charging_change()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +121,11 @@ impl Header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
pub fn with_fuel_gauge(self, fuel_gauge: Option<FuelGauge>) -> Self {
|
||||||
|
Self { fuel_gauge, ..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);
|
||||||
@ -148,24 +159,31 @@ impl Component for Header {
|
|||||||
|
|
||||||
let bounds = bounds.inset(Self::HEADER_INSETS);
|
let bounds = bounds.inset(Self::HEADER_INSETS);
|
||||||
let rest = if let Some(b) = &mut self.right_button {
|
let rest = if let Some(b) = &mut self.right_button {
|
||||||
let (rest, button_area) = bounds.split_right(Self::HEADER_BUTTON_WIDTH);
|
let (rest, right_button_area) = bounds.split_right(Self::HEADER_BUTTON_WIDTH);
|
||||||
b.place(button_area);
|
b.place(right_button_area);
|
||||||
rest
|
rest
|
||||||
} else {
|
} else {
|
||||||
bounds
|
bounds
|
||||||
};
|
};
|
||||||
|
|
||||||
let icon_width = self.left_icon_width();
|
let icon_width = self.left_icon_width();
|
||||||
let (rest, title_area) = rest.split_left(icon_width);
|
let (left_button_area, title_area) = rest.split_left(icon_width);
|
||||||
|
|
||||||
self.left_button.place(rest);
|
self.left_button.place(left_button_area);
|
||||||
self.title.place(title_area);
|
self.title.place(title_area);
|
||||||
|
self.fuel_gauge.place(title_area.union(left_button_area));
|
||||||
|
if let Some(fuel_gauge) = &mut self.fuel_gauge {
|
||||||
|
// Force update the fuel gauge state, so it is up-to-date
|
||||||
|
// necessary e.g. for the first DeviceMenu Submenu re-entry
|
||||||
|
fuel_gauge.update_pm_state();
|
||||||
|
}
|
||||||
|
|
||||||
self.area = bounds;
|
self.area = bounds;
|
||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
self.title.event(ctx, event);
|
self.fuel_gauge.event(ctx, event);
|
||||||
|
|
||||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||||
return Some(self.left_button_msg);
|
return Some(self.left_button_msg);
|
||||||
@ -179,6 +197,13 @@ impl Component for Header {
|
|||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
self.right_button.render(target);
|
self.right_button.render(target);
|
||||||
|
if self
|
||||||
|
.fuel_gauge
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|fg| fg.should_be_shown())
|
||||||
|
{
|
||||||
|
self.fuel_gauge.render(target);
|
||||||
|
} else {
|
||||||
self.left_button.render(target);
|
self.left_button.render(target);
|
||||||
if let Some(icon) = self.icon {
|
if let Some(icon) = self.icon {
|
||||||
shape::ToifImage::new(self.area.left_center(), icon.toif)
|
shape::ToifImage::new(self.area.left_center(), icon.toif)
|
||||||
@ -189,6 +214,7 @@ impl Component for Header {
|
|||||||
self.title.render(target);
|
self.title.render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
impl crate::trace::Trace for Header {
|
impl crate::trace::Trace for Header {
|
||||||
|
@ -7,21 +7,22 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{text::TextStyle, Component, Event, EventCtx, Label, Never},
|
component::{text::TextStyle, Component, Event, EventCtx, Label, Never},
|
||||||
display::{image::ImageInfo, Color},
|
display::{image::ImageInfo, Color},
|
||||||
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
|
||||||
layout::util::get_user_custom_image,
|
layout::util::get_user_custom_image,
|
||||||
lerp::Lerp,
|
lerp::Lerp,
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
super::{
|
super::{
|
||||||
component::{Button, ButtonMsg},
|
component::{Button, ButtonContent, ButtonMsg},
|
||||||
fonts,
|
fonts,
|
||||||
},
|
},
|
||||||
constant::{HEIGHT, SCREEN, WIDTH},
|
constant::{HEIGHT, SCREEN, WIDTH},
|
||||||
theme::{self, firmware::button_homebar_style, TILES_GRID},
|
theme::{self, firmware::button_homebar_style, TILES_GRID},
|
||||||
ActionBar, ActionBarMsg, Hint,
|
ActionBar, ActionBarMsg, FuelGauge, Hint,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCK_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(3000);
|
const LOCK_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(3000);
|
||||||
@ -42,8 +43,12 @@ pub struct Homescreen {
|
|||||||
lockable: bool,
|
lockable: bool,
|
||||||
/// Whether the homescreen is locked
|
/// Whether the homescreen is locked
|
||||||
locked: bool,
|
locked: bool,
|
||||||
|
/// Whether the homescreen is a boot screen
|
||||||
|
bootscreen: bool,
|
||||||
/// Hold to lock button placed everywhere except the `action_bar`
|
/// Hold to lock button placed everywhere except the `action_bar`
|
||||||
virtual_locking_button: Button,
|
virtual_locking_button: Button,
|
||||||
|
/// Fuel gauge (battery status indicator) rendered in the `action_bar` area
|
||||||
|
fuel_gauge: FuelGauge,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum HomescreenMsg {
|
pub enum HomescreenMsg {
|
||||||
@ -91,31 +96,53 @@ impl Homescreen {
|
|||||||
led_color = None;
|
led_color = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionBar button
|
|
||||||
let button_style = button_homebar_style(notification_level);
|
|
||||||
let button = if bootscreen {
|
|
||||||
Button::with_homebar_content(Some(TR::lockscreen__tap_to_connect.into()))
|
|
||||||
.styled(button_style)
|
|
||||||
} else if locked {
|
|
||||||
Button::with_homebar_content(Some(TR::lockscreen__tap_to_unlock.into()))
|
|
||||||
.styled(button_style)
|
|
||||||
} else {
|
|
||||||
// TODO: Battery/Connectivity button content
|
|
||||||
Button::with_homebar_content(None).styled(button_style)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
label: HomeLabel::new(label, shadow),
|
label: HomeLabel::new(label, shadow),
|
||||||
hint,
|
hint,
|
||||||
action_bar: ActionBar::new_single(button),
|
action_bar: ActionBar::new_single(
|
||||||
|
Button::new(Self::homebar_content(bootscreen, locked))
|
||||||
|
.styled(button_homebar_style(notification_level)),
|
||||||
|
),
|
||||||
image,
|
image,
|
||||||
led_color,
|
led_color,
|
||||||
lockable,
|
lockable,
|
||||||
locked,
|
locked,
|
||||||
|
bootscreen,
|
||||||
virtual_locking_button: Button::empty().with_long_press(LOCK_HOLD_DURATION),
|
virtual_locking_button: Button::empty().with_long_press(LOCK_HOLD_DURATION),
|
||||||
|
fuel_gauge: FuelGauge::on_charging_change_or_attach()
|
||||||
|
.with_alignment(Alignment::Center)
|
||||||
|
.with_font(fonts::FONT_SATOSHI_MEDIUM_26),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn homebar_content(bootscreen: bool, locked: bool) -> ButtonContent {
|
||||||
|
let text = if bootscreen {
|
||||||
|
Some(TR::lockscreen__tap_to_connect.into())
|
||||||
|
} else if locked {
|
||||||
|
Some(TR::lockscreen__tap_to_unlock.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
ButtonContent::HomeBar(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_fuel_gauge(&mut self, ctx: &mut EventCtx, event: Event) {
|
||||||
|
if animation_disabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fuel_gauge.event(ctx, event);
|
||||||
|
let bar_content = if self.fuel_gauge.should_be_shown() {
|
||||||
|
ButtonContent::Empty
|
||||||
|
} else {
|
||||||
|
Self::homebar_content(self.bootscreen, self.locked)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(b) = self.action_bar.right_button_mut() {
|
||||||
|
b.set_content(bar_content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn event_hold(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
|
fn event_hold(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
|
||||||
if let Some(ButtonMsg::LongPressed) = self.virtual_locking_button.event(ctx, event) {
|
if let Some(ButtonMsg::LongPressed) = self.virtual_locking_button.event(ctx, event) {
|
||||||
return true;
|
return true;
|
||||||
@ -146,6 +173,7 @@ impl Component for Homescreen {
|
|||||||
|
|
||||||
self.label.place(label_area);
|
self.label.place(label_area);
|
||||||
self.action_bar.place(bar_area);
|
self.action_bar.place(bar_area);
|
||||||
|
self.fuel_gauge.place(bar_area);
|
||||||
// Locking button is placed everywhere except the action bar
|
// Locking button is placed everywhere except the action bar
|
||||||
let locking_area = bounds.inset(Insets::bottom(self.action_bar.touch_area().height()));
|
let locking_area = bounds.inset(Insets::bottom(self.action_bar.touch_area().height()));
|
||||||
self.virtual_locking_button.place(locking_area);
|
self.virtual_locking_button.place(locking_area);
|
||||||
@ -153,6 +181,7 @@ impl Component for Homescreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
self.event_fuel_gauge(ctx, event);
|
||||||
if let Some(ActionBarMsg::Confirmed) = self.action_bar.event(ctx, event) {
|
if let Some(ActionBarMsg::Confirmed) = self.action_bar.event(ctx, event) {
|
||||||
if self.locked {
|
if self.locked {
|
||||||
return Some(HomescreenMsg::Dismissed);
|
return Some(HomescreenMsg::Dismissed);
|
||||||
@ -178,6 +207,9 @@ impl Component for Homescreen {
|
|||||||
self.label.render(target);
|
self.label.render(target);
|
||||||
self.hint.render(target);
|
self.hint.render(target);
|
||||||
self.action_bar.render(target);
|
self.action_bar.render(target);
|
||||||
|
if self.fuel_gauge.should_be_shown() {
|
||||||
|
self.fuel_gauge.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ mod device_menu_screen;
|
|||||||
mod fido;
|
mod fido;
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
mod fido_icons;
|
mod fido_icons;
|
||||||
|
mod fuel_gauge;
|
||||||
mod header;
|
mod header;
|
||||||
mod hint;
|
mod hint;
|
||||||
mod hold_to_confirm;
|
mod hold_to_confirm;
|
||||||
@ -25,6 +26,7 @@ pub use brightness_screen::SetBrightnessScreen;
|
|||||||
pub use confirm_homescreen::{ConfirmHomescreen, ConfirmHomescreenMsg};
|
pub use confirm_homescreen::{ConfirmHomescreen, ConfirmHomescreenMsg};
|
||||||
pub use device_menu_screen::{DeviceMenuMsg, DeviceMenuScreen};
|
pub use device_menu_screen::{DeviceMenuMsg, DeviceMenuScreen};
|
||||||
pub use fido::{FidoAccountName, FidoCredential};
|
pub use fido::{FidoAccountName, FidoCredential};
|
||||||
|
pub use fuel_gauge::FuelGauge;
|
||||||
pub use header::{Header, HeaderMsg};
|
pub use header::{Header, HeaderMsg};
|
||||||
pub use hint::Hint;
|
pub use hint::Hint;
|
||||||
pub use hold_to_confirm::HoldToConfirmAnim;
|
pub use hold_to_confirm::HoldToConfirmAnim;
|
||||||
|
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/empty.png
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/empty.png
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/empty.toif
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/empty.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/full.png
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/full.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 280 B |
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/full.toif
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/full.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/low.png
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/low.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 271 B |
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/low.toif
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/low.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/mid.png
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/mid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 283 B |
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/mid.toif
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/mid.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/zap.png
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/zap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 243 B |
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/zap.toif
Normal file
BIN
core/embed/rust/src/ui/layout_eckhart/res/battery/zap.toif
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 154 B |
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 236 B |
Binary file not shown.
@ -16,6 +16,8 @@ use super::{
|
|||||||
|
|
||||||
pub const CONFIRM_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(1500);
|
pub const CONFIRM_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(1500);
|
||||||
pub const ERASE_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(1500);
|
pub const ERASE_HOLD_DURATION: ShortDuration = ShortDuration::from_millis(1500);
|
||||||
|
/// Duration for the battery status showcase on the ActionBar or Header
|
||||||
|
pub const FUEL_GAUGE_DURATION: ShortDuration = ShortDuration::from_millis(2000);
|
||||||
|
|
||||||
// Text styles
|
// Text styles
|
||||||
/// Alias for use with copied code, might be deleted later
|
/// Alias for use with copied code, might be deleted later
|
||||||
|
@ -116,8 +116,11 @@ include_icon!(ICON_LOGO, "layout_eckhart/res/lock_full.toif");
|
|||||||
include_icon!(ICON_WARNING40, "layout_eckhart/res/warning40.toif");
|
include_icon!(ICON_WARNING40, "layout_eckhart/res/warning40.toif");
|
||||||
|
|
||||||
// Battery icons
|
// Battery icons
|
||||||
include_icon!(ICON_BATTERY_BAR, "layout_eckhart/res/battery_bar.toif");
|
include_icon!(ICON_BATTERY_ZAP, "layout_eckhart/res/battery/zap.toif");
|
||||||
include_icon!(ICON_BATTERY_ZAP, "layout_eckhart/res/battery_zap.toif");
|
include_icon!(ICON_BATTERY_FULL, "layout_eckhart/res/battery/full.toif");
|
||||||
|
include_icon!(ICON_BATTERY_MID, "layout_eckhart/res/battery/mid.toif");
|
||||||
|
include_icon!(ICON_BATTERY_LOW, "layout_eckhart/res/battery/low.toif");
|
||||||
|
include_icon!(ICON_BATTERY_EMPTY, "layout_eckhart/res/battery/empty.toif");
|
||||||
|
|
||||||
// Border overlay icons for bootloader screens and hold to confirm animation
|
// Border overlay icons for bootloader screens and hold to confirm animation
|
||||||
include_icon!(ICON_BORDER_BL, "layout_eckhart/res/border/BL.toif");
|
include_icon!(ICON_BORDER_BL, "layout_eckhart/res/border/BL.toif");
|
||||||
|
Loading…
Reference in New Issue
Block a user