mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-22 06:18:07 +00:00
feat(core): integrate new drawing library
[no changelog]
This commit is contained in:
parent
5d8a7ac5bf
commit
28aa0a7ee3
@ -72,12 +72,16 @@ static void format_ver(const char *format, uint32_t version, char *buffer,
|
|||||||
|
|
||||||
// boot UI
|
// boot UI
|
||||||
|
|
||||||
|
#ifndef NEW_RENDERING
|
||||||
static uint16_t boot_background;
|
static uint16_t boot_background;
|
||||||
|
#endif
|
||||||
|
|
||||||
static bool initial_setup = true;
|
static bool initial_setup = true;
|
||||||
|
|
||||||
void ui_set_initial_setup(bool initial) { initial_setup = initial; }
|
void ui_set_initial_setup(bool initial) { initial_setup = initial; }
|
||||||
|
|
||||||
void ui_screen_boot(const vendor_header *const vhdr,
|
#ifndef NEW_RENDERING
|
||||||
|
static void ui_screen_boot_old(const vendor_header *const vhdr,
|
||||||
const image_header *const hdr) {
|
const image_header *const hdr) {
|
||||||
const int show_string = ((vhdr->vtrust & VTRUST_STRING) == 0);
|
const int show_string = ((vhdr->vtrust & VTRUST_STRING) == 0);
|
||||||
if ((vhdr->vtrust & VTRUST_RED) == 0) {
|
if ((vhdr->vtrust & VTRUST_RED) == 0) {
|
||||||
@ -130,9 +134,11 @@ void ui_screen_boot(const vendor_header *const vhdr,
|
|||||||
display_pixeldata_dirty();
|
display_pixeldata_dirty();
|
||||||
display_refresh();
|
display_refresh();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void ui_screen_boot_wait(int wait_seconds) {
|
#ifndef NEW_RENDERING
|
||||||
char wait_str[16];
|
static void ui_screen_boot_wait(int wait_seconds) {
|
||||||
|
char wait_str[32];
|
||||||
mini_snprintf(wait_str, sizeof(wait_str), "starting in %d s", wait_seconds);
|
mini_snprintf(wait_str, sizeof(wait_str), "starting in %d s", wait_seconds);
|
||||||
display_bar(0, BOOT_WAIT_Y_TOP, DISPLAY_RESX, BOOT_WAIT_HEIGHT,
|
display_bar(0, BOOT_WAIT_Y_TOP, DISPLAY_RESX, BOOT_WAIT_HEIGHT,
|
||||||
boot_background);
|
boot_background);
|
||||||
@ -141,6 +147,7 @@ void ui_screen_boot_wait(int wait_seconds) {
|
|||||||
display_pixeldata_dirty();
|
display_pixeldata_dirty();
|
||||||
display_refresh();
|
display_refresh();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined USE_TOUCH
|
#if defined USE_TOUCH
|
||||||
#include "touch.h"
|
#include "touch.h"
|
||||||
@ -182,7 +189,8 @@ void ui_click(void) {
|
|||||||
#error "No input method defined"
|
#error "No input method defined"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void ui_screen_boot_click(void) {
|
#ifndef NEW_RENDERING
|
||||||
|
static void ui_screen_boot_click(void) {
|
||||||
display_bar(0, BOOT_WAIT_Y_TOP, DISPLAY_RESX, BOOT_WAIT_HEIGHT,
|
display_bar(0, BOOT_WAIT_Y_TOP, DISPLAY_RESX, BOOT_WAIT_HEIGHT,
|
||||||
boot_background);
|
boot_background);
|
||||||
bld_continue_label(boot_background);
|
bld_continue_label(boot_background);
|
||||||
@ -190,6 +198,33 @@ void ui_screen_boot_click(void) {
|
|||||||
display_refresh();
|
display_refresh();
|
||||||
ui_click();
|
ui_click();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef NEW_RENDERING
|
||||||
|
void ui_screen_boot(const vendor_header *const vhdr,
|
||||||
|
const image_header *const hdr, int wait) {
|
||||||
|
bool show_string = ((vhdr->vtrust & VTRUST_STRING) == 0);
|
||||||
|
const char *vendor_str = show_string ? vhdr->vstr : NULL;
|
||||||
|
const size_t vendor_str_len = show_string ? vhdr->vstr_len : 0;
|
||||||
|
bool red_screen = ((vhdr->vtrust & VTRUST_RED) == 0);
|
||||||
|
uint32_t vimg_len = *(uint32_t *)(vhdr->vimg + 8);
|
||||||
|
|
||||||
|
screen_boot(red_screen, vendor_str, vendor_str_len, hdr->version, vhdr->vimg,
|
||||||
|
vimg_len, wait);
|
||||||
|
}
|
||||||
|
#else // NEW_RENDERING
|
||||||
|
|
||||||
|
void ui_screen_boot(const vendor_header *const vhdr,
|
||||||
|
const image_header *const hdr, int wait) {
|
||||||
|
if (wait == 0) {
|
||||||
|
ui_screen_boot_old(vhdr, hdr);
|
||||||
|
} else if (wait > 0) {
|
||||||
|
ui_screen_boot_wait(wait);
|
||||||
|
} else {
|
||||||
|
ui_screen_boot_click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// welcome UI
|
// welcome UI
|
||||||
|
|
||||||
|
@ -34,10 +34,22 @@ typedef enum {
|
|||||||
SCREEN_WELCOME = 5,
|
SCREEN_WELCOME = 5,
|
||||||
} screen_t;
|
} screen_t;
|
||||||
|
|
||||||
|
// Displays a warning screeen before jumping to the untrusted firmware
|
||||||
|
//
|
||||||
|
// Shows vendor image, vendor string and firmware version
|
||||||
|
// and optional message to the user (see `wait` argument)
|
||||||
|
//
|
||||||
|
// `wait` argument specifies a message to the user
|
||||||
|
// 0 do not show any message
|
||||||
|
// > 0 show a message like "starting in %d s"
|
||||||
|
// < 0 show a message like "press button to continue"
|
||||||
void ui_screen_boot(const vendor_header* const vhdr,
|
void ui_screen_boot(const vendor_header* const vhdr,
|
||||||
const image_header* const hdr);
|
const image_header* const hdr, int wait);
|
||||||
void ui_screen_boot_wait(int wait_seconds);
|
|
||||||
void ui_screen_boot_click(void);
|
// Waits until the user confirms the untrusted firmware
|
||||||
|
//
|
||||||
|
// Implementation is device specific - it wait's until
|
||||||
|
// the user presses a button, touches the display
|
||||||
void ui_click(void);
|
void ui_click(void);
|
||||||
|
|
||||||
void ui_screen_welcome(void);
|
void ui_screen_welcome(void);
|
||||||
|
@ -331,13 +331,13 @@ void real_jump_to_firmware(void) {
|
|||||||
// if all VTRUST flags are unset = ultimate trust => skip the procedure
|
// if all VTRUST flags are unset = ultimate trust => skip the procedure
|
||||||
if ((vhdr.vtrust & VTRUST_ALL) != VTRUST_ALL) {
|
if ((vhdr.vtrust & VTRUST_ALL) != VTRUST_ALL) {
|
||||||
ui_fadeout();
|
ui_fadeout();
|
||||||
ui_screen_boot(&vhdr, hdr);
|
ui_screen_boot(&vhdr, hdr, 0);
|
||||||
ui_fadein();
|
ui_fadein();
|
||||||
|
|
||||||
int delay = (vhdr.vtrust & VTRUST_WAIT) ^ VTRUST_WAIT;
|
int delay = (vhdr.vtrust & VTRUST_WAIT) ^ VTRUST_WAIT;
|
||||||
if (delay > 1) {
|
if (delay > 1) {
|
||||||
while (delay > 0) {
|
while (delay > 0) {
|
||||||
ui_screen_boot_wait(delay);
|
ui_screen_boot(&vhdr, hdr, delay);
|
||||||
hal_delay(1000);
|
hal_delay(1000);
|
||||||
delay--;
|
delay--;
|
||||||
}
|
}
|
||||||
@ -346,7 +346,8 @@ void real_jump_to_firmware(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((vhdr.vtrust & VTRUST_CLICK) == 0) {
|
if ((vhdr.vtrust & VTRUST_CLICK) == 0) {
|
||||||
ui_screen_boot_click();
|
ui_screen_boot(&vhdr, hdr, -1);
|
||||||
|
ui_click();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui_screen_boot_stage_1(false);
|
ui_screen_boot_stage_1(false);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#include "rust_ui_bootloader.h"
|
#include "rust_ui_bootloader.h"
|
||||||
#include "rust_ui_common.h"
|
#include "rust_ui_common.h"
|
||||||
|
@ -24,3 +24,6 @@ void screen_boot_stage_1(bool fading);
|
|||||||
uint32_t screen_unlock_bootloader_confirm(void);
|
uint32_t screen_unlock_bootloader_confirm(void);
|
||||||
void screen_unlock_bootloader_success(void);
|
void screen_unlock_bootloader_success(void);
|
||||||
void bld_continue_label(uint16_t bg_color);
|
void bld_continue_label(uint16_t bg_color);
|
||||||
|
void screen_boot(bool warning, const char* vendor_str, size_t vendor_str_len,
|
||||||
|
uint32_t version, const void* vendor_img,
|
||||||
|
size_t vendor_img_len, int wait);
|
||||||
|
@ -13,6 +13,7 @@ extern "C" fn screen_welcome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
extern "C" fn bld_continue_label(bg_color: cty::uint16_t) {
|
extern "C" fn bld_continue_label(bg_color: cty::uint16_t) {
|
||||||
ModelUI::bld_continue_label(bg_color.into());
|
ModelUI::bld_continue_label(bg_color.into());
|
||||||
}
|
}
|
||||||
@ -101,6 +102,28 @@ extern "C" fn screen_boot_stage_1(fading: bool) {
|
|||||||
ModelUI::screen_boot_stage_1(fading)
|
ModelUI::screen_boot_stage_1(fading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
extern "C" fn screen_boot(
|
||||||
|
warning: bool,
|
||||||
|
vendor_str: *const cty::c_char,
|
||||||
|
vendor_str_len: usize,
|
||||||
|
version: u32,
|
||||||
|
vendor_img: *const cty::c_void,
|
||||||
|
vendor_img_len: usize,
|
||||||
|
wait: i32,
|
||||||
|
) {
|
||||||
|
let vendor_str = unsafe { from_c_array(vendor_str, vendor_str_len) };
|
||||||
|
let vendor_img =
|
||||||
|
unsafe { core::slice::from_raw_parts(vendor_img as *const u8, vendor_img_len) };
|
||||||
|
|
||||||
|
// Splits a version stored as a u32 into four numbers
|
||||||
|
// starting with the major version.
|
||||||
|
let version = version.to_le_bytes();
|
||||||
|
|
||||||
|
ModelUI::screen_boot(warning, vendor_str, version, vendor_img, wait);
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
extern "C" fn screen_wipe_progress(progress: u16, initialize: bool) {
|
extern "C" fn screen_wipe_progress(progress: u16, initialize: bool) {
|
||||||
ModelUI::screen_wipe_progress(progress, initialize)
|
ModelUI::screen_wipe_progress(progress, initialize)
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
//! Reexporting the `screens` module according to the
|
//! Reexporting the `screens` module according to the
|
||||||
//! current feature (Trezor model)
|
//! current feature (Trezor model)
|
||||||
|
|
||||||
|
use crate::ui::ui_features::{ModelUI, UIFeaturesCommon};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::image::Image,
|
component::image::Image,
|
||||||
display::{Color, Icon},
|
display::{Color, Icon},
|
||||||
geometry::{Alignment2D, Point},
|
geometry::{Alignment2D, Point},
|
||||||
ui_features::{ModelUI, UIFeaturesCommon},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ui::util::from_c_str;
|
use crate::ui::util::from_c_str;
|
||||||
@ -29,6 +31,7 @@ extern "C" fn screen_boot_stage_2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
extern "C" fn display_icon(
|
extern "C" fn display_icon(
|
||||||
x: cty::int16_t,
|
x: cty::int16_t,
|
||||||
y: cty::int16_t,
|
y: cty::int16_t,
|
||||||
@ -48,6 +51,7 @@ extern "C" fn display_icon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
extern "C" fn display_image(
|
extern "C" fn display_image(
|
||||||
x: cty::int16_t,
|
x: cty::int16_t,
|
||||||
y: cty::int16_t,
|
y: cty::int16_t,
|
||||||
|
62
core/embed/rust/src/ui/component/bar.rs
Normal file
62
core/embed/rust/src/ui/component/bar.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
component::{Component, Event, EventCtx, Never},
|
||||||
|
display,
|
||||||
|
display::Color,
|
||||||
|
geometry::Rect,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Bar {
|
||||||
|
area: Rect,
|
||||||
|
color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
radius: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bar {
|
||||||
|
pub fn new(color: Color, bg_color: Color, radius: i16) -> Self {
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
color,
|
||||||
|
bg_color,
|
||||||
|
radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Bar {
|
||||||
|
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> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
display::rect_fill_rounded(self.area, self.color, self.bg_color, self.radius as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_bg(self.color)
|
||||||
|
.with_radius(self.radius)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_bounds")]
|
||||||
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
|
sink(self.area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for Bar {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("Bar");
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ use crate::{
|
|||||||
component::{maybe::PaintOverlapping, MsgMap},
|
component::{maybe::PaintOverlapping, MsgMap},
|
||||||
display::{self, Color},
|
display::{self, Color},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,6 +62,8 @@ pub trait Component {
|
|||||||
/// the `Child` wrapper.
|
/// the `Child` wrapper.
|
||||||
fn paint(&mut self);
|
fn paint(&mut self);
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>);
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
/// Report current paint bounds of this component. Used for debugging.
|
/// Report current paint bounds of this component. Used for debugging.
|
||||||
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
||||||
@ -154,6 +157,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.component.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.component.bounds(sink)
|
self.component.bounds(sink)
|
||||||
@ -254,6 +261,10 @@ where
|
|||||||
self.inner.paint();
|
self.inner.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.inner.bounds(sink)
|
self.inner.bounds(sink)
|
||||||
@ -292,6 +303,11 @@ where
|
|||||||
self.1.paint();
|
self.1.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.0.render(target);
|
||||||
|
self.1.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.0.bounds(sink);
|
self.0.bounds(sink);
|
||||||
@ -341,6 +357,12 @@ where
|
|||||||
self.2.paint();
|
self.2.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.0.render(target);
|
||||||
|
self.1.render(target);
|
||||||
|
self.2.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.0.bounds(sink);
|
self.0.bounds(sink);
|
||||||
@ -368,6 +390,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if let Some(ref c) = self {
|
||||||
|
c.render(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
match self {
|
match self {
|
||||||
Some(ref mut c) => c.place(bounds),
|
Some(ref mut c) => c.place(bounds),
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use super::{Component, Event, EventCtx};
|
use super::{Component, Event, EventCtx};
|
||||||
use crate::ui::geometry::{Insets, Rect};
|
use crate::ui::{
|
||||||
|
geometry::{Insets, Rect},
|
||||||
|
shape::Renderer,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Border<T> {
|
pub struct Border<T> {
|
||||||
border: Insets,
|
border: Insets,
|
||||||
@ -39,6 +42,10 @@ where
|
|||||||
self.inner.paint()
|
self.inner.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.inner.bounds(sink);
|
self.inner.bounds(sink);
|
||||||
|
@ -3,7 +3,8 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, Never, Pad},
|
component::{Component, Event, EventCtx, Never, Pad},
|
||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
|
shape::{self, Renderer},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,6 +56,20 @@ impl Component for Connect {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let font = Font::NORMAL;
|
||||||
|
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
self.message.map(|t| {
|
||||||
|
shape::Text::new(self.bg.area.center() + Offset::y(font.text_height() / 2), t)
|
||||||
|
.with_fg(self.fg)
|
||||||
|
.with_font(font)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::{Component, Event, EventCtx, Never};
|
use super::{Component, Event, EventCtx, Never};
|
||||||
use crate::ui::geometry::Rect;
|
use crate::ui::{geometry::Rect, shape::Renderer};
|
||||||
|
|
||||||
pub struct Empty;
|
pub struct Empty;
|
||||||
|
|
||||||
@ -15,6 +15,8 @@ impl Component for Empty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {}
|
fn paint(&mut self) {}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -6,6 +6,8 @@ use crate::ui::{
|
|||||||
Color, Icon,
|
Color, Icon,
|
||||||
},
|
},
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
@ -48,6 +50,12 @@ impl Component for Image {
|
|||||||
self.draw(self.area.center(), Alignment2D::CENTER);
|
self.draw(self.area.center(), Alignment2D::CENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::ToifImage::new(self.area.center(), self.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(Rect::from_center_and_size(
|
sink(Rect::from_center_and_size(
|
||||||
@ -130,6 +138,15 @@ impl Component for BlendedImage {
|
|||||||
self.paint_image();
|
self.paint_image();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::ToifImage::new(self.bg_top_left, self.bg.toif)
|
||||||
|
.with_fg(self.bg_color)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(self.bg_top_left + self.fg_offset, self.fg.toif)
|
||||||
|
.with_fg(self.fg_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(Rect::from_top_left_and_size(
|
sink(Rect::from_top_left_and_size(
|
||||||
|
69
core/embed/rust/src/ui/component/jpeg.rs
Normal file
69
core/embed/rust/src/ui/component/jpeg.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::{
|
||||||
|
io::BinaryData,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx, Never},
|
||||||
|
display,
|
||||||
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Jpeg {
|
||||||
|
area: Rect,
|
||||||
|
image: BinaryData<'static>,
|
||||||
|
scale: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Jpeg {
|
||||||
|
pub fn new(image: BinaryData<'static>, scale: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
area: Rect::zero(),
|
||||||
|
image,
|
||||||
|
scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Jpeg {
|
||||||
|
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> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
|
// discarded before returning to micropython.
|
||||||
|
let jpeg_data = unsafe { self.image.data() };
|
||||||
|
|
||||||
|
if let Some((size, _)) = display::tjpgd::jpeg_info(jpeg_data) {
|
||||||
|
let off = Offset::new(size.x / (2 << self.scale), size.y / (2 << self.scale));
|
||||||
|
display::tjpgd::jpeg(jpeg_data, self.area.center() - off, self.scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::JpegImage::new_image(self.area.center(), self.image)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_scale(self.scale)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_bounds")]
|
||||||
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
|
sink(self.area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for Jpeg {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("Jpeg");
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
component::{Component, Event, EventCtx, Never},
|
component::{Component, Event, EventCtx, Never},
|
||||||
display::Font,
|
display::Font,
|
||||||
geometry::{Alignment, Insets, Offset, Point, Rect},
|
geometry::{Alignment, Insets, Offset, Point, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,6 +115,10 @@ impl Component for Label<'_> {
|
|||||||
self.text.map(|c| self.layout.render_text(c));
|
self.text.map(|c| self.layout.render_text(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.text.map(|c| self.layout.render_text2(c, target));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.layout.bounds)
|
sink(self.layout.bounds)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::{Component, Event, EventCtx};
|
use super::{Component, Event, EventCtx};
|
||||||
use crate::ui::geometry::Rect;
|
use crate::ui::{geometry::Rect, shape::Renderer};
|
||||||
|
|
||||||
pub struct MsgMap<T, F> {
|
pub struct MsgMap<T, F> {
|
||||||
inner: T,
|
inner: T,
|
||||||
@ -31,6 +31,10 @@ where
|
|||||||
self.inner.paint()
|
self.inner.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.inner.bounds(sink);
|
self.inner.bounds(sink);
|
||||||
|
@ -4,9 +4,9 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
animation::Animation,
|
animation::Animation,
|
||||||
component::{Component, Event, EventCtx, Never, TimerToken},
|
component::{Component, Event, EventCtx, Never, TimerToken},
|
||||||
display,
|
display::{self, Color, Font},
|
||||||
display::{Color, Font},
|
geometry::{Offset, Rect},
|
||||||
geometry::Rect,
|
shape::{self, Renderer},
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -123,6 +123,19 @@ impl Marquee {
|
|||||||
self.text
|
self.text
|
||||||
.map(|t| display::marquee(self.area, t, offset, self.font, self.fg, self.bg));
|
.map(|t| display::marquee(self.area, t, offset, self.font, self.fg, self.bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_anim<'s>(&'s self, target: &mut impl Renderer<'s>, offset: i16) {
|
||||||
|
target.in_window(self.area, &|target| {
|
||||||
|
let text_height = self.font.text_height();
|
||||||
|
let pos = self.area.top_left() + Offset::new(offset, text_height - 1);
|
||||||
|
self.text.map(|t| {
|
||||||
|
shape::Text::new(pos, t)
|
||||||
|
.with_font(self.font)
|
||||||
|
.with_fg(self.fg)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Marquee {
|
impl Component for Marquee {
|
||||||
@ -214,6 +227,30 @@ impl Component for Marquee {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
State::Initial => {
|
||||||
|
self.render_anim(target, 0);
|
||||||
|
}
|
||||||
|
State::PauseRight => {
|
||||||
|
self.render_anim(target, self.min_offset);
|
||||||
|
}
|
||||||
|
State::PauseLeft => {
|
||||||
|
self.render_anim(target, self.max_offset);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let progress = self.progress(now);
|
||||||
|
if let Some(done) = progress {
|
||||||
|
self.render_anim(target, done);
|
||||||
|
} else {
|
||||||
|
self.render_anim(target, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -2,6 +2,7 @@ use crate::ui::{
|
|||||||
component::{Component, ComponentExt, Event, EventCtx, Pad},
|
component::{Component, ComponentExt, Event, EventCtx, Pad},
|
||||||
display::{self, Color},
|
display::{self, Color},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Maybe<T> {
|
pub struct Maybe<T> {
|
||||||
@ -94,6 +95,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
if self.visible {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.pad.area);
|
sink(self.pad.area);
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
#![forbid(unsafe_code)]
|
//#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod bar;
|
||||||
pub mod base;
|
pub mod base;
|
||||||
pub mod border;
|
pub mod border;
|
||||||
pub mod connect;
|
pub mod connect;
|
||||||
pub mod empty;
|
pub mod empty;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
#[cfg(all(feature = "jpeg", feature = "micropython"))]
|
||||||
|
pub mod jpeg;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod marquee;
|
pub mod marquee;
|
||||||
pub mod maybe;
|
pub mod maybe;
|
||||||
pub mod pad;
|
pub mod pad;
|
||||||
pub mod paginated;
|
pub mod paginated;
|
||||||
pub mod painter;
|
|
||||||
pub mod placed;
|
pub mod placed;
|
||||||
pub mod qr_code;
|
pub mod qr_code;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod timeout;
|
pub mod timeout;
|
||||||
|
|
||||||
|
pub use bar::Bar;
|
||||||
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, TimerToken};
|
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, TimerToken};
|
||||||
pub use border::Border;
|
pub use border::Border;
|
||||||
pub use empty::Empty;
|
pub use empty::Empty;
|
||||||
|
#[cfg(all(feature = "jpeg", feature = "micropython"))]
|
||||||
|
pub use jpeg::Jpeg;
|
||||||
pub use label::Label;
|
pub use label::Label;
|
||||||
pub use map::MsgMap;
|
pub use map::MsgMap;
|
||||||
pub use marquee::Marquee;
|
pub use marquee::Marquee;
|
||||||
pub use maybe::Maybe;
|
pub use maybe::Maybe;
|
||||||
pub use pad::Pad;
|
pub use pad::Pad;
|
||||||
pub use paginated::{PageMsg, Paginate};
|
pub use paginated::{PageMsg, Paginate};
|
||||||
pub use painter::Painter;
|
|
||||||
pub use placed::{FixedHeightBar, Floating, GridPlaced, Split};
|
pub use placed::{FixedHeightBar, Floating, GridPlaced, Split};
|
||||||
pub use qr_code::Qr;
|
pub use qr_code::Qr;
|
||||||
pub use text::{
|
pub use text::{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
display::{self, Color},
|
display::{self, Color},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Pad {
|
pub struct Pad {
|
||||||
@ -52,4 +54,10 @@ impl Pad {
|
|||||||
display::rect_fill(self.area, self.color);
|
display::rect_fill(self.area, self.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_bg(self.color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
#[cfg(feature = "jpeg")]
|
|
||||||
use crate::ui::geometry::Offset;
|
|
||||||
use crate::ui::{
|
|
||||||
component::{image::Image, Component, Event, EventCtx, Never},
|
|
||||||
display,
|
|
||||||
geometry::{Alignment2D, Rect},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Painter<F> {
|
|
||||||
area: Rect,
|
|
||||||
func: F,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F> Painter<F> {
|
|
||||||
pub fn new(func: F) -> Self {
|
|
||||||
Self {
|
|
||||||
func,
|
|
||||||
area: Rect::zero(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F> Component for Painter<F>
|
|
||||||
where
|
|
||||||
F: FnMut(Rect),
|
|
||||||
{
|
|
||||||
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> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(&mut self) {
|
|
||||||
(self.func)(self.area);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
|
||||||
sink(self.area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
|
||||||
impl<F> crate::trace::Trace for Painter<F> {
|
|
||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
|
||||||
t.component("Painter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_painter(image: Image) -> Painter<impl FnMut(Rect)> {
|
|
||||||
let f = move |area: Rect| image.draw(area.center(), Alignment2D::CENTER);
|
|
||||||
Painter::new(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "jpeg")]
|
|
||||||
pub fn jpeg_painter<'a>(
|
|
||||||
image: impl Fn() -> &'a [u8],
|
|
||||||
size: Offset,
|
|
||||||
scale: u8,
|
|
||||||
) -> Painter<impl FnMut(Rect)> {
|
|
||||||
let off = Offset::new(size.x / (2 << scale), size.y / (2 << scale));
|
|
||||||
let f = move |area: Rect| display::tjpgd::jpeg(image(), area.center() - off, scale);
|
|
||||||
Painter::new(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rect_painter(fg: display::Color, bg: display::Color) -> Painter<impl FnMut(Rect)> {
|
|
||||||
let f = move |area: Rect| display::rect_fill_rounded(area, fg, bg, 2);
|
|
||||||
Painter::new(f)
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
geometry::{Alignment, Alignment2D, Axis, Grid, GridCellSpan, Insets, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Axis, Grid, GridCellSpan, Insets, Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct GridPlaced<T> {
|
pub struct GridPlaced<T> {
|
||||||
@ -63,6 +64,10 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.inner.paint()
|
self.inner.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
@ -106,6 +111,10 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.inner.paint()
|
self.inner.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
@ -178,6 +187,10 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.inner.paint()
|
self.inner.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.inner.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
@ -269,6 +282,11 @@ where
|
|||||||
self.first.paint();
|
self.first.paint();
|
||||||
self.second.paint();
|
self.second.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.first.render(target);
|
||||||
|
self.second.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -8,6 +8,8 @@ use crate::{
|
|||||||
constant,
|
constant,
|
||||||
display::{pixeldata, pixeldata_dirty, rect_fill_rounded, set_window, Color},
|
display::{pixeldata, pixeldata_dirty, rect_fill_rounded, set_window, Color},
|
||||||
geometry::{Insets, Offset, Rect},
|
geometry::{Insets, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,6 +143,39 @@ impl Component for Qr {
|
|||||||
Self::draw(&qr, area, self.border, scale);
|
Self::draw(&qr, area, self.border, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let mut outbuffer = [0u8; QR_MAX_VERSION.buffer_len()];
|
||||||
|
let mut tempbuffer = [0u8; QR_MAX_VERSION.buffer_len()];
|
||||||
|
|
||||||
|
let qr = QrCode::encode_text(
|
||||||
|
self.text.as_ref(),
|
||||||
|
&mut tempbuffer,
|
||||||
|
&mut outbuffer,
|
||||||
|
QrCodeEcc::Medium,
|
||||||
|
Version::MIN,
|
||||||
|
QR_MAX_VERSION,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let qr = unwrap!(qr);
|
||||||
|
|
||||||
|
let scale = (self.area.width().min(self.area.height()) - self.border) / (qr.size() as i16);
|
||||||
|
let side = scale * qr.size() as i16;
|
||||||
|
let qr_area = Rect::from_center_and_size(self.area.center(), Offset::uniform(side));
|
||||||
|
|
||||||
|
if self.border > 0 {
|
||||||
|
shape::Bar::new(qr_area.expand(self.border))
|
||||||
|
.with_bg(LIGHT)
|
||||||
|
.with_radius(CORNER_RADIUS as i16 + 1) // !@# + 1 to fix difference on TR
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
shape::QrImage::new(qr_area, &qr)
|
||||||
|
.with_fg(LIGHT)
|
||||||
|
.with_bg(DARK)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area)
|
sink(self.area)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx, Never, Paginate},
|
component::{Component, Event, EventCtx, Never, Paginate},
|
||||||
geometry::{Alignment, Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer},
|
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer, TextRenderer2},
|
||||||
op::OpTextLayout,
|
op::OpTextLayout,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,6 +134,10 @@ impl Component for FormattedText {
|
|||||||
self.layout_content(&mut TextRenderer);
|
self.layout_content(&mut TextRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.layout_content(&mut TextRenderer2::new(target));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.op_layout.layout.bounds)
|
sink(self.op_layout.layout.bounds)
|
||||||
|
@ -2,6 +2,8 @@ use crate::ui::{
|
|||||||
display,
|
display,
|
||||||
display::{toif::Icon, Color, Font, GlyphMetrics},
|
display::{toif::Icon, Color, Font, GlyphMetrics},
|
||||||
geometry::{Alignment, Alignment2D, Dimensions, Offset, Point, Rect},
|
geometry::{Alignment, Alignment2D, Dimensions, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ELLIPSIS: &str = "...";
|
const ELLIPSIS: &str = "...";
|
||||||
@ -235,6 +237,15 @@ impl TextLayout {
|
|||||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer)
|
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw as much text as possible on the current screen.
|
||||||
|
pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit {
|
||||||
|
self.layout_text(
|
||||||
|
text,
|
||||||
|
&mut self.initial_cursor(),
|
||||||
|
&mut TextRenderer2::new(target),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Loop through the `text` and try to fit it on the current screen,
|
/// Loop through the `text` and try to fit it on the current screen,
|
||||||
/// reporting events to `sink`, which may do something with them (e.g. draw
|
/// reporting events to `sink`, which may do something with them (e.g. draw
|
||||||
/// on screen).
|
/// on screen).
|
||||||
@ -530,6 +541,67 @@ impl LayoutSink for TextRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TextRenderer2<'a, 's, R>(pub &'a mut R, core::marker::PhantomData<&'s ()>)
|
||||||
|
where
|
||||||
|
R: Renderer<'s>;
|
||||||
|
|
||||||
|
impl<'a, 's, R> TextRenderer2<'a, 's, R>
|
||||||
|
where
|
||||||
|
R: Renderer<'s>,
|
||||||
|
{
|
||||||
|
pub fn new(target: &'a mut R) -> Self {
|
||||||
|
Self(target, core::marker::PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 's, R> LayoutSink for TextRenderer2<'a, 's, R>
|
||||||
|
where
|
||||||
|
R: Renderer<'s>,
|
||||||
|
{
|
||||||
|
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) {
|
||||||
|
shape::Text::new(cursor, text)
|
||||||
|
.with_font(layout.style.text_font)
|
||||||
|
.with_fg(layout.style.text_color)
|
||||||
|
.render(self.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||||
|
shape::Text::new(cursor, "-")
|
||||||
|
.with_font(layout.style.text_font)
|
||||||
|
.with_fg(layout.style.hyphen_color)
|
||||||
|
.render(self.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||||
|
if let Some((icon, margin)) = layout.style.ellipsis_icon {
|
||||||
|
let bottom_left = cursor + Offset::x(margin);
|
||||||
|
shape::ToifImage::new(bottom_left, icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
|
.with_fg(layout.style.ellipsis_color)
|
||||||
|
.render(self.0);
|
||||||
|
} else {
|
||||||
|
shape::Text::new(cursor, ELLIPSIS)
|
||||||
|
.with_font(layout.style.text_font)
|
||||||
|
.with_fg(layout.style.ellipsis_color)
|
||||||
|
.render(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_page_ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||||
|
if let Some((icon, _margin)) = layout.style.prev_page_ellipsis_icon {
|
||||||
|
shape::ToifImage::new(cursor, icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
|
.with_fg(layout.style.ellipsis_color)
|
||||||
|
.render(self.0);
|
||||||
|
} else {
|
||||||
|
shape::Text::new(cursor, ELLIPSIS)
|
||||||
|
.with_font(layout.style.text_font)
|
||||||
|
.with_fg(layout.style.ellipsis_color)
|
||||||
|
.render(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
pub mod trace {
|
pub mod trace {
|
||||||
use crate::{trace::ListTracer, ui::geometry::Point};
|
use crate::{trace::ListTracer, ui::geometry::Point};
|
||||||
|
@ -8,6 +8,8 @@ use crate::{
|
|||||||
geometry::{
|
geometry::{
|
||||||
Alignment, Alignment2D, Dimensions, Insets, LinearPlacement, Offset, Point, Rect,
|
Alignment, Alignment2D, Dimensions, Insets, LinearPlacement, Offset, Point, Rect,
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,6 +187,17 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
Self::foreach_visible(
|
||||||
|
&self.source,
|
||||||
|
&self.visible,
|
||||||
|
self.offset,
|
||||||
|
&mut |layout, content| {
|
||||||
|
layout.render_text2(content, target);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area);
|
sink(self.area);
|
||||||
@ -593,6 +606,19 @@ impl<T> Checklist<T> {
|
|||||||
layout.style.background_color,
|
layout.style.background_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_icon<'s>(
|
||||||
|
&self,
|
||||||
|
layout: &TextLayout,
|
||||||
|
icon: Icon,
|
||||||
|
offset: Offset,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
) {
|
||||||
|
let top_left = Point::new(self.area.x0, layout.bounds.y0);
|
||||||
|
shape::ToifImage::new(top_left + offset, icon.toif)
|
||||||
|
.with_fg(layout.style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Component for Checklist<T>
|
impl<'a, T> Component for Checklist<T>
|
||||||
@ -632,6 +658,28 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.paragraphs.render(target);
|
||||||
|
|
||||||
|
let current_visible = self.current.saturating_sub(self.paragraphs.offset.par);
|
||||||
|
for layout in self.paragraphs.visible.iter().take(current_visible) {
|
||||||
|
self.render_icon(
|
||||||
|
&layout.layout(&self.paragraphs.source),
|
||||||
|
self.icon_done,
|
||||||
|
self.done_offset,
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) {
|
||||||
|
self.render_icon(
|
||||||
|
&layout.layout(&self.paragraphs.source),
|
||||||
|
self.icon_current,
|
||||||
|
self.current_offset,
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area);
|
sink(self.area);
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
display::{Color, Font},
|
display::{Color, Font},
|
||||||
geometry::{Alignment, Rect},
|
geometry::{Alignment, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,6 +38,33 @@ pub fn text_multiline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws longer multiline texts inside an area.
|
||||||
|
/// Splits lines on word boundaries/whitespace.
|
||||||
|
/// When a word is too long to fit one line, splitting
|
||||||
|
/// it on multiple lines with "-" at the line-ends.
|
||||||
|
///
|
||||||
|
/// If it fits, returns the rest of the area.
|
||||||
|
/// If it does not fit, returns `None`.
|
||||||
|
pub fn text_multiline2<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
text: TString<'_>,
|
||||||
|
font: Font,
|
||||||
|
fg_color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
alignment: Alignment,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
|
||||||
|
let text_layout = TextLayout::new(text_style)
|
||||||
|
.with_bounds(area)
|
||||||
|
.with_align(alignment);
|
||||||
|
let layout_fit = text.map(|t| text_layout.render_text2(t, target));
|
||||||
|
match layout_fit {
|
||||||
|
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
|
||||||
|
LayoutFit::OutOfBounds { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Same as `text_multiline` above, but aligns the text to the bottom of the
|
/// Same as `text_multiline` above, but aligns the text to the bottom of the
|
||||||
/// area.
|
/// area.
|
||||||
pub fn text_multiline_bottom(
|
pub fn text_multiline_bottom(
|
||||||
@ -66,3 +94,34 @@ pub fn text_multiline_bottom(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as `text_multiline` above, but aligns the text to the bottom of the
|
||||||
|
/// area.
|
||||||
|
pub fn text_multiline_bottom2<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
text: TString<'_>,
|
||||||
|
font: Font,
|
||||||
|
fg_color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
alignment: Alignment,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
|
||||||
|
let mut text_layout = TextLayout::new(text_style)
|
||||||
|
.with_bounds(area)
|
||||||
|
.with_align(alignment);
|
||||||
|
// When text fits the area, displaying it in the bottom part.
|
||||||
|
// When not, render it "normally".
|
||||||
|
text.map(|t| match text_layout.fit_text(t) {
|
||||||
|
LayoutFit::Fitting { height, .. } => {
|
||||||
|
let (top, bottom) = area.split_bottom(height);
|
||||||
|
text_layout = text_layout.with_bounds(bottom);
|
||||||
|
text_layout.render_text2(t, target);
|
||||||
|
Some(top)
|
||||||
|
}
|
||||||
|
LayoutFit::OutOfBounds { .. } => {
|
||||||
|
text_layout.render_text2(t, target);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, TimerToken},
|
component::{Component, Event, EventCtx, TimerToken},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,6 +45,8 @@ impl Component for Timeout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {}
|
fn paint(&mut self) {}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -24,6 +24,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{display::Color, shape::render_on_display};
|
||||||
|
|
||||||
#[cfg(feature = "button")]
|
#[cfg(feature = "button")]
|
||||||
use crate::ui::event::ButtonEvent;
|
use crate::ui::event::ButtonEvent;
|
||||||
#[cfg(feature = "touch")]
|
#[cfg(feature = "touch")]
|
||||||
@ -69,11 +72,26 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn obj_paint(&mut self) -> bool {
|
fn obj_paint(&mut self) -> bool {
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
|
{
|
||||||
let will_paint = self.inner().will_paint();
|
let will_paint = self.inner().will_paint();
|
||||||
self.paint();
|
self.paint();
|
||||||
will_paint
|
will_paint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
{
|
||||||
|
let will_paint = self.inner().will_paint();
|
||||||
|
if will_paint {
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
self.render(target);
|
||||||
|
});
|
||||||
|
self.skip_paint();
|
||||||
|
}
|
||||||
|
will_paint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn obj_bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn obj_bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.bounds(sink)
|
self.bounds(sink)
|
||||||
|
@ -12,8 +12,12 @@ use crate::ui::{
|
|||||||
ui_features::ModelUI,
|
ui_features::ModelUI,
|
||||||
UIFeaturesCommon,
|
UIFeaturesCommon,
|
||||||
};
|
};
|
||||||
|
|
||||||
use num_traits::ToPrimitive;
|
use num_traits::ToPrimitive;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{display::color::Color, shape::render_on_display};
|
||||||
|
|
||||||
pub trait ReturnToC {
|
pub trait ReturnToC {
|
||||||
fn return_to_c(self) -> u32;
|
fn return_to_c(self) -> u32;
|
||||||
}
|
}
|
||||||
@ -63,16 +67,28 @@ fn touch_eval() -> Option<TouchEvent> {
|
|||||||
TouchEvent::new(event_type, ex as _, ey as _).ok()
|
TouchEvent::new(event_type, ex as _, ey as _).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run<F>(frame: &mut F) -> u32
|
fn render(frame: &mut impl Component) {
|
||||||
where
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
F: Component,
|
|
||||||
F::Msg: ReturnToC,
|
|
||||||
{
|
{
|
||||||
frame.place(ModelUI::SCREEN);
|
|
||||||
ModelUI::fadeout();
|
|
||||||
display::sync();
|
display::sync();
|
||||||
frame.paint();
|
frame.paint();
|
||||||
display::refresh();
|
display::refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
{
|
||||||
|
display::sync();
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
frame.render(target);
|
||||||
|
});
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(frame: &mut impl Component<Msg = impl ReturnToC>) -> u32 {
|
||||||
|
frame.place(ModelUI::SCREEN);
|
||||||
|
ModelUI::fadeout();
|
||||||
|
render(frame);
|
||||||
ModelUI::fadein();
|
ModelUI::fadein();
|
||||||
|
|
||||||
#[cfg(feature = "button")]
|
#[cfg(feature = "button")]
|
||||||
@ -93,24 +109,20 @@ where
|
|||||||
if let Some(message) = msg {
|
if let Some(message) = msg {
|
||||||
return message.return_to_c();
|
return message.return_to_c();
|
||||||
}
|
}
|
||||||
display::sync();
|
render(frame);
|
||||||
frame.paint();
|
|
||||||
display::refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show<F>(frame: &mut F, fading: bool)
|
pub fn show(frame: &mut impl Component, fading: bool) {
|
||||||
where
|
|
||||||
F: Component,
|
|
||||||
{
|
|
||||||
frame.place(ModelUI::SCREEN);
|
frame.place(ModelUI::SCREEN);
|
||||||
|
|
||||||
if fading {
|
if fading {
|
||||||
ModelUI::fadeout()
|
ModelUI::fadeout()
|
||||||
};
|
};
|
||||||
display::sync();
|
|
||||||
frame.paint();
|
render(frame);
|
||||||
display::refresh();
|
|
||||||
if fading {
|
if fading {
|
||||||
ModelUI::fadein()
|
ModelUI::fadein()
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
io::BinaryData,
|
||||||
micropython::{
|
micropython::{
|
||||||
buffer::{hexlify_bytes, StrBuffer},
|
buffer::{hexlify_bytes, StrBuffer},
|
||||||
gc::Gc,
|
gc::Gc,
|
||||||
@ -167,10 +168,10 @@ pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj {
|
|||||||
unsafe { try_or_raise(block) }
|
unsafe { try_or_raise(block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user_custom_image() -> Result<Gc<[u8]>, Error> {
|
pub fn get_user_custom_image() -> Result<BinaryData<'static>, Error> {
|
||||||
let len = get_avatar_len()?;
|
let len = get_avatar_len()?;
|
||||||
let mut data = Gc::<[u8]>::new_slice(len)?;
|
let mut data = Gc::<[u8]>::new_slice(len)?;
|
||||||
// SAFETY: buffer is freshly allocated so nobody else has it.
|
// SAFETY: buffer is freshly allocated so nobody else has it.
|
||||||
load_avatar(unsafe { Gc::<[u8]>::as_mut(&mut data) })?;
|
load_avatar(unsafe { Gc::<[u8]>::as_mut(&mut data) })?;
|
||||||
Ok(data)
|
Ok(data.into())
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ use crate::{
|
|||||||
constant::screen,
|
constant::screen,
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::{Alignment, Insets, Point, Rect},
|
geometry::{Alignment, Insets, Point, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,6 +107,15 @@ impl<'a> Component for Intro<'a> {
|
|||||||
self.menu.paint();
|
self.menu.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.title.render(target);
|
||||||
|
self.text.render(target);
|
||||||
|
self.warn.render(target);
|
||||||
|
self.host.render(target);
|
||||||
|
self.menu.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.menu.bounds(sink);
|
self.menu.bounds(sink);
|
||||||
|
@ -5,6 +5,7 @@ use crate::{
|
|||||||
constant::{screen, WIDTH},
|
constant::{screen, WIDTH},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::{Insets, Point, Rect},
|
geometry::{Insets, Point, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,6 +109,14 @@ impl Component for Menu {
|
|||||||
self.reset.paint();
|
self.reset.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.title.render(target);
|
||||||
|
self.close.render(target);
|
||||||
|
self.reboot.render(target);
|
||||||
|
self.reset.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.close.bounds(sink);
|
self.close.bounds(sink);
|
||||||
|
@ -23,12 +23,30 @@ use super::{
|
|||||||
FIRE40, RESULT_FW_INSTALL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL, TEXT_WIPE_BOLD,
|
FIRE40, RESULT_FW_INSTALL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL, TEXT_WIPE_BOLD,
|
||||||
TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
|
TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
|
||||||
},
|
},
|
||||||
BACKLIGHT_NORMAL, BLACK, GREEN_LIGHT, GREY, WHITE,
|
BACKLIGHT_NORMAL, GREEN_LIGHT, GREY,
|
||||||
},
|
},
|
||||||
ModelMercuryFeatures,
|
ModelMercuryFeatures,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ui::{ui_features::UIFeaturesBootloader, UIFeaturesCommon};
|
use crate::ui::{ui_features::UIFeaturesBootloader, UIFeaturesCommon};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
|
use super::theme::BLACK;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{
|
||||||
|
constant,
|
||||||
|
display::toif::Toif,
|
||||||
|
geometry::{Alignment, Alignment2D},
|
||||||
|
shape,
|
||||||
|
shape::render_on_display,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use ufmt::uwrite;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use super::theme::bootloader::BLD_WARN_COLOR;
|
||||||
use intro::Intro;
|
use intro::Intro;
|
||||||
use menu::Menu;
|
use menu::Menu;
|
||||||
|
|
||||||
@ -44,6 +62,7 @@ const SCREEN: Rect = ModelMercuryFeatures::SCREEN;
|
|||||||
const PROGRESS_TEXT_ORIGIN: Point = Point::new(2, 28);
|
const PROGRESS_TEXT_ORIGIN: Point = Point::new(2, 28);
|
||||||
|
|
||||||
impl ModelMercuryFeatures {
|
impl ModelMercuryFeatures {
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn screen_progress(
|
fn screen_progress(
|
||||||
text: &str,
|
text: &str,
|
||||||
progress: u16,
|
progress: u16,
|
||||||
@ -77,6 +96,70 @@ impl ModelMercuryFeatures {
|
|||||||
Self::fadein();
|
Self::fadein();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_progress(
|
||||||
|
text: &str,
|
||||||
|
progress: u16,
|
||||||
|
initialize: bool,
|
||||||
|
fg_color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
icon: Option<(Icon, Color)>,
|
||||||
|
center_text: Option<&str>,
|
||||||
|
) {
|
||||||
|
if initialize {
|
||||||
|
Self::fadeout();
|
||||||
|
}
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(bg_color), |target| {
|
||||||
|
shape::Text::new(PROGRESS_TEXT_ORIGIN, text)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let loader_offset: i16 = 19;
|
||||||
|
let center_text_offset: i16 = 10;
|
||||||
|
let center = SCREEN.center() + Offset::y(loader_offset);
|
||||||
|
let inactive_color = bg_color.blend(fg_color, 85);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(inactive_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(fg_color)
|
||||||
|
.with_end_angle(((progress as i32 * shape::PI4 as i32 * 8) / 1000) as i16)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_INNER + 2)
|
||||||
|
.with_bg(bg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
if let Some((icon, color)) = icon {
|
||||||
|
shape::ToifImage::new(center, icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(center_text) = center_text {
|
||||||
|
shape::Text::new(
|
||||||
|
SCREEN.center() + Offset::y(loader_offset + center_text_offset),
|
||||||
|
center_text,
|
||||||
|
)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
if initialize {
|
||||||
|
Self::fadein();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UIFeaturesBootloader for ModelMercuryFeatures {
|
impl UIFeaturesBootloader for ModelMercuryFeatures {
|
||||||
@ -85,12 +168,13 @@ impl UIFeaturesBootloader for ModelMercuryFeatures {
|
|||||||
show(&mut frame, true);
|
show(&mut frame, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn bld_continue_label(bg_color: Color) {
|
fn bld_continue_label(bg_color: Color) {
|
||||||
display::text_center(
|
display::text_center(
|
||||||
Point::new(SCREEN.width() / 2, SCREEN.height() - 5),
|
Point::new(SCREEN.width() / 2, SCREEN.height() - 5),
|
||||||
"click to continue ...",
|
"click to continue ...",
|
||||||
Font::NORMAL,
|
Font::NORMAL,
|
||||||
WHITE,
|
BLD_FG,
|
||||||
bg_color,
|
bg_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,8 +211,6 @@ impl UIFeaturesBootloader for ModelMercuryFeatures {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
display::refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screen_install_fail() {
|
fn screen_install_fail() {
|
||||||
@ -252,6 +334,7 @@ impl UIFeaturesBootloader for ModelMercuryFeatures {
|
|||||||
Self::fadeout();
|
Self::fadeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
display::rect_fill(SCREEN, BLACK);
|
display::rect_fill(SCREEN, BLACK);
|
||||||
|
|
||||||
let mut frame = WelcomeScreen::new();
|
let mut frame = WelcomeScreen::new();
|
||||||
@ -262,7 +345,6 @@ impl UIFeaturesBootloader for ModelMercuryFeatures {
|
|||||||
} else {
|
} else {
|
||||||
display::set_backlight(BACKLIGHT_NORMAL);
|
display::set_backlight(BACKLIGHT_NORMAL);
|
||||||
}
|
}
|
||||||
display::refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screen_wipe_progress(progress: u16, initialize: bool) {
|
fn screen_wipe_progress(progress: u16, initialize: bool) {
|
||||||
@ -322,4 +404,89 @@ impl UIFeaturesBootloader for ModelMercuryFeatures {
|
|||||||
);
|
);
|
||||||
show(&mut frame, true);
|
show(&mut frame, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_boot(
|
||||||
|
warning: bool,
|
||||||
|
vendor_str: Option<&str>,
|
||||||
|
version: [u8; 4],
|
||||||
|
vendor_img: &[u8],
|
||||||
|
wait: i32,
|
||||||
|
) {
|
||||||
|
let bg_color = if warning { BLD_WARN_COLOR } else { BLD_BG };
|
||||||
|
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(bg_color), |target| {
|
||||||
|
// Draw vendor image if it's valid and has size of 120x120
|
||||||
|
if let Ok(toif) = Toif::new(vendor_img) {
|
||||||
|
if (toif.width() == 120) && (toif.height() == 120) {
|
||||||
|
// Image position depends on the vendor string presence
|
||||||
|
let pos = if vendor_str.is_some() {
|
||||||
|
Point::new(SCREEN.width() / 2, 30)
|
||||||
|
} else {
|
||||||
|
Point::new(SCREEN.width() / 2, 60)
|
||||||
|
};
|
||||||
|
|
||||||
|
shape::ToifImage::new(pos, toif)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw vendor string if present
|
||||||
|
if let Some(text) = vendor_str {
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 50);
|
||||||
|
shape::Text::new(pos, text)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG) //COLOR_BL_BG
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 25);
|
||||||
|
|
||||||
|
let mut version_text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(
|
||||||
|
version_text,
|
||||||
|
"{}.{}.{}",
|
||||||
|
version[0],
|
||||||
|
version[1],
|
||||||
|
version[2]
|
||||||
|
));
|
||||||
|
|
||||||
|
shape::Text::new(pos, version_text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a message
|
||||||
|
match wait.cmp(&0) {
|
||||||
|
core::cmp::Ordering::Equal => {}
|
||||||
|
core::cmp::Ordering::Greater => {
|
||||||
|
let mut text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(text, "starting in {} s", wait));
|
||||||
|
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
|
||||||
|
shape::Text::new(pos, text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
core::cmp::Ordering::Less => {
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
|
||||||
|
shape::Text::new(pos, "click to continue ...")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ use crate::ui::{
|
|||||||
constant::screen,
|
constant::screen,
|
||||||
display::{self, Font},
|
display::{self, Font},
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::theme::{BLACK, GREY, WHITE};
|
use super::super::theme::{BLACK, GREY, WHITE};
|
||||||
@ -61,4 +63,33 @@ impl Component for Welcome {
|
|||||||
BLACK,
|
BLACK,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(TEXT_ORIGIN, "Get started")
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(TEXT_ORIGIN + Offset::y(STRIDE), "with your Trezor")
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(TEXT_ORIGIN + Offset::y(2 * STRIDE), "at")
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let at_width = Font::NORMAL.text_width("at ");
|
||||||
|
|
||||||
|
shape::Text::new(
|
||||||
|
TEXT_ORIGIN + Offset::new(at_width, 2 * STRIDE),
|
||||||
|
"trezor.io/start",
|
||||||
|
)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ use crate::{
|
|||||||
constant::screen,
|
constant::screen,
|
||||||
display::{Color, Icon},
|
display::{Color, Icon},
|
||||||
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -239,6 +241,40 @@ impl Component for Confirm<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.content_pad.render(target);
|
||||||
|
|
||||||
|
if let Some(info) = self.info.as_ref() {
|
||||||
|
if self.show_info {
|
||||||
|
info.close_button.render(target);
|
||||||
|
info.title.render(target);
|
||||||
|
info.text.render(target);
|
||||||
|
self.left_button.render(target);
|
||||||
|
self.right_button.render(target);
|
||||||
|
// short-circuit before painting the main components
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
info.info_button.render(target);
|
||||||
|
// pass through to the rest of the paint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.message.render(target);
|
||||||
|
self.alert.render(target);
|
||||||
|
self.left_button.render(target);
|
||||||
|
self.right_button.render(target);
|
||||||
|
match &self.title {
|
||||||
|
ConfirmTitle::Text(label) => label.render(target),
|
||||||
|
ConfirmTitle::Icon(icon) => {
|
||||||
|
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.left_button.bounds(sink);
|
self.left_button.bounds(sink);
|
||||||
|
@ -8,6 +8,8 @@ use crate::{
|
|||||||
display::{self, toif::Icon, Color, Font},
|
display::{self, toif::Icon, Color, Font},
|
||||||
event::TouchEvent,
|
event::TouchEvent,
|
||||||
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,6 +187,18 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::IconBlend(_, _, _) => {}
|
||||||
|
_ => shape::Bar::new(self.area)
|
||||||
|
.with_bg(style.button_color)
|
||||||
|
.with_fg(style.border_color)
|
||||||
|
.with_thickness(style.border_width)
|
||||||
|
.with_radius(style.border_radius as i16)
|
||||||
|
.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paint_content(&self, style: &ButtonStyle) {
|
pub fn paint_content(&self, style: &ButtonStyle) {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ButtonContent::Empty => {}
|
ButtonContent::Empty => {}
|
||||||
@ -223,6 +237,45 @@ impl Button {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::Empty => {}
|
||||||
|
ButtonContent::Text(text) => {
|
||||||
|
let width = text.map(|c| style.font.text_width(c));
|
||||||
|
let height = style.font.text_height();
|
||||||
|
let start_of_baseline = self.area.center()
|
||||||
|
+ Offset::new(-width / 2, height / 2)
|
||||||
|
+ Offset::y(Self::BASELINE_OFFSET);
|
||||||
|
text.map(|text| {
|
||||||
|
shape::Text::new(start_of_baseline, text)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ButtonContent::Icon(icon) => {
|
||||||
|
shape::ToifImage::new(self.area.center(), icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
ButtonContent::IconAndText(child) => {
|
||||||
|
child.render(target, self.area, self.style(), Self::BASELINE_OFFSET);
|
||||||
|
}
|
||||||
|
ButtonContent::IconBlend(bg, fg, offset) => {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_bg(style.background_color)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(self.area.top_left(), bg.toif)
|
||||||
|
.with_fg(style.button_color)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Button {
|
impl Component for Button {
|
||||||
@ -311,6 +364,12 @@ impl Component for Button {
|
|||||||
self.paint_content(style);
|
self.paint_content(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let style = self.style();
|
||||||
|
self.render_background(target, style);
|
||||||
|
self.render_content(target, style);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area);
|
sink(self.area);
|
||||||
@ -437,4 +496,52 @@ impl IconText {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(
|
||||||
|
&self,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
baseline_offset: i16,
|
||||||
|
) {
|
||||||
|
let width = style.font.text_width(self.text);
|
||||||
|
let height = style.font.text_height();
|
||||||
|
|
||||||
|
let mut use_icon = false;
|
||||||
|
let mut use_text = false;
|
||||||
|
|
||||||
|
let mut icon_pos = Point::new(
|
||||||
|
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
|
||||||
|
area.center().y,
|
||||||
|
);
|
||||||
|
let mut text_pos =
|
||||||
|
area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset);
|
||||||
|
|
||||||
|
if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) {
|
||||||
|
//display both icon and text
|
||||||
|
text_pos = Point::new(area.top_left().x + Self::ICON_SPACE, text_pos.y);
|
||||||
|
use_text = true;
|
||||||
|
use_icon = true;
|
||||||
|
} else if area.width() > (width + Self::TEXT_MARGIN) {
|
||||||
|
use_text = true;
|
||||||
|
} else {
|
||||||
|
//if we can't fit the text, retreat to centering the icon
|
||||||
|
icon_pos = area.center();
|
||||||
|
use_icon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_text {
|
||||||
|
shape::Text::new(text_pos, self.text)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_icon {
|
||||||
|
shape::ToifImage::new(icon_pos, self.icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ use crate::{
|
|||||||
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
|
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
|
||||||
constant::screen,
|
constant::screen,
|
||||||
geometry::{Alignment2D, Point, Rect},
|
geometry::{Alignment2D, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,4 +91,19 @@ impl<'a> Component for ErrorScreen<'a> {
|
|||||||
self.message.paint();
|
self.message.paint();
|
||||||
self.footer.paint();
|
self.footer.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
let icon = ICON_WARNING40;
|
||||||
|
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.with_bg(FATAL_ERROR_COLOR)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.title.render(target);
|
||||||
|
self.message.render(target);
|
||||||
|
self.footer.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::{Alignment, Insets, Offset, Rect},
|
geometry::{Alignment, Insets, Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -175,6 +176,12 @@ where
|
|||||||
self.button.paint();
|
self.button.paint();
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
}
|
}
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.title.render(target);
|
||||||
|
self.subtitle.render(target);
|
||||||
|
self.button.render(target);
|
||||||
|
self.content.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
|
@ -6,7 +6,8 @@ use crate::{
|
|||||||
animation::Animation,
|
animation::Animation,
|
||||||
component::{Component, Event, EventCtx, Pad},
|
component::{Component, Event, EventCtx, Pad},
|
||||||
display::{self, toif::Icon, Color},
|
display::{self, toif::Icon, Color},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
shape::{self, Renderer},
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -205,6 +206,49 @@ impl Component for Loader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// TODO: Consider passing the current instant along with the event -- that way,
|
||||||
|
// we could synchronize painting across the component tree. Also could be useful
|
||||||
|
// in automated tests.
|
||||||
|
// In practice, taking the current instant here is more precise in case some
|
||||||
|
// other component in the tree takes a long time to draw.
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if let Some(progress) = self.progress(now) {
|
||||||
|
let style = if progress < display::LOADER_MAX {
|
||||||
|
self.styles.normal
|
||||||
|
} else {
|
||||||
|
self.styles.active
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pad.render(target);
|
||||||
|
|
||||||
|
let center = self.pad.area.center();
|
||||||
|
|
||||||
|
let inactive_color = Color::black().blend(style.loader_color, 85);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(inactive_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(style.loader_color)
|
||||||
|
.with_end_angle(((progress as i32 * shape::PI4 as i32 * 8) / 1000) as i16)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_INNER)
|
||||||
|
.with_bg(style.background_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
if let Some((icon, color)) = style.icon {
|
||||||
|
shape::ToifImage::new(center, icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LoaderStyleSheet {
|
pub struct LoaderStyleSheet {
|
||||||
|
@ -5,6 +5,8 @@ use crate::{
|
|||||||
constant::screen,
|
constant::screen,
|
||||||
display::{self, Color, Font, Icon},
|
display::{self, Color, Font, Icon},
|
||||||
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,6 +96,20 @@ impl Component for ResultFooter<'_> {
|
|||||||
// footer text
|
// footer text
|
||||||
self.text.paint();
|
self.text.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// divider line
|
||||||
|
let bar = Rect::from_center_and_size(
|
||||||
|
Point::new(self.area.center().x, self.area.y0),
|
||||||
|
Offset::new(self.area.width(), 1),
|
||||||
|
);
|
||||||
|
shape::Bar::new(bar)
|
||||||
|
.with_fg(self.style.divider_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
// footer text
|
||||||
|
self.text.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ResultScreen<'a> {
|
pub struct ResultScreen<'a> {
|
||||||
@ -165,4 +181,21 @@ impl<'a> Component for ResultScreen<'a> {
|
|||||||
self.message.paint();
|
self.message.paint();
|
||||||
self.footer.paint();
|
self.footer.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.footer_pad.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(
|
||||||
|
Point::new(screen().center().x, ICON_CENTER_Y),
|
||||||
|
self.icon.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(self.style.fg_color)
|
||||||
|
.with_bg(self.style.bg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.message.render(target);
|
||||||
|
self.footer.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx, Never},
|
component::{Component, Event, EventCtx, Never},
|
||||||
display,
|
display,
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
display::font::Font,
|
||||||
|
geometry::{Alignment, Alignment2D, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::theme;
|
use super::theme;
|
||||||
@ -10,7 +13,7 @@ const TEXT_BOTTOM_MARGIN: i16 = 54;
|
|||||||
const ICON_TOP_MARGIN: i16 = 48;
|
const ICON_TOP_MARGIN: i16 = 48;
|
||||||
#[cfg(not(feature = "bootloader"))]
|
#[cfg(not(feature = "bootloader"))]
|
||||||
const MODEL_NAME_FONT: display::Font = display::Font::DEMIBOLD;
|
const MODEL_NAME_FONT: display::Font = display::Font::DEMIBOLD;
|
||||||
#[cfg(not(feature = "bootloader"))]
|
|
||||||
use crate::trezorhal::model;
|
use crate::trezorhal::model;
|
||||||
|
|
||||||
pub struct WelcomeScreen {
|
pub struct WelcomeScreen {
|
||||||
@ -44,12 +47,32 @@ impl Component for WelcomeScreen {
|
|||||||
);
|
);
|
||||||
display::text_center(
|
display::text_center(
|
||||||
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
|
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
|
||||||
"Trezor Safe 5",
|
model::FULL_NAME,
|
||||||
display::Font::NORMAL,
|
display::Font::NORMAL,
|
||||||
theme::FG,
|
theme::FG,
|
||||||
theme::BG,
|
theme::BG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::ToifImage::new(
|
||||||
|
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
|
||||||
|
theme::ICON_LOGO.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(
|
||||||
|
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
|
||||||
|
model::FULL_NAME,
|
||||||
|
)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -5,17 +5,37 @@ use super::{
|
|||||||
constant,
|
constant,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{display::Color, shape::render_on_display};
|
||||||
|
|
||||||
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
|
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
|
||||||
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
|
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
|
||||||
frame.place(constant::screen());
|
frame.place(constant::screen());
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
frame.render(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
frame.paint();
|
frame.paint();
|
||||||
|
|
||||||
display::refresh();
|
display::refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn screen_boot_stage_2() {
|
pub fn screen_boot_stage_2() {
|
||||||
let mut frame = WelcomeScreen::new();
|
let mut frame = WelcomeScreen::new();
|
||||||
frame.place(screen());
|
frame.place(screen());
|
||||||
|
|
||||||
display::sync();
|
display::sync();
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
frame.render(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
frame.paint();
|
frame.paint();
|
||||||
|
|
||||||
display::refresh();
|
display::refresh();
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ use crate::{
|
|||||||
component::{Child, Component, Event, EventCtx, Label, Pad},
|
component::{Child, Component, Event, EventCtx, Label, Pad},
|
||||||
geometry::{Alignment, Alignment2D, Rect},
|
geometry::{Alignment, Alignment2D, Rect},
|
||||||
layout::simplified::ReturnToC,
|
layout::simplified::ReturnToC,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,6 +106,27 @@ impl<'a> Component for Intro<'a> {
|
|||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.title.render(target);
|
||||||
|
|
||||||
|
let area = self.bg.area;
|
||||||
|
|
||||||
|
shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif)
|
||||||
|
.with_align(Alignment2D::TOP_RIGHT)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.warn.render(target);
|
||||||
|
self.text.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.title.bounds(sink);
|
self.title.bounds(sink);
|
||||||
|
@ -7,8 +7,10 @@ use crate::{
|
|||||||
constant::screen,
|
constant::screen,
|
||||||
display,
|
display,
|
||||||
display::{Font, Icon},
|
display::{Font, Icon},
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||||
layout::simplified::ReturnToC,
|
layout::simplified::ReturnToC,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,6 +71,26 @@ impl Choice for MenuChoice {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool) {
|
||||||
|
// Icon on top and two lines of text below
|
||||||
|
shape::ToifImage::new(SCREEN_CENTER + Offset::y(-20), self.icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(SCREEN_CENTER, self.first_line)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(SCREEN_CENTER + Offset::y(10), self.second_line)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
fn btn_layout(&self) -> ButtonLayout {
|
fn btn_layout(&self) -> ButtonLayout {
|
||||||
ButtonLayout::arrow_armed_arrow("SELECT".into())
|
ButtonLayout::arrow_armed_arrow("SELECT".into())
|
||||||
}
|
}
|
||||||
@ -162,6 +184,11 @@ impl Component for Menu {
|
|||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.choice_page.bounds(sink)
|
self.choice_page.bounds(sink)
|
||||||
|
@ -7,7 +7,7 @@ use crate::{
|
|||||||
constant,
|
constant,
|
||||||
constant::{HEIGHT, SCREEN},
|
constant::{HEIGHT, SCREEN},
|
||||||
display::{self, Color, Font, Icon},
|
display::{self, Color, Font, Icon},
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point},
|
||||||
layout::simplified::{run, show, ReturnToC},
|
layout::simplified::{run, show, ReturnToC},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -19,11 +19,22 @@ use super::{
|
|||||||
},
|
},
|
||||||
theme::{
|
theme::{
|
||||||
bootloader::{BLD_BG, BLD_FG, ICON_ALERT, ICON_SPINNER, ICON_SUCCESS},
|
bootloader::{BLD_BG, BLD_FG, ICON_ALERT, ICON_SPINNER, ICON_SUCCESS},
|
||||||
ICON_ARM_LEFT, ICON_ARM_RIGHT, TEXT_BOLD, TEXT_NORMAL, WHITE,
|
ICON_ARM_LEFT, ICON_ARM_RIGHT, TEXT_BOLD, TEXT_NORMAL,
|
||||||
},
|
},
|
||||||
ModelTRFeatures,
|
ModelTRFeatures,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
|
use crate::ui::geometry::Rect;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{
|
||||||
|
display::toif::Toif, geometry::Alignment, model_tr::cshape, shape, shape::render_on_display,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use ufmt::uwrite;
|
||||||
|
|
||||||
mod intro;
|
mod intro;
|
||||||
mod menu;
|
mod menu;
|
||||||
mod welcome;
|
mod welcome;
|
||||||
@ -42,6 +53,7 @@ impl ReturnToC for ConfirmMsg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ModelTRFeatures {
|
impl ModelTRFeatures {
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn screen_progress(
|
fn screen_progress(
|
||||||
text: &str,
|
text: &str,
|
||||||
text2: &str,
|
text2: &str,
|
||||||
@ -84,6 +96,50 @@ impl ModelTRFeatures {
|
|||||||
|
|
||||||
display::refresh();
|
display::refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_progress(
|
||||||
|
text: &str,
|
||||||
|
text2: &str,
|
||||||
|
progress: u16,
|
||||||
|
_initialize: bool,
|
||||||
|
fg_color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
icon: Option<(Icon, Color)>,
|
||||||
|
) {
|
||||||
|
let progress = if progress < 20 { 20 } else { progress };
|
||||||
|
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(bg_color), |target| {
|
||||||
|
let center = SCREEN.top_center() + Offset::y(12);
|
||||||
|
|
||||||
|
cshape::LoaderCircular::new(center, progress)
|
||||||
|
.with_color(fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
if let Some((icon, color)) = icon {
|
||||||
|
shape::ToifImage::new(center, icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
shape::Text::new(SCREEN.center() + Offset::y(8), text)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::BOLD)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(SCREEN.center() + Offset::y(20), text2)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::BOLD)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UIFeaturesBootloader for ModelTRFeatures {
|
impl UIFeaturesBootloader for ModelTRFeatures {
|
||||||
@ -92,24 +148,25 @@ impl UIFeaturesBootloader for ModelTRFeatures {
|
|||||||
show(&mut frame, true);
|
show(&mut frame, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn bld_continue_label(bg_color: Color) {
|
fn bld_continue_label(bg_color: Color) {
|
||||||
display::text_center(
|
display::text_center(
|
||||||
Point::new(constant::WIDTH / 2, HEIGHT - 2),
|
Point::new(constant::WIDTH / 2, HEIGHT - 2),
|
||||||
"CONTINUE",
|
"CONTINUE",
|
||||||
Font::NORMAL,
|
Font::NORMAL,
|
||||||
WHITE,
|
BLD_FG,
|
||||||
bg_color,
|
bg_color,
|
||||||
);
|
);
|
||||||
ICON_ARM_LEFT.draw(
|
ICON_ARM_LEFT.draw(
|
||||||
Point::new(constant::WIDTH / 2 - 36, HEIGHT - 6),
|
Point::new(constant::WIDTH / 2 - 36, HEIGHT - 6),
|
||||||
Alignment2D::TOP_LEFT,
|
Alignment2D::TOP_LEFT,
|
||||||
WHITE,
|
BLD_FG,
|
||||||
bg_color,
|
bg_color,
|
||||||
);
|
);
|
||||||
ICON_ARM_RIGHT.draw(
|
ICON_ARM_RIGHT.draw(
|
||||||
Point::new(constant::WIDTH / 2 + 25, HEIGHT - 6),
|
Point::new(constant::WIDTH / 2 + 25, HEIGHT - 6),
|
||||||
Alignment2D::TOP_LEFT,
|
Alignment2D::TOP_LEFT,
|
||||||
WHITE,
|
BLD_FG,
|
||||||
bg_color,
|
bg_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -261,6 +318,7 @@ impl UIFeaturesBootloader for ModelTRFeatures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn screen_boot_stage_1(_fading: bool) {
|
fn screen_boot_stage_1(_fading: bool) {
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
display::rect_fill(SCREEN, BLD_BG);
|
display::rect_fill(SCREEN, BLD_BG);
|
||||||
|
|
||||||
let mut frame = WelcomeScreen::new(true);
|
let mut frame = WelcomeScreen::new(true);
|
||||||
@ -315,4 +373,92 @@ impl UIFeaturesBootloader for ModelTRFeatures {
|
|||||||
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_ALERT, title, content, true);
|
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_ALERT, title, content, true);
|
||||||
show(&mut frame, false);
|
show(&mut frame, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_boot(
|
||||||
|
_warning: bool,
|
||||||
|
vendor_str: Option<&str>,
|
||||||
|
version: [u8; 4],
|
||||||
|
vendor_img: &[u8],
|
||||||
|
wait: i32,
|
||||||
|
) {
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(BLD_BG), |target| {
|
||||||
|
// Draw vendor image if it's valid and has size of 24x24
|
||||||
|
if let Ok(toif) = Toif::new(vendor_img) {
|
||||||
|
if (toif.width() == 24) && (toif.height() == 24) {
|
||||||
|
let pos = Point::new((constant::WIDTH - 22) / 2, 0);
|
||||||
|
shape::ToifImage::new(pos, toif)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw vendor string if present
|
||||||
|
if let Some(text) = vendor_str {
|
||||||
|
let pos = Point::new(constant::WIDTH / 2, 36);
|
||||||
|
shape::Text::new(pos, text)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG) //COLOR_BL_BG
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let pos = Point::new(constant::WIDTH / 2, 46);
|
||||||
|
|
||||||
|
let mut version_text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(
|
||||||
|
version_text,
|
||||||
|
"{}.{}.{}",
|
||||||
|
version[0],
|
||||||
|
version[1],
|
||||||
|
version[2]
|
||||||
|
));
|
||||||
|
|
||||||
|
shape::Text::new(pos, version_text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a message
|
||||||
|
match wait.cmp(&0) {
|
||||||
|
core::cmp::Ordering::Equal => {}
|
||||||
|
core::cmp::Ordering::Greater => {
|
||||||
|
let mut text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(text, "starting in {} s", wait));
|
||||||
|
|
||||||
|
let pos = Point::new(constant::WIDTH / 2, HEIGHT - 5);
|
||||||
|
shape::Text::new(pos, text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
core::cmp::Ordering::Less => {
|
||||||
|
let pos = Point::new(constant::WIDTH / 2, HEIGHT - 2);
|
||||||
|
shape::Text::new(pos, "CONTINUE")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let pos = Point::new(constant::WIDTH / 2 - 36, HEIGHT - 6);
|
||||||
|
shape::ToifImage::new(pos, ICON_ARM_LEFT.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let pos = Point::new(constant::WIDTH / 2 + 25, HEIGHT - 6);
|
||||||
|
shape::ToifImage::new(pos, ICON_ARM_RIGHT.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx, Never, Pad},
|
component::{Component, Event, EventCtx, Never, Pad},
|
||||||
display::{self, Font},
|
display::{self, Font},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::theme::bootloader::{BLD_BG, BLD_FG};
|
use super::super::theme::bootloader::{BLD_BG, BLD_FG};
|
||||||
@ -57,4 +59,28 @@ impl Component for Welcome {
|
|||||||
BLD_BG,
|
BLD_BG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
let top_center = self.bg.area.top_center();
|
||||||
|
|
||||||
|
shape::Text::new(top_center + Offset::y(24), "Get started with")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(top_center + Offset::y(32), "your Trezor at")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(top_center + Offset::y(48), "trezor.io/start")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::BOLD)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
|
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
|
||||||
},
|
},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -259,6 +260,16 @@ impl Component for AddressDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
match self.current_page {
|
||||||
|
0 => self.qr_code.render(target),
|
||||||
|
1 => self.details_view.render(target),
|
||||||
|
_ => self.xpub_view.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area)
|
sink(self.area)
|
||||||
|
@ -4,6 +4,8 @@ use crate::{
|
|||||||
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
|
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
|
||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
geometry::{Point, Rect},
|
geometry::{Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -216,6 +218,32 @@ impl Component for Confirm<'_> {
|
|||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
let mut display_top_left = |text: TString| {
|
||||||
|
text.map(|t| {
|
||||||
|
shape::Text::new(Point::zero(), t)
|
||||||
|
.with_font(Font::BOLD)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// We are either on the info screen or on the "main" screen
|
||||||
|
if self.showing_info_screen {
|
||||||
|
if let Some(title) = self.info_title {
|
||||||
|
display_top_left(title);
|
||||||
|
}
|
||||||
|
self.info_text.render(target);
|
||||||
|
} else {
|
||||||
|
display_top_left(self.title);
|
||||||
|
self.message.render(target);
|
||||||
|
self.alert.render(target);
|
||||||
|
}
|
||||||
|
self.buttons.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.buttons.bounds(sink);
|
self.buttons.bounds(sink);
|
||||||
|
@ -7,6 +7,8 @@ use crate::{
|
|||||||
display::{self, Color, Font, Icon},
|
display::{self, Color, Font, Icon},
|
||||||
event::PhysicalButton,
|
event::PhysicalButton,
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -260,6 +262,88 @@ impl Component for Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let style = self.style();
|
||||||
|
let fg_color = style.text_color;
|
||||||
|
let bg_color = fg_color.negate();
|
||||||
|
let area = self.get_current_area();
|
||||||
|
let inversed_colors = bg_color != theme::BG;
|
||||||
|
|
||||||
|
// Filling the background (with 2-pixel rounding when applicable)
|
||||||
|
if inversed_colors {
|
||||||
|
shape::Bar::new(area)
|
||||||
|
.with_radius(3)
|
||||||
|
.with_bg(bg_color)
|
||||||
|
.render(target);
|
||||||
|
} else if style.with_outline {
|
||||||
|
shape::Bar::new(area)
|
||||||
|
.with_radius(3)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
shape::Bar::new(area).with_bg(bg_color).render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally display "arms" at both sides of content - always in FG and BG
|
||||||
|
// colors (they are not inverted).
|
||||||
|
if style.with_arms {
|
||||||
|
shape::ToifImage::new(area.left_center(), theme::ICON_ARM_LEFT.toif)
|
||||||
|
.with_align(Alignment2D::TOP_RIGHT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(area.right_center(), theme::ICON_ARM_RIGHT.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Painting the content
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::Text(text) => text.map(|t| {
|
||||||
|
shape::Text::new(
|
||||||
|
self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)),
|
||||||
|
t,
|
||||||
|
)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
}),
|
||||||
|
ButtonContent::Icon(icon) => {
|
||||||
|
// Allowing for possible offset of the area from current style
|
||||||
|
let icon_area = area.translate(style.offset);
|
||||||
|
if style.with_outline {
|
||||||
|
shape::ToifImage::new(icon_area.center(), icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
// Positioning the icon in the corresponding corner/center
|
||||||
|
match self.pos {
|
||||||
|
ButtonPos::Left => {
|
||||||
|
shape::ToifImage::new(icon_area.bottom_left(), icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonPos::Right => {
|
||||||
|
shape::ToifImage::new(icon_area.bottom_right(), icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_RIGHT)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonPos::Middle => shape::ToifImage::new(icon_area.center(), icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
component::{base::Event, Component, EventCtx, Pad, TimerToken},
|
component::{base::Event, Component, EventCtx, Pad, TimerToken},
|
||||||
event::{ButtonEvent, PhysicalButton},
|
event::{ButtonEvent, PhysicalButton},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,6 +94,18 @@ impl ButtonType {
|
|||||||
Self::Nothing => {}
|
Self::Nothing => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
match self {
|
||||||
|
Self::Button(button) => {
|
||||||
|
button.render(target);
|
||||||
|
}
|
||||||
|
Self::HoldToConfirm(htc) => {
|
||||||
|
htc.render(target);
|
||||||
|
}
|
||||||
|
Self::Nothing => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapping a button and its state, so that it can be easily
|
/// Wrapping a button and its state, so that it can be easily
|
||||||
@ -154,6 +167,10 @@ impl ButtonContainer {
|
|||||||
self.button_type.paint();
|
self.button_type.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.button_type.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
/// Setting the visual state of the button - released/pressed.
|
/// Setting the visual state of the button - released/pressed.
|
||||||
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
||||||
if let ButtonType::Button(btn) = &mut self.button_type {
|
if let ButtonType::Button(btn) = &mut self.button_type {
|
||||||
@ -575,6 +592,13 @@ impl Component for ButtonController {
|
|||||||
self.right_btn.paint();
|
self.right_btn.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
self.left_btn.render(target);
|
||||||
|
self.middle_btn.render(target);
|
||||||
|
self.right_btn.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
// Saving button area so that we can re-place the buttons
|
// Saving button area so that we can re-place the buttons
|
||||||
// when they get updated
|
// when they get updated
|
||||||
@ -754,6 +778,8 @@ impl Component for AutomaticMover {
|
|||||||
|
|
||||||
fn paint(&mut self) {}
|
fn paint(&mut self) {}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
// Moving automatically only when we receive a TimerToken that we have
|
// Moving automatically only when we receive a TimerToken that we have
|
||||||
// requested before
|
// requested before
|
||||||
|
@ -2,6 +2,8 @@ use crate::ui::{
|
|||||||
component::{Component, Event, EventCtx, Never, Pad},
|
component::{Component, Event, EventCtx, Never, Pad},
|
||||||
display::Font,
|
display::Font,
|
||||||
geometry::{Alignment, Point, Rect},
|
geometry::{Alignment, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::long_line_content_with_ellipsis,
|
util::long_line_content_with_ellipsis,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,16 +111,39 @@ where
|
|||||||
common::display_left(baseline, &self.text, self.font);
|
common::display_left(baseline, &self.text, self.font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_left<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let baseline = Point::new(self.pad.area.x0, self.y_baseline());
|
||||||
|
shape::Text::new(baseline, self.text.as_ref())
|
||||||
|
.with_font(self.font)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_center(&self) {
|
fn paint_center(&self) {
|
||||||
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
|
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
|
||||||
common::display_center(baseline, &self.text, self.font);
|
common::display_center(baseline, &self.text, self.font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_center<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
|
||||||
|
shape::Text::new(baseline, self.text.as_ref())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(self.font)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_right(&self) {
|
fn paint_right(&self) {
|
||||||
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
|
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
|
||||||
common::display_right(baseline, &self.text, self.font);
|
common::display_right(baseline, &self.text, self.font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_right<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
|
||||||
|
shape::Text::new(baseline, self.text.as_ref())
|
||||||
|
.with_align(Alignment::End)
|
||||||
|
.with_font(self.font)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_long_content_with_ellipsis(&self) {
|
fn paint_long_content_with_ellipsis(&self) {
|
||||||
let text_to_display = long_line_content_with_ellipsis(
|
let text_to_display = long_line_content_with_ellipsis(
|
||||||
self.text.as_ref(),
|
self.text.as_ref(),
|
||||||
@ -175,4 +200,20 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
if self.show_content {
|
||||||
|
// In the case text cannot fit, show ellipsis and its right part
|
||||||
|
if !self.text_fits_completely() {
|
||||||
|
self.paint_long_content_with_ellipsis();
|
||||||
|
} else {
|
||||||
|
match self.alignment {
|
||||||
|
Alignment::Start => self.render_left(target),
|
||||||
|
Alignment::Center => self.render_center(target),
|
||||||
|
Alignment::End => self.render_right(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,16 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
base::Never,
|
base::Never,
|
||||||
text::util::{text_multiline, text_multiline_bottom},
|
text::util::{
|
||||||
|
text_multiline, text_multiline2, text_multiline_bottom, text_multiline_bottom2,
|
||||||
|
},
|
||||||
Component, Event, EventCtx,
|
Component, Event, EventCtx,
|
||||||
},
|
},
|
||||||
display::{self, Font},
|
display::{self, Font},
|
||||||
geometry::{Alignment, Insets, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
||||||
|
model_tr::cshape,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -124,6 +129,56 @@ impl Component for CoinJoinProgress {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// TOP
|
||||||
|
let center = self.area.center() + Offset::y(self.loader_y_offset);
|
||||||
|
|
||||||
|
if self.indeterminate {
|
||||||
|
text_multiline2(
|
||||||
|
target,
|
||||||
|
self.area,
|
||||||
|
TR::coinjoin__title_progress.into(),
|
||||||
|
Font::BOLD,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
Alignment::Center,
|
||||||
|
);
|
||||||
|
cshape::LoaderSmall::new(center, self.value)
|
||||||
|
.with_color(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
cshape::LoaderCircular::new(center, self.value)
|
||||||
|
.with_color(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(center, theme::ICON_TICK_FAT.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOTTOM
|
||||||
|
let top_rest = text_multiline_bottom2(
|
||||||
|
target,
|
||||||
|
self.area,
|
||||||
|
TR::coinjoin__do_not_disconnect.into(),
|
||||||
|
Font::BOLD,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
Alignment::Center,
|
||||||
|
);
|
||||||
|
if let Some(rest) = top_rest {
|
||||||
|
text_multiline_bottom2(
|
||||||
|
target,
|
||||||
|
rest.inset(Insets::bottom(FOOTER_TEXT_MARGIN)),
|
||||||
|
self.text,
|
||||||
|
Font::NORMAL,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
Alignment::Center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -5,6 +5,9 @@ use crate::{
|
|||||||
constant::{screen, WIDTH},
|
constant::{screen, WIDTH},
|
||||||
display,
|
display,
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
|
model_tr::cshape,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,4 +101,29 @@ impl Component for ErrorScreen<'_> {
|
|||||||
|
|
||||||
self.footer.paint();
|
self.footer.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
if self.show_icons {
|
||||||
|
shape::ToifImage::new(screen().top_left(), theme::ICON_WARN_TITLE.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(screen().top_right(), theme::ICON_WARN_TITLE.toif)
|
||||||
|
.with_align(Alignment2D::TOP_RIGHT)
|
||||||
|
.with_fg(FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
self.title.render(target);
|
||||||
|
self.message.render(target);
|
||||||
|
|
||||||
|
cshape::HorizontalLine::new(Point::new(0, DIVIDER_POSITION), WIDTH)
|
||||||
|
.with_step(3)
|
||||||
|
.with_color(FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.footer.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
|
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
|
||||||
constant::SCREEN,
|
constant::SCREEN,
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -313,6 +314,23 @@ where
|
|||||||
// (and painting buttons last would cover the lower part).
|
// (and painting buttons last would cover the lower part).
|
||||||
self.current_page.paint();
|
self.current_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
// Scrollbars are painted only with a title and when requested
|
||||||
|
if self.title.is_some() {
|
||||||
|
if self.show_scrollbar {
|
||||||
|
self.scrollbar.render(target);
|
||||||
|
}
|
||||||
|
self.title.render(target);
|
||||||
|
}
|
||||||
|
self.buttons.render(target);
|
||||||
|
// On purpose painting current page at the end, after buttons,
|
||||||
|
// because we sometimes (in the case of QR code) need to use the
|
||||||
|
// whole height of the display for showing the content
|
||||||
|
// (and painting buttons last would cover the lower part).
|
||||||
|
self.current_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{base::Component, FormattedText, Paginate},
|
component::{base::Component, FormattedText, Paginate},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,6 +122,10 @@ impl Page {
|
|||||||
self.formatted.paint();
|
self.formatted.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.formatted.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
self.formatted.place(bounds);
|
self.formatted.place(bounds);
|
||||||
self.page_count = self.page_count();
|
self.page_count = self.page_count();
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Child, Component, ComponentExt, Event, EventCtx, Paginate},
|
component::{Child, Component, ComponentExt, Event, EventCtx, Paginate},
|
||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,6 +81,11 @@ where
|
|||||||
self.title.paint();
|
self.title.paint();
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.title.render(target);
|
||||||
|
self.content.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Paginate for Frame<T>
|
impl<T> Paginate for Frame<T>
|
||||||
@ -197,6 +203,12 @@ where
|
|||||||
self.scrollbar.paint();
|
self.scrollbar.paint();
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.title.render(target);
|
||||||
|
self.scrollbar.render(target);
|
||||||
|
self.content.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -5,6 +5,7 @@ use crate::{
|
|||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
event::ButtonEvent,
|
event::ButtonEvent,
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,6 +123,10 @@ impl Component for HoldToConfirm {
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.loader.paint();
|
self.loader.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.loader.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
io::BinaryData,
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
translations::TR,
|
translations::TR,
|
||||||
trezorhal::usb::usb_configured,
|
trezorhal::usb::usb_configured,
|
||||||
@ -6,13 +7,17 @@ use crate::{
|
|||||||
component::{Child, Component, Event, EventCtx, Label},
|
component::{Child, Component, Event, EventCtx, Label},
|
||||||
constant::{HEIGHT, WIDTH},
|
constant::{HEIGHT, WIDTH},
|
||||||
display::{
|
display::{
|
||||||
self, rect_fill,
|
self,
|
||||||
toif::{Toif, ToifFormat},
|
image::{ImageInfo, ToifFormat},
|
||||||
|
rect_fill,
|
||||||
|
toif::Toif,
|
||||||
Font, Icon,
|
Font, Icon,
|
||||||
},
|
},
|
||||||
event::USBEvent,
|
event::USBEvent,
|
||||||
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,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,6 +51,16 @@ fn paint_default_image() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_default_image<'s>(target: &mut impl Renderer<'s>) {
|
||||||
|
shape::ToifImage::new(
|
||||||
|
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
|
||||||
|
theme::ICON_LOGO.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
enum CurrentScreen {
|
enum CurrentScreen {
|
||||||
EmptyAtStart,
|
EmptyAtStart,
|
||||||
Homescreen,
|
Homescreen,
|
||||||
@ -57,6 +72,7 @@ pub struct Homescreen {
|
|||||||
// always painted, so we need to always paint the label too
|
// always painted, so we need to always paint the label too
|
||||||
label: Label<'static>,
|
label: Label<'static>,
|
||||||
notification: Option<(TString<'static>, u8)>,
|
notification: Option<(TString<'static>, u8)>,
|
||||||
|
custom_image: Option<BinaryData<'static>>,
|
||||||
/// Used for HTC functionality to lock device from homescreen
|
/// Used for HTC functionality to lock device from homescreen
|
||||||
invisible_buttons: Child<ButtonController>,
|
invisible_buttons: Child<ButtonController>,
|
||||||
/// Holds the loader component
|
/// Holds the loader component
|
||||||
@ -78,9 +94,11 @@ impl Homescreen {
|
|||||||
let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into());
|
let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into());
|
||||||
let loader =
|
let loader =
|
||||||
loader_description.map(|desc| Child::new(ProgressLoader::new(desc, HOLD_TO_LOCK_MS)));
|
loader_description.map(|desc| Child::new(ProgressLoader::new(desc, HOLD_TO_LOCK_MS)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
label: Label::centered(label, theme::TEXT_BIG),
|
label: Label::centered(label, theme::TEXT_BIG),
|
||||||
notification,
|
notification,
|
||||||
|
custom_image: get_homescreen_image(),
|
||||||
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
|
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
|
||||||
loader,
|
loader,
|
||||||
show_loader: false,
|
show_loader: false,
|
||||||
@ -89,18 +107,27 @@ impl Homescreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint_homescreen_image(&self) {
|
fn paint_homescreen_image(&self) {
|
||||||
let homescreen_bytes = get_user_custom_image().ok();
|
if let Some(image) = self.custom_image {
|
||||||
let homescreen = homescreen_bytes
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
.as_ref()
|
// discarded before returning to micropython.
|
||||||
.and_then(|data| Toif::new(data.as_ref()).ok())
|
let toif = unwrap!(Toif::new(unsafe { image.data() }));
|
||||||
.filter(check_homescreen_format);
|
|
||||||
if let Some(toif) = homescreen {
|
|
||||||
toif.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
|
toif.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
|
||||||
} else {
|
} else {
|
||||||
paint_default_image();
|
paint_default_image();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_homescreen_image<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if let Some(image) = self.custom_image {
|
||||||
|
shape::ToifImage::new_image(TOP_CENTER, image)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
render_default_image(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_notification(&self) {
|
fn paint_notification(&self) {
|
||||||
let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height());
|
let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height());
|
||||||
if !usb_configured() {
|
if !usb_configured() {
|
||||||
@ -132,6 +159,51 @@ impl Homescreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_notification<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height());
|
||||||
|
if !usb_configured() {
|
||||||
|
shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
// TODO: fill warning icons here as well?
|
||||||
|
TR::homescreen__title_no_usb_connection.map_translated(|t| {
|
||||||
|
shape::Text::new(baseline, t)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(NOTIFICATION_FONT)
|
||||||
|
.render(target)
|
||||||
|
});
|
||||||
|
} else if let Some((notification, _level)) = &self.notification {
|
||||||
|
shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
notification.map(|c| {
|
||||||
|
shape::Text::new(baseline, c)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(NOTIFICATION_FONT)
|
||||||
|
.render(target)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Painting warning icons in top corners when the text is short enough not to
|
||||||
|
// collide with them
|
||||||
|
let icon_width = NOTIFICATION_ICON.toif.width();
|
||||||
|
let text_width = notification.map(|c| NOTIFICATION_FONT.text_width(c));
|
||||||
|
if AREA.width() >= text_width + (icon_width + 1) * 2 {
|
||||||
|
shape::ToifImage::new(AREA.top_left(), NOTIFICATION_ICON.toif)
|
||||||
|
.with_align(Alignment2D::TOP_LEFT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(AREA.top_right(), NOTIFICATION_ICON.toif)
|
||||||
|
.with_align(Alignment2D::TOP_RIGHT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_label(&mut self) {
|
fn paint_label(&mut self) {
|
||||||
// paint black background to place the label
|
// paint black background to place the label
|
||||||
let mut outset = Insets::uniform(LABEL_OUTSET);
|
let mut outset = Insets::uniform(LABEL_OUTSET);
|
||||||
@ -142,6 +214,19 @@ impl Homescreen {
|
|||||||
self.label.paint();
|
self.label.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_label<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// paint black background to place the label
|
||||||
|
let mut outset = Insets::uniform(LABEL_OUTSET);
|
||||||
|
// the margin at top is bigger (caused by text-height vs line-height?)
|
||||||
|
// compensate by shrinking the outset
|
||||||
|
outset.top -= 5;
|
||||||
|
shape::Bar::new(self.label.text_area().outset(outset))
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.label.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
/// So that notification is well visible even on homescreen image
|
/// So that notification is well visible even on homescreen image
|
||||||
fn fill_notification_background(&self) {
|
fn fill_notification_background(&self) {
|
||||||
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
|
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
|
||||||
@ -229,6 +314,19 @@ impl Component for Homescreen {
|
|||||||
self.paint_label();
|
self.paint_label();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// Redraw the whole screen when the screen changes (loader vs homescreen)
|
||||||
|
if self.show_loader {
|
||||||
|
self.loader.render(target);
|
||||||
|
} else {
|
||||||
|
// Painting the homescreen image first, as the notification and label
|
||||||
|
// should be "on top of it"
|
||||||
|
self.render_homescreen_image(target);
|
||||||
|
self.render_notification(target);
|
||||||
|
self.render_label(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Lockscreen<'a> {
|
pub struct Lockscreen<'a> {
|
||||||
@ -301,29 +399,50 @@ impl Component for Lockscreen<'_> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if self.screensaver {
|
||||||
|
// keep screen blank
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shape::ToifImage::new(
|
||||||
|
TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN),
|
||||||
|
theme::ICON_LOCK.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.instruction.render(target);
|
||||||
|
self.label.render(target);
|
||||||
|
|
||||||
|
if let Some(icon) = &self.coinjoin_icon {
|
||||||
|
shape::ToifImage::new(COINJOIN_CORNER, icon.toif)
|
||||||
|
.with_align(Alignment2D::TOP_RIGHT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfirmHomescreen<F> {
|
pub struct ConfirmHomescreen {
|
||||||
title: Child<Label<'static>>,
|
title: Child<Label<'static>>,
|
||||||
buffer_func: F,
|
image: BinaryData<'static>,
|
||||||
buttons: Child<ButtonController>,
|
buttons: Child<ButtonController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> ConfirmHomescreen<F> {
|
impl ConfirmHomescreen {
|
||||||
pub fn new(title: TString<'static>, buffer_func: F) -> Self {
|
pub fn new(title: TString<'static>, image: BinaryData<'static>) -> Self {
|
||||||
let btn_layout = ButtonLayout::cancel_none_text(TR::buttons__change.into());
|
let btn_layout = ButtonLayout::cancel_none_text(TR::buttons__change.into());
|
||||||
ConfirmHomescreen {
|
ConfirmHomescreen {
|
||||||
title: Child::new(Label::centered(title, theme::TEXT_BOLD_UPPER)),
|
title: Child::new(Label::centered(title, theme::TEXT_BOLD_UPPER)),
|
||||||
buffer_func,
|
image,
|
||||||
buttons: Child::new(ButtonController::new(btn_layout)),
|
buttons: Child::new(ButtonController::new(btn_layout)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, F> Component for ConfirmHomescreen<F>
|
impl Component for ConfirmHomescreen {
|
||||||
where
|
|
||||||
F: Fn() -> &'a [u8],
|
|
||||||
{
|
|
||||||
type Msg = CancelConfirmMsg;
|
type Msg = CancelConfirmMsg;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
@ -348,12 +467,13 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
// Drawing the image full-screen first and then other things on top
|
if self.image.is_empty() {
|
||||||
let buffer = (self.buffer_func)();
|
|
||||||
if buffer.is_empty() {
|
|
||||||
paint_default_image();
|
paint_default_image();
|
||||||
} else {
|
} else {
|
||||||
let toif_data = unwrap!(Toif::new(buffer));
|
// Drawing the image full-screen first and then other things on top
|
||||||
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
|
// discarded before returning to micropython.
|
||||||
|
let toif_data = unwrap!(Toif::new(unsafe { self.image.data() }));
|
||||||
toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
|
toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
|
||||||
};
|
};
|
||||||
// Need to make all the title background black, so the title text is well
|
// Need to make all the title background black, so the title text is well
|
||||||
@ -363,10 +483,47 @@ where
|
|||||||
self.title.paint();
|
self.title.paint();
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if self.image.is_empty() {
|
||||||
|
render_default_image(target);
|
||||||
|
} else {
|
||||||
|
shape::ToifImage::new_image(TOP_CENTER, self.image)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
};
|
||||||
|
// Need to make all the title background black, so the title text is well
|
||||||
|
// visible
|
||||||
|
let title_area = self.title.inner().area();
|
||||||
|
|
||||||
|
shape::Bar::new(title_area)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.title.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_homescreen_format(toif: &Toif) -> bool {
|
pub fn check_homescreen_format(image: BinaryData) -> bool {
|
||||||
toif.format() == ToifFormat::GrayScaleEH && toif.width() == WIDTH && toif.height() == HEIGHT
|
match ImageInfo::parse(image) {
|
||||||
|
ImageInfo::Toif(info) => {
|
||||||
|
info.width() == WIDTH
|
||||||
|
&& info.height() == HEIGHT
|
||||||
|
&& info.format() == ToifFormat::GrayScaleEH
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_homescreen_image() -> Option<BinaryData<'static>> {
|
||||||
|
if let Ok(image) = get_user_custom_image() {
|
||||||
|
if check_homescreen_format(image) {
|
||||||
|
return Some(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
@ -388,7 +545,7 @@ impl crate::trace::Trace for Lockscreen<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
impl<F> crate::trace::Trace for ConfirmHomescreen<F> {
|
impl crate::trace::Trace for ConfirmHomescreen {
|
||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
t.component("ConfirmHomescreen");
|
t.component("ConfirmHomescreen");
|
||||||
t.child("title", &self.title);
|
t.child("title", &self.title);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Child, Component, Event, EventCtx, Pad},
|
component::{Child, Component, Event, EventCtx, Pad},
|
||||||
geometry::{Insets, Offset, Rect},
|
geometry::{Insets, Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -14,11 +15,17 @@ pub trait Choice {
|
|||||||
// Only `paint_center` is required, the rest is optional
|
// Only `paint_center` is required, the rest is optional
|
||||||
// and therefore has a default implementation.
|
// and therefore has a default implementation.
|
||||||
fn paint_center(&self, area: Rect, inverse: bool);
|
fn paint_center(&self, area: Rect, inverse: bool);
|
||||||
|
|
||||||
|
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool);
|
||||||
|
|
||||||
fn width_center(&self) -> i16 {
|
fn width_center(&self) -> i16 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_side(&self, _area: Rect) {}
|
fn paint_side(&self, _area: Rect) {}
|
||||||
|
|
||||||
|
fn render_side<'s>(&self, _target: &mut impl Renderer<'s>, _area: Rect) {}
|
||||||
|
|
||||||
fn width_side(&self) -> i16 {
|
fn width_side(&self) -> i16 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@ -248,6 +255,43 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display current, previous and next choices according to
|
||||||
|
/// the current ChoiceItem.
|
||||||
|
fn render_choices<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// Getting the row area for the choices - so that displaying
|
||||||
|
// items in the used font will show them in the middle vertically.
|
||||||
|
let area_height_half = self.pad.area.height() / 2;
|
||||||
|
let font_size_half = theme::FONT_CHOICE_ITEMS.visible_text_height("Ay") / 2;
|
||||||
|
let center_row_area = self
|
||||||
|
.pad
|
||||||
|
.area
|
||||||
|
.split_top(area_height_half)
|
||||||
|
.0
|
||||||
|
.outset(Insets::bottom(font_size_half));
|
||||||
|
|
||||||
|
// Drawing the current item in the middle.
|
||||||
|
self.show_current_choice2(target, center_row_area);
|
||||||
|
|
||||||
|
// Not drawing the rest when not wanted
|
||||||
|
if self.show_only_one_item {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting the remaining left and right areas.
|
||||||
|
let center_width = self.get_current_item().width_center();
|
||||||
|
let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
|
||||||
|
|
||||||
|
// Possibly drawing on the left side.
|
||||||
|
if self.has_previous_choice() || self.is_carousel {
|
||||||
|
self.show_left_choices2(target, left_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possibly drawing on the right side.
|
||||||
|
if self.has_next_choice() || self.is_carousel {
|
||||||
|
self.show_right_choices2(target, right_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Setting current buttons, and clearing.
|
/// Setting current buttons, and clearing.
|
||||||
fn update(&mut self, ctx: &mut EventCtx) {
|
fn update(&mut self, ctx: &mut EventCtx) {
|
||||||
self.set_buttons(ctx);
|
self.set_buttons(ctx);
|
||||||
@ -296,6 +340,12 @@ where
|
|||||||
.paint_center(area, self.inverse_selected_item);
|
.paint_center(area, self.inverse_selected_item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display the current choice in the middle.
|
||||||
|
fn show_current_choice2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||||
|
self.get_current_item()
|
||||||
|
.render_center(target, area, self.inverse_selected_item);
|
||||||
|
}
|
||||||
|
|
||||||
/// Display all the choices fitting on the left side.
|
/// Display all the choices fitting on the left side.
|
||||||
/// Going as far as possible.
|
/// Going as far as possible.
|
||||||
fn show_left_choices(&self, area: Rect) {
|
fn show_left_choices(&self, area: Rect) {
|
||||||
@ -338,6 +388,48 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display all the choices fitting on the left side.
|
||||||
|
/// Going as far as possible.
|
||||||
|
fn show_left_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||||
|
// NOTE: page index can get negative here, so having it as i16 instead of usize
|
||||||
|
let mut page_index = self.page_counter as i16 - 1;
|
||||||
|
let mut current_area = area.split_right(self.items_distance).0;
|
||||||
|
while current_area.width() > 0 {
|
||||||
|
// Breaking out of the loop if we exhausted left items
|
||||||
|
// and the carousel mode is not enabled.
|
||||||
|
if page_index < 0 {
|
||||||
|
if self.is_carousel {
|
||||||
|
// Moving to the last page.
|
||||||
|
page_index = self.last_page_index() as i16;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (choice, _) = self.choices.get(page_index as usize);
|
||||||
|
let choice_width = choice.width_side();
|
||||||
|
|
||||||
|
if current_area.width() <= choice_width && !self.show_incomplete {
|
||||||
|
// early break for an item that will not fit the remaining space
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to calculate the area explicitly because we want to allow it
|
||||||
|
// to exceed the bounds of the original area.
|
||||||
|
let choice_area = Rect::from_top_right_and_size(
|
||||||
|
current_area.top_right(),
|
||||||
|
Offset::new(choice_width, current_area.height()),
|
||||||
|
);
|
||||||
|
choice.render_side(target, choice_area);
|
||||||
|
|
||||||
|
// Updating loop variables.
|
||||||
|
current_area = current_area
|
||||||
|
.split_right(choice_width + self.items_distance)
|
||||||
|
.0;
|
||||||
|
page_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Display all the choices fitting on the right side.
|
/// Display all the choices fitting on the right side.
|
||||||
/// Going as far as possible.
|
/// Going as far as possible.
|
||||||
fn show_right_choices(&self, area: Rect) {
|
fn show_right_choices(&self, area: Rect) {
|
||||||
@ -379,6 +471,47 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display all the choices fitting on the right side.
|
||||||
|
/// Going as far as possible.
|
||||||
|
fn show_right_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||||
|
let mut page_index = self.page_counter + 1;
|
||||||
|
let mut current_area = area.split_left(self.items_distance).1;
|
||||||
|
while current_area.width() > 0 {
|
||||||
|
// Breaking out of the loop if we exhausted right items
|
||||||
|
// and the carousel mode is not enabled.
|
||||||
|
if page_index > self.last_page_index() {
|
||||||
|
if self.is_carousel {
|
||||||
|
// Moving to the first page.
|
||||||
|
page_index = 0;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (choice, _) = self.choices.get(page_index);
|
||||||
|
let choice_width = choice.width_side();
|
||||||
|
|
||||||
|
if current_area.width() <= choice_width && !self.show_incomplete {
|
||||||
|
// early break for an item that will not fit the remaining space
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to calculate the area explicitly because we want to allow it
|
||||||
|
// to exceed the bounds of the original area.
|
||||||
|
let choice_area = Rect::from_top_left_and_size(
|
||||||
|
current_area.top_left(),
|
||||||
|
Offset::new(choice_width, current_area.height()),
|
||||||
|
);
|
||||||
|
choice.render_side(target, choice_area);
|
||||||
|
|
||||||
|
// Updating loop variables.
|
||||||
|
current_area = current_area
|
||||||
|
.split_left(choice_width + self.items_distance)
|
||||||
|
.1;
|
||||||
|
page_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrease the page counter to the previous page.
|
/// Decrease the page counter to the previous page.
|
||||||
fn decrease_page_counter(&mut self) {
|
fn decrease_page_counter(&mut self) {
|
||||||
self.page_counter -= 1;
|
self.page_counter -= 1;
|
||||||
@ -586,6 +719,12 @@ where
|
|||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
self.paint_choices();
|
self.paint_choices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
self.render_choices(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -3,6 +3,8 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
|
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,6 +109,27 @@ impl Choice for ChoiceItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Painting the item as the main choice in the middle.
|
||||||
|
/// Showing both the icon and text, if the icon is available.
|
||||||
|
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, area: Rect, inverse: bool) {
|
||||||
|
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
|
||||||
|
render_rounded_highlight(
|
||||||
|
target,
|
||||||
|
area,
|
||||||
|
Offset::new(width, self.font.visible_text_height("Ay")),
|
||||||
|
inverse,
|
||||||
|
);
|
||||||
|
render_text_icon(
|
||||||
|
target,
|
||||||
|
area,
|
||||||
|
width,
|
||||||
|
Some(self.text.as_ref()),
|
||||||
|
self.icon,
|
||||||
|
self.font,
|
||||||
|
inverse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Getting the overall width in pixels when displayed in center.
|
/// Getting the overall width in pixels when displayed in center.
|
||||||
/// That means both the icon and text will be shown.
|
/// That means both the icon and text will be shown.
|
||||||
fn width_center(&self) -> i16 {
|
fn width_center(&self) -> i16 {
|
||||||
@ -125,6 +148,20 @@ impl Choice for ChoiceItem {
|
|||||||
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
|
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Painting smaller version of the item on the side.
|
||||||
|
fn render_side<'s>(&self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||||
|
let width = text_icon_width(self.side_text(), self.icon, self.font);
|
||||||
|
render_text_icon(
|
||||||
|
target,
|
||||||
|
area,
|
||||||
|
width,
|
||||||
|
self.side_text(),
|
||||||
|
self.icon,
|
||||||
|
self.font,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Getting current button layout.
|
/// Getting current button layout.
|
||||||
fn btn_layout(&self) -> ButtonLayout {
|
fn btn_layout(&self) -> ButtonLayout {
|
||||||
self.btn_layout.clone()
|
self.btn_layout.clone()
|
||||||
@ -151,6 +188,37 @@ fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_rounded_highlight<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
size: Offset,
|
||||||
|
inverse: bool,
|
||||||
|
) {
|
||||||
|
let bound = theme::BUTTON_OUTLINE;
|
||||||
|
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
|
||||||
|
let x_size = size.x + 2 * bound;
|
||||||
|
let y_size = size.y + 2 * bound;
|
||||||
|
let outline_size = Offset::new(x_size, y_size);
|
||||||
|
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
|
||||||
|
if inverse {
|
||||||
|
shape::Bar::new(outline)
|
||||||
|
.with_radius(1)
|
||||||
|
.with_bg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
// Draw outline by drawing two rounded rectangles
|
||||||
|
shape::Bar::new(outline)
|
||||||
|
.with_radius(1)
|
||||||
|
.with_bg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Bar::new(outline.shrink(1))
|
||||||
|
.with_radius(1)
|
||||||
|
.with_bg(theme::BG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
|
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
|
||||||
match (text, icon) {
|
match (text, icon) {
|
||||||
(Some(text), Some(icon)) => {
|
(Some(text), Some(icon)) => {
|
||||||
@ -194,6 +262,40 @@ fn paint_text_icon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_text_icon<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
width: i16,
|
||||||
|
text: Option<&str>,
|
||||||
|
icon: Option<Icon>,
|
||||||
|
font: Font,
|
||||||
|
inverse: bool,
|
||||||
|
) {
|
||||||
|
let fg_color = if inverse { theme::BG } else { theme::FG };
|
||||||
|
|
||||||
|
let mut baseline = area.bottom_center() - Offset::x(width / 2);
|
||||||
|
if let Some(icon) = icon {
|
||||||
|
let height_diff = font.visible_text_height("Ay") - icon.toif.height();
|
||||||
|
let vertical_offset = Offset::y(-height_diff / 2);
|
||||||
|
shape::ToifImage::new(baseline + vertical_offset, icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(text) = text {
|
||||||
|
// Possibly shifting the baseline left, when there is a text bearing.
|
||||||
|
// This is to center the text properly.
|
||||||
|
baseline = baseline - Offset::x(font.start_x_bearing(text));
|
||||||
|
shape::Text::new(baseline, text)
|
||||||
|
.with_font(font)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,6 +82,10 @@ impl Component for NumberInput {
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -6,6 +6,7 @@ use crate::{
|
|||||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
util::char_to_string,
|
util::char_to_string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -448,6 +449,11 @@ impl Component for PassphraseEntry {
|
|||||||
self.passphrase_dots.paint();
|
self.passphrase_dots.paint();
|
||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.passphrase_dots.render(target);
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -6,6 +6,7 @@ use crate::{
|
|||||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||||
display::{Font, Icon},
|
display::{Font, Icon},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -323,6 +324,12 @@ impl Component for PinEntry<'_> {
|
|||||||
self.pin_line.paint();
|
self.pin_line.paint();
|
||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.header_line.render(target);
|
||||||
|
self.pin_line.render(target);
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx},
|
component::{Component, Event, EventCtx},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,6 +116,10 @@ impl Component for SimpleChoice {
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
util::char_to_string,
|
util::char_to_string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -314,6 +315,11 @@ impl Component for WordlistEntry {
|
|||||||
self.chosen_letters.paint();
|
self.chosen_letters.paint();
|
||||||
self.choice_page.paint();
|
self.choice_page.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.chosen_letters.render(target);
|
||||||
|
self.choice_page.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -6,7 +6,9 @@ use crate::{
|
|||||||
component::{Child, Component, Event, EventCtx},
|
component::{Child, Component, Event, EventCtx},
|
||||||
constant,
|
constant,
|
||||||
display::{self, Color, Font, LOADER_MAX},
|
display::{self, Color, Font, LOADER_MAX},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -164,6 +166,51 @@ impl Loader {
|
|||||||
invert_from as i16,
|
invert_from as i16,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_loader<'s>(
|
||||||
|
&'s self,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
style: &LoaderStyle,
|
||||||
|
done: i32,
|
||||||
|
) {
|
||||||
|
let width = self.area.width();
|
||||||
|
// NOTE: need to calculate this in `i32`, it would overflow using `i16`
|
||||||
|
let split_point = (((width as i32 + 1) * done) / (display::LOADER_MAX as i32)) as i16;
|
||||||
|
let (r_left, r_right) = self.area.split_left(split_point);
|
||||||
|
let parts = [(r_left, true), (r_right, false)];
|
||||||
|
parts.map(|(r, invert)| {
|
||||||
|
target.in_clip(r, &|target| {
|
||||||
|
if invert {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_radius(3)
|
||||||
|
.with_bg(style.fg_color)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_radius(3)
|
||||||
|
.with_fg(style.fg_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_color = if invert {
|
||||||
|
style.bg_color
|
||||||
|
} else {
|
||||||
|
style.fg_color
|
||||||
|
};
|
||||||
|
|
||||||
|
self.get_text().map(|t| {
|
||||||
|
let pt = Point::new(
|
||||||
|
style.font.horz_center(self.area.x0, self.area.x1, t),
|
||||||
|
style.font.vert_center(self.area.y0, self.area.y1, "A"),
|
||||||
|
);
|
||||||
|
shape::Text::new(pt, t)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(text_color)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Loader {
|
impl Component for Loader {
|
||||||
@ -223,6 +270,28 @@ impl Component for Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// TODO: Consider passing the current instant along with the event -- that way,
|
||||||
|
// we could synchronize painting across the component tree. Also could be useful
|
||||||
|
// in automated tests.
|
||||||
|
// In practice, taking the current instant here is more precise in case some
|
||||||
|
// other component in the tree takes a long time to draw.
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if let State::Initial = self.state {
|
||||||
|
self.render_loader(target, self.styles.normal, 0);
|
||||||
|
} else if let State::Grown = self.state {
|
||||||
|
self.render_loader(target, self.styles.normal, display::LOADER_MAX as i32);
|
||||||
|
} else {
|
||||||
|
let progress = self.progress(now);
|
||||||
|
if let Some(done) = progress {
|
||||||
|
self.render_loader(target, self.styles.normal, done as i32);
|
||||||
|
} else {
|
||||||
|
self.render_loader(target, self.styles.normal, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LoaderStyleSheet {
|
pub struct LoaderStyleSheet {
|
||||||
@ -323,6 +392,10 @@ impl Component for ProgressLoader {
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.loader.paint();
|
self.loader.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.loader.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
|
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
|
||||||
display::Color,
|
display::Color,
|
||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -215,6 +216,12 @@ where
|
|||||||
self.content.paint();
|
self.content.paint();
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.pad.render(target);
|
||||||
|
self.content.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -10,7 +10,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
constant,
|
constant,
|
||||||
display::{self, Font, Icon, LOADER_MAX},
|
display::{self, Font, Icon, LOADER_MAX},
|
||||||
geometry::Rect,
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
model_tr::cshape,
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -151,6 +154,29 @@ impl Component for Progress {
|
|||||||
self.description.paint();
|
self.description.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.title.render(target);
|
||||||
|
|
||||||
|
let area = constant::screen();
|
||||||
|
let center = area.center() + Offset::y(self.loader_y_offset);
|
||||||
|
|
||||||
|
if self.indeterminate {
|
||||||
|
cshape::LoaderStarry::new(center, self.value)
|
||||||
|
.with_color(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
} else {
|
||||||
|
cshape::LoaderCircular::new(center, self.value)
|
||||||
|
.with_color(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(center, self.icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
self.description_pad.render(target);
|
||||||
|
self.description.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(Self::AREA);
|
sink(Self::AREA);
|
||||||
|
@ -3,6 +3,8 @@ use crate::ui::{
|
|||||||
constant::{screen, HEIGHT, WIDTH},
|
constant::{screen, HEIGHT, WIDTH},
|
||||||
display::{Color, Icon},
|
display::{Color, Icon},
|
||||||
geometry::{Alignment2D, Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MESSAGE_AREA_START: i16 = 24 + 11;
|
const MESSAGE_AREA_START: i16 = 24 + 11;
|
||||||
@ -107,4 +109,17 @@ impl Component for ResultScreen<'_> {
|
|||||||
self.message_top.paint();
|
self.message_top.paint();
|
||||||
self.message_bottom.paint();
|
self.message_bottom.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.small_pad.render(target);
|
||||||
|
|
||||||
|
shape::ToifImage::new(screen().top_center() + Offset::y(ICON_TOP), self.icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(self.fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.message_top.render(target);
|
||||||
|
self.message_bottom.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ use crate::ui::{
|
|||||||
component::{Component, Event, EventCtx, Never, Pad, Paginate},
|
component::{Component, Event, EventCtx, Never, Pad, Paginate},
|
||||||
display,
|
display,
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::theme;
|
use super::super::theme;
|
||||||
@ -103,6 +105,35 @@ impl ScrollBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a (seemingly circular) dot given its top left point.
|
||||||
|
/// Make it full when it is active, otherwise paint just the perimeter and
|
||||||
|
/// leave center empty.
|
||||||
|
fn render_dot<'s>(&self, target: &mut impl Renderer<'s>, dot_type: &DotType, top_right: Point) {
|
||||||
|
let full_square =
|
||||||
|
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
|
||||||
|
|
||||||
|
match dot_type {
|
||||||
|
DotType::BigFull => shape::Bar::new(full_square)
|
||||||
|
.with_radius(2)
|
||||||
|
.with_bg(theme::FG)
|
||||||
|
.render(target),
|
||||||
|
|
||||||
|
DotType::Big => shape::Bar::new(full_square)
|
||||||
|
.with_radius(2)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target),
|
||||||
|
|
||||||
|
DotType::Middle => shape::Bar::new(full_square.shrink(1))
|
||||||
|
.with_radius(1)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target),
|
||||||
|
|
||||||
|
DotType::Small => shape::Bar::new(full_square.shrink(2))
|
||||||
|
.with_bg(theme::FG)
|
||||||
|
.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a sequence of dots to be drawn, with specifying their appearance.
|
/// Get a sequence of dots to be drawn, with specifying their appearance.
|
||||||
/// Painting only big dots in case of 2 and 3 pages,
|
/// Painting only big dots in case of 2 and 3 pages,
|
||||||
/// three big and 1 middle in case of 4 pages,
|
/// three big and 1 middle in case of 4 pages,
|
||||||
@ -202,6 +233,14 @@ impl ScrollBar {
|
|||||||
top_right.x -= Self::DOTS_INTERVAL;
|
top_right.x -= Self::DOTS_INTERVAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_horizontal<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let mut top_right = self.pad.area.top_right();
|
||||||
|
for dot in self.get_drawable_dots().iter().rev() {
|
||||||
|
self.render_dot(target, dot, top_right);
|
||||||
|
top_right.x -= Self::DOTS_INTERVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for ScrollBar {
|
impl Component for ScrollBar {
|
||||||
@ -233,6 +272,17 @@ impl Component for ScrollBar {
|
|||||||
self.pad.paint();
|
self.pad.paint();
|
||||||
self.paint_horizontal();
|
self.paint_horizontal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Displaying one dot for each page.
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// Not showing the scrollbar dot when there is only one page
|
||||||
|
if self.page_count <= 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pad.render(target);
|
||||||
|
self.render_horizontal(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Paginate for ScrollBar {
|
impl Paginate for ScrollBar {
|
||||||
|
@ -3,10 +3,13 @@ use crate::{
|
|||||||
translations::TR,
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
text::util::text_multiline, Child, Component, Event, EventCtx, Never, Paginate,
|
text::util::{text_multiline, text_multiline2},
|
||||||
|
Child, Component, Event, EventCtx, Never, Paginate,
|
||||||
},
|
},
|
||||||
display::Font,
|
display::Font,
|
||||||
geometry::{Alignment, Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,6 +98,20 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display the final page with user confirmation.
|
||||||
|
fn render_final_page<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let final_text = self.get_final_text();
|
||||||
|
text_multiline2(
|
||||||
|
target,
|
||||||
|
self.area.split_top(INFO_TOP_OFFSET).1,
|
||||||
|
final_text.as_str().into(),
|
||||||
|
Font::NORMAL,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
Alignment::Start,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Display current set of recovery words.
|
/// Display current set of recovery words.
|
||||||
fn paint_words(&mut self) {
|
fn paint_words(&mut self) {
|
||||||
let mut y_offset = 0;
|
let mut y_offset = 0;
|
||||||
@ -112,6 +129,32 @@ where
|
|||||||
display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT);
|
display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display current set of recovery words.
|
||||||
|
fn render_words<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let mut y_offset = 0;
|
||||||
|
// Showing the word index and the words itself
|
||||||
|
for i in 0..WORDS_PER_PAGE {
|
||||||
|
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
|
||||||
|
let index = self.word_index() + i;
|
||||||
|
if index >= self.share_words.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let word = &self.share_words[index];
|
||||||
|
let baseline = self.area.top_left() + Offset::y(y_offset);
|
||||||
|
let ordinal = build_string!(5, inttostr!(index as u8 + 1), ".");
|
||||||
|
|
||||||
|
shape::Text::new(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal)
|
||||||
|
.with_font(NUMBER_FONT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(baseline + Offset::x(WORD_X_OFFSET), word.as_ref())
|
||||||
|
.with_font(WORD_FONT)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Component for ShareWords<T>
|
impl<T> Component for ShareWords<T>
|
||||||
@ -147,6 +190,17 @@ where
|
|||||||
self.paint_words();
|
self.paint_words();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// Showing scrollbar in all cases
|
||||||
|
// Individual pages are responsible for not colliding with it
|
||||||
|
self.scrollbar.render(target);
|
||||||
|
if self.is_final_page() {
|
||||||
|
self.render_final_page(target);
|
||||||
|
} else {
|
||||||
|
self.render_words(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Paginate for ShareWords<T>
|
impl<T> Paginate for ShareWords<T>
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Child, Component, Event, EventCtx},
|
component::{Child, Component, Event, EventCtx},
|
||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,6 +78,11 @@ where
|
|||||||
self.content.paint();
|
self.content.paint();
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.content.render(target);
|
||||||
|
self.buttons.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -4,7 +4,9 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, Marquee, Never},
|
component::{Component, Event, EventCtx, Marquee, Never},
|
||||||
display,
|
display,
|
||||||
geometry::{Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,6 +61,22 @@ impl Title {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display title/header at the top left of the given area.
|
||||||
|
pub fn render_header_left<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
title: &TString<'static>,
|
||||||
|
area: Rect,
|
||||||
|
) {
|
||||||
|
let text_height = theme::FONT_HEADER.text_height();
|
||||||
|
let title_baseline = area.top_left() + Offset::y(text_height - 1);
|
||||||
|
title.map(|s| {
|
||||||
|
shape::Text::new(title_baseline, s)
|
||||||
|
.with_font(theme::FONT_HEADER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Display title/header centered at the top of the given area.
|
/// Display title/header centered at the top of the given area.
|
||||||
pub fn paint_header_centered(title: &TString<'static>, area: Rect) {
|
pub fn paint_header_centered(title: &TString<'static>, area: Rect) {
|
||||||
let text_height = theme::FONT_HEADER.text_height();
|
let text_height = theme::FONT_HEADER.text_height();
|
||||||
@ -67,6 +85,23 @@ impl Title {
|
|||||||
display::text_center(title_baseline, s, theme::FONT_HEADER, theme::FG, theme::BG)
|
display::text_center(title_baseline, s, theme::FONT_HEADER, theme::FG, theme::BG)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display title/header centered at the top of the given area.
|
||||||
|
pub fn render_header_centered<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
title: &TString<'static>,
|
||||||
|
area: Rect,
|
||||||
|
) {
|
||||||
|
let text_height = theme::FONT_HEADER.text_height();
|
||||||
|
let title_baseline = area.top_center() + Offset::y(text_height - 1);
|
||||||
|
title.map(|s| {
|
||||||
|
shape::Text::new(title_baseline, s)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(theme::FONT_HEADER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Title {
|
impl Component for Title {
|
||||||
@ -99,6 +134,16 @@ impl Component for Title {
|
|||||||
Self::paint_header_left(&self.title, self.area);
|
Self::paint_header_left(&self.title, self.area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if self.needs_marquee {
|
||||||
|
self.marquee.render(target);
|
||||||
|
} else if self.centered {
|
||||||
|
Self::render_header_centered(target, &self.title, self.area);
|
||||||
|
} else {
|
||||||
|
Self::render_header_left(target, &self.title, self.area);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG-ONLY SECTION BELOW
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx, Never},
|
component::{Component, Event, EventCtx, Never},
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
geometry::{Alignment2D, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::theme;
|
use super::super::theme;
|
||||||
@ -52,6 +54,30 @@ impl Component for WelcomeScreen {
|
|||||||
theme::BG,
|
theme::BG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
shape::ToifImage::new(
|
||||||
|
self.area.bottom_center() - Offset::y(5),
|
||||||
|
theme::ICON_DEVICE_NAME.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::BOTTOM_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let icon = if self.empty_lock {
|
||||||
|
theme::ICON_LOGO_EMPTY
|
||||||
|
} else {
|
||||||
|
theme::ICON_LOGO
|
||||||
|
};
|
||||||
|
|
||||||
|
shape::ToifImage::new(
|
||||||
|
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
|
||||||
|
icon.toif,
|
||||||
|
)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
87
core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs
Normal file
87
core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
display::Color,
|
||||||
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape::{Canvas, DrawingCache, Renderer, Shape, ShapeClone},
|
||||||
|
};
|
||||||
|
|
||||||
|
use without_alloc::alloc::LocalAllocLeakExt;
|
||||||
|
|
||||||
|
// Shape of horizontal solid/dotted line
|
||||||
|
pub struct HorizontalLine {
|
||||||
|
/// Position of the left-top point
|
||||||
|
pos: Point,
|
||||||
|
// Length of the line
|
||||||
|
length: i16,
|
||||||
|
/// Line thickness (default 1)
|
||||||
|
thickness: u8,
|
||||||
|
/// Steps of dots (default 0 - full line)
|
||||||
|
step: u8,
|
||||||
|
/// Color
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HorizontalLine {
|
||||||
|
pub fn new(pos: Point, length: i16) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
length,
|
||||||
|
thickness: 1,
|
||||||
|
step: 0,
|
||||||
|
color: Color::white(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(self, color: Color) -> Self {
|
||||||
|
Self { color, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_thickness(self, thickness: u8) -> Self {
|
||||||
|
Self { thickness, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_step(self, step: u8) -> Self {
|
||||||
|
Self { step, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
|
||||||
|
renderer.render_shape(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Shape<'s> for HorizontalLine {
|
||||||
|
fn bounds(&self) -> Rect {
|
||||||
|
let size = Offset::new(self.length, self.thickness as i16);
|
||||||
|
Rect::from_top_left_and_size(self.pos, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, _cache: &DrawingCache) {}
|
||||||
|
|
||||||
|
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
|
||||||
|
if self.step <= self.thickness {
|
||||||
|
// Solid line
|
||||||
|
let size = Offset::new(self.length, self.thickness as i16);
|
||||||
|
let r = Rect::from_top_left_and_size(self.pos, size);
|
||||||
|
canvas.fill_rect(r, self.color, 255);
|
||||||
|
} else {
|
||||||
|
// Dotted line
|
||||||
|
let thickness = self.thickness as i16;
|
||||||
|
for x in (0..self.length - thickness).step_by(self.step as usize) {
|
||||||
|
let r = Rect::from_top_left_and_size(
|
||||||
|
self.pos + Offset::x(x),
|
||||||
|
Offset::uniform(thickness),
|
||||||
|
);
|
||||||
|
canvas.fill_rect(r, self.color, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ShapeClone<'s> for HorizontalLine {
|
||||||
|
fn clone_at_bump<T>(self, bump: &'s T) -> Option<&'s mut dyn Shape<'s>>
|
||||||
|
where
|
||||||
|
T: LocalAllocLeakExt<'s>,
|
||||||
|
{
|
||||||
|
let clone = bump.alloc_t()?;
|
||||||
|
Some(clone.uninit.init(HorizontalLine { ..self }))
|
||||||
|
}
|
||||||
|
}
|
116
core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs
Normal file
116
core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
display::Color,
|
||||||
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape::{Canvas, DrawingCache, Renderer, Shape, ShapeClone},
|
||||||
|
};
|
||||||
|
|
||||||
|
use without_alloc::alloc::LocalAllocLeakExt;
|
||||||
|
|
||||||
|
static CELLS: [Offset; 24] = [
|
||||||
|
Offset::new(1, -4),
|
||||||
|
Offset::new(2, -4),
|
||||||
|
Offset::new(3, -3),
|
||||||
|
Offset::new(4, -2),
|
||||||
|
Offset::new(4, -1),
|
||||||
|
Offset::new(4, 0),
|
||||||
|
Offset::new(4, 1),
|
||||||
|
Offset::new(4, 2),
|
||||||
|
Offset::new(3, 3),
|
||||||
|
Offset::new(2, 4),
|
||||||
|
Offset::new(1, 4),
|
||||||
|
Offset::new(0, 4),
|
||||||
|
Offset::new(-1, 4),
|
||||||
|
Offset::new(-2, 4),
|
||||||
|
Offset::new(-3, 3),
|
||||||
|
Offset::new(-4, 2),
|
||||||
|
Offset::new(-4, 1),
|
||||||
|
Offset::new(-4, 0),
|
||||||
|
Offset::new(-4, -1),
|
||||||
|
Offset::new(-4, -2),
|
||||||
|
Offset::new(-3, -3),
|
||||||
|
Offset::new(-2, -4),
|
||||||
|
Offset::new(-1, -4),
|
||||||
|
Offset::new(0, -4),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct LoaderCircular {
|
||||||
|
/// Position of point (0,0)
|
||||||
|
pos: Point,
|
||||||
|
/// Value 0..1000
|
||||||
|
value: u16,
|
||||||
|
/// Color
|
||||||
|
color: Color,
|
||||||
|
/// Scale (length of square size)
|
||||||
|
scale: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoaderCircular {
|
||||||
|
pub fn new(pos: Point, value: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
value,
|
||||||
|
color: Color::white(),
|
||||||
|
scale: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(self, color: Color) -> Self {
|
||||||
|
Self { color, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_scale(self, scale: i16) -> Self {
|
||||||
|
Self { scale, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cells(&self) -> &[Offset] {
|
||||||
|
let value = self.value.clamp(0, 1000);
|
||||||
|
let last = (CELLS.len() * value as usize) / 1000;
|
||||||
|
&CELLS[..last]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_rect(&self, offset: Offset) -> Rect {
|
||||||
|
let pt = Point::new(
|
||||||
|
self.pos.x + (offset.x * self.scale) - self.scale / 2,
|
||||||
|
self.pos.y + (offset.y * self.scale) - self.scale / 2,
|
||||||
|
);
|
||||||
|
Rect::from_top_left_and_size(pt, Offset::uniform(self.scale))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
|
||||||
|
renderer.render_shape(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Shape<'s> for LoaderCircular {
|
||||||
|
fn bounds(&self) -> Rect {
|
||||||
|
let cells = self.cells();
|
||||||
|
|
||||||
|
if cells.is_empty() {
|
||||||
|
Rect::zero()
|
||||||
|
} else {
|
||||||
|
let mut b = self.cell_rect(cells[0]);
|
||||||
|
cells[1..]
|
||||||
|
.iter()
|
||||||
|
.for_each(|c| b = b.union(self.cell_rect(*c)));
|
||||||
|
b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, _cache: &DrawingCache) {}
|
||||||
|
|
||||||
|
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
|
||||||
|
for c in self.cells().iter() {
|
||||||
|
canvas.fill_rect(self.cell_rect(*c), self.color, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ShapeClone<'s> for LoaderCircular {
|
||||||
|
fn clone_at_bump<T>(self, bump: &'s T) -> Option<&'s mut dyn Shape>
|
||||||
|
where
|
||||||
|
T: LocalAllocLeakExt<'s>,
|
||||||
|
{
|
||||||
|
let clone = bump.alloc_t()?;
|
||||||
|
Some(clone.uninit.init(LoaderCircular { ..self }))
|
||||||
|
}
|
||||||
|
}
|
88
core/embed/rust/src/ui/model_tr/cshape/loader_small.rs
Normal file
88
core/embed/rust/src/ui/model_tr/cshape/loader_small.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
display::Color,
|
||||||
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape::{Canvas, DrawingCache, Renderer, Shape, ShapeClone},
|
||||||
|
};
|
||||||
|
|
||||||
|
use without_alloc::alloc::LocalAllocLeakExt;
|
||||||
|
|
||||||
|
use core::f32::consts::SQRT_2;
|
||||||
|
|
||||||
|
const STAR_COUNT: usize = 8;
|
||||||
|
const RADIUS: i16 = 3;
|
||||||
|
const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16;
|
||||||
|
|
||||||
|
// Offset of the normal point and then the extra offset for the main point
|
||||||
|
static STARS: [(Offset, Offset); STAR_COUNT] = [
|
||||||
|
(Offset::y(-RADIUS), Offset::y(-1)),
|
||||||
|
(Offset::new(DIAGONAL, -DIAGONAL), Offset::new(1, -1)),
|
||||||
|
(Offset::x(RADIUS), Offset::x(1)),
|
||||||
|
(Offset::new(DIAGONAL, DIAGONAL), Offset::new(1, 1)),
|
||||||
|
(Offset::y(RADIUS), Offset::y(1)),
|
||||||
|
(Offset::new(-DIAGONAL, DIAGONAL), Offset::new(-1, 1)),
|
||||||
|
(Offset::x(-RADIUS), Offset::x(-1)),
|
||||||
|
(Offset::new(-DIAGONAL, -DIAGONAL), Offset::new(-1, -1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A shape of a TS3 small loader
|
||||||
|
pub struct LoaderSmall {
|
||||||
|
/// Position of point (0,0)
|
||||||
|
pos: Point,
|
||||||
|
/// Value 0..1000
|
||||||
|
value: u16,
|
||||||
|
/// Color
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoaderSmall {
|
||||||
|
pub fn new(pos: Point, value: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
value,
|
||||||
|
color: Color::white(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(self, color: Color) -> Self {
|
||||||
|
Self { color, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
|
||||||
|
renderer.render_shape(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape<'_> for LoaderSmall {
|
||||||
|
fn bounds(&self) -> Rect {
|
||||||
|
Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, _cache: &DrawingCache) {}
|
||||||
|
|
||||||
|
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
|
||||||
|
// Calculate index of the highlighted star
|
||||||
|
let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT;
|
||||||
|
|
||||||
|
for (i, (star_offset, hili_offset)) in STARS.iter().enumerate() {
|
||||||
|
if (sel_idx + 1) % STAR_COUNT != i {
|
||||||
|
// Draw a star if it's not behind the highlighted one (clockwise)
|
||||||
|
let star_pos = self.pos + *star_offset;
|
||||||
|
canvas.draw_pixel(star_pos, self.color);
|
||||||
|
if sel_idx == i {
|
||||||
|
// Higlight the main star
|
||||||
|
canvas.draw_pixel(star_pos + *hili_offset, self.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ShapeClone<'s> for LoaderSmall {
|
||||||
|
fn clone_at_bump<T>(self, bump: &'s T) -> Option<&'s mut dyn Shape>
|
||||||
|
where
|
||||||
|
T: LocalAllocLeakExt<'s>,
|
||||||
|
{
|
||||||
|
let clone = bump.alloc_t()?;
|
||||||
|
Some(clone.uninit.init(LoaderSmall { ..self }))
|
||||||
|
}
|
||||||
|
}
|
105
core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs
Normal file
105
core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
display::Color,
|
||||||
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape::{Canvas, DrawingCache, Renderer, Shape, ShapeClone},
|
||||||
|
};
|
||||||
|
|
||||||
|
use without_alloc::alloc::LocalAllocLeakExt;
|
||||||
|
|
||||||
|
use core::f32::consts::SQRT_2;
|
||||||
|
|
||||||
|
const STAR_COUNT: usize = 8;
|
||||||
|
const STAR_SMALL: i16 = 2;
|
||||||
|
const STAR_MEDIUM: i16 = 4;
|
||||||
|
const STAR_LARGE: i16 = 6;
|
||||||
|
|
||||||
|
const RADIUS: i16 = 13;
|
||||||
|
const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16;
|
||||||
|
|
||||||
|
// Offset of the normal point and then the extra offset for the main point
|
||||||
|
static STARS: [Offset; STAR_COUNT] = [
|
||||||
|
Offset::y(-RADIUS),
|
||||||
|
Offset::new(DIAGONAL, -DIAGONAL),
|
||||||
|
Offset::x(RADIUS),
|
||||||
|
Offset::new(DIAGONAL, DIAGONAL),
|
||||||
|
Offset::y(RADIUS),
|
||||||
|
Offset::new(-DIAGONAL, DIAGONAL),
|
||||||
|
Offset::x(-RADIUS),
|
||||||
|
Offset::new(-DIAGONAL, -DIAGONAL),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A shape of a TS3 starry loader
|
||||||
|
pub struct LoaderStarry {
|
||||||
|
/// Position of point (0,0)
|
||||||
|
pos: Point,
|
||||||
|
/// Value 0..1000
|
||||||
|
value: u16,
|
||||||
|
/// Color
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoaderStarry {
|
||||||
|
pub fn new(pos: Point, value: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
value,
|
||||||
|
color: Color::white(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(self, color: Color) -> Self {
|
||||||
|
Self { color, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) {
|
||||||
|
renderer.render_shape(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_large_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
|
||||||
|
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_LARGE));
|
||||||
|
canvas.fill_round_rect(r, 2, self.color, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_medium_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
|
||||||
|
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_MEDIUM));
|
||||||
|
canvas.fill_round_rect(r, 1, self.color, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_small_star(&self, canvas: &mut dyn Canvas, offset: Offset) {
|
||||||
|
let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_SMALL));
|
||||||
|
canvas.fill_rect(r, self.color, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape<'_> for LoaderStarry {
|
||||||
|
fn bounds(&self) -> Rect {
|
||||||
|
Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + STAR_LARGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, _cache: &DrawingCache) {}
|
||||||
|
|
||||||
|
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) {
|
||||||
|
// Calculate the index of the big star
|
||||||
|
let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT;
|
||||||
|
|
||||||
|
for (i, c) in STARS.iter().enumerate() {
|
||||||
|
if i == sel_idx {
|
||||||
|
self.draw_large_star(canvas, *c);
|
||||||
|
} else if (sel_idx + 1) % 8 == i || (sel_idx - 1) % 8 == i {
|
||||||
|
self.draw_medium_star(canvas, *c);
|
||||||
|
} else {
|
||||||
|
self.draw_small_star(canvas, *c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ShapeClone<'s> for LoaderStarry {
|
||||||
|
fn clone_at_bump<T>(self, bump: &'s T) -> Option<&'s mut dyn Shape>
|
||||||
|
where
|
||||||
|
T: LocalAllocLeakExt<'s>,
|
||||||
|
{
|
||||||
|
let clone = bump.alloc_t()?;
|
||||||
|
Some(clone.uninit.init(LoaderStarry { ..self }))
|
||||||
|
}
|
||||||
|
}
|
9
core/embed/rust/src/ui/model_tr/cshape/mod.rs
Normal file
9
core/embed/rust/src/ui/model_tr/cshape/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod dotted_line;
|
||||||
|
pub mod loader_circular;
|
||||||
|
pub mod loader_small;
|
||||||
|
pub mod loader_starry;
|
||||||
|
|
||||||
|
pub use dotted_line::HorizontalLine;
|
||||||
|
pub use loader_circular::LoaderCircular;
|
||||||
|
pub use loader_small::LoaderSmall;
|
||||||
|
pub use loader_starry::LoaderStarry;
|
@ -6,15 +6,8 @@ use crate::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
maybe_trace::MaybeTrace,
|
maybe_trace::MaybeTrace,
|
||||||
micropython::{
|
micropython::{
|
||||||
buffer::{get_buffer, StrBuffer},
|
buffer::StrBuffer, gc::Gc, iter::IterBuf, list::List, map::Map, module::Module, obj::Obj,
|
||||||
gc::Gc,
|
qstr::Qstr, util,
|
||||||
iter::IterBuf,
|
|
||||||
list::List,
|
|
||||||
map::Map,
|
|
||||||
module::Module,
|
|
||||||
obj::Obj,
|
|
||||||
qstr::Qstr,
|
|
||||||
util,
|
|
||||||
},
|
},
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
translations::TR,
|
translations::TR,
|
||||||
@ -34,7 +27,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
ComponentExt, FormattedText, Label, LineBreaking, Timeout,
|
ComponentExt, FormattedText, Label, LineBreaking, Timeout,
|
||||||
},
|
},
|
||||||
display, geometry,
|
geometry,
|
||||||
layout::{
|
layout::{
|
||||||
obj::{ComponentMsgObj, LayoutObj},
|
obj::{ComponentMsgObj, LayoutObj},
|
||||||
result::{CANCELLED, CONFIRMED, INFO},
|
result::{CANCELLED, CONFIRMED, INFO},
|
||||||
@ -216,10 +209,7 @@ impl<'a> ComponentMsgObj for Lockscreen<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, F> ComponentMsgObj for ConfirmHomescreen<F>
|
impl ComponentMsgObj for ConfirmHomescreen {
|
||||||
where
|
|
||||||
F: Fn() -> &'a [u8],
|
|
||||||
{
|
|
||||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
match msg {
|
match msg {
|
||||||
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
||||||
@ -400,15 +390,8 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
|
|||||||
extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
|
let image: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
|
||||||
|
let obj = LayoutObj::new(ConfirmHomescreen::new(title, image.try_into()?))?;
|
||||||
// Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8]
|
|
||||||
// in every paint pass.
|
|
||||||
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
|
||||||
// discarded before returning to micropython.
|
|
||||||
let buffer_func = move || unsafe { unwrap!(get_buffer(data)) };
|
|
||||||
|
|
||||||
let obj = LayoutObj::new(ConfirmHomescreen::new(title, buffer_func))?;
|
|
||||||
Ok(obj.into())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1615,13 +1598,8 @@ extern "C" fn new_confirm_firmware_update(
|
|||||||
|
|
||||||
pub extern "C" fn upy_check_homescreen_format(data: Obj) -> Obj {
|
pub extern "C" fn upy_check_homescreen_format(data: Obj) -> Obj {
|
||||||
let block = || {
|
let block = || {
|
||||||
// SAFETY: buffer does not outlive this function
|
let image = data.try_into()?;
|
||||||
let buffer = unsafe { get_buffer(data) }?;
|
Ok(check_homescreen_format(image).into())
|
||||||
|
|
||||||
Ok(display::toif::Toif::new(buffer)
|
|
||||||
.map(|toif| check_homescreen_format(&toif))
|
|
||||||
.unwrap_or(false)
|
|
||||||
.into())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe { util::try_or_raise(block) }
|
unsafe { util::try_or_raise(block) }
|
||||||
|
@ -5,6 +5,7 @@ pub mod bootloader;
|
|||||||
pub mod common_messages;
|
pub mod common_messages;
|
||||||
pub mod component;
|
pub mod component;
|
||||||
pub mod constant;
|
pub mod constant;
|
||||||
|
pub mod cshape;
|
||||||
#[cfg(feature = "micropython")]
|
#[cfg(feature = "micropython")]
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
mod screens;
|
mod screens;
|
||||||
|
@ -2,11 +2,21 @@ use crate::ui::{
|
|||||||
component::base::Component, constant::screen, display, model_tr::component::WelcomeScreen,
|
component::base::Component, constant::screen, display, model_tr::component::WelcomeScreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{display::Color, shape::render_on_display};
|
||||||
|
|
||||||
use super::{component::ErrorScreen, constant};
|
use super::{component::ErrorScreen, constant};
|
||||||
|
|
||||||
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
|
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
|
||||||
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
|
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
|
||||||
frame.place(constant::screen());
|
frame.place(constant::screen());
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
frame.render(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
frame.paint();
|
frame.paint();
|
||||||
display::refresh();
|
display::refresh();
|
||||||
}
|
}
|
||||||
@ -14,7 +24,20 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
|
|||||||
pub fn screen_boot_stage_2() {
|
pub fn screen_boot_stage_2() {
|
||||||
let mut frame = WelcomeScreen::new(false);
|
let mut frame = WelcomeScreen::new(false);
|
||||||
frame.place(screen());
|
frame.place(screen());
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
{
|
||||||
|
display::sync();
|
||||||
|
render_on_display(None, Some(Color::black()), |target| {
|
||||||
|
frame.render(target);
|
||||||
|
});
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
|
{
|
||||||
display::sync();
|
display::sync();
|
||||||
frame.paint();
|
frame.paint();
|
||||||
display::refresh();
|
display::refresh();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ use crate::{
|
|||||||
CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA,
|
CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,6 +106,15 @@ impl<'a> Component for Intro<'a> {
|
|||||||
self.menu.paint();
|
self.menu.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.title.render(target);
|
||||||
|
self.text.render(target);
|
||||||
|
self.warn.render(target);
|
||||||
|
self.host.render(target);
|
||||||
|
self.menu.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.menu.bounds(sink);
|
self.menu.bounds(sink);
|
||||||
|
@ -13,6 +13,7 @@ use crate::{
|
|||||||
X32,
|
X32,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,6 +109,14 @@ impl Component for Menu {
|
|||||||
self.reset.paint();
|
self.reset.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.title.render(target);
|
||||||
|
self.close.render(target);
|
||||||
|
self.reboot.render(target);
|
||||||
|
self.reset.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.close.bounds(sink);
|
self.close.bounds(sink);
|
||||||
|
@ -23,12 +23,31 @@ use super::{
|
|||||||
FIRE40, RESULT_FW_INSTALL, RESULT_INITIAL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL,
|
FIRE40, RESULT_FW_INSTALL, RESULT_INITIAL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL,
|
||||||
TEXT_WIPE_BOLD, TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
|
TEXT_WIPE_BOLD, TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
|
||||||
},
|
},
|
||||||
BACKLIGHT_NORMAL, BLACK, FG, WHITE,
|
BACKLIGHT_NORMAL, FG,
|
||||||
},
|
},
|
||||||
ModelTTFeatures,
|
ModelTTFeatures,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ui::{ui_features::UIFeaturesBootloader, UIFeaturesCommon};
|
use crate::ui::{ui_features::UIFeaturesBootloader, UIFeaturesCommon};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
|
use super::theme::BLACK;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use crate::ui::{
|
||||||
|
constant,
|
||||||
|
display::toif::Toif,
|
||||||
|
geometry::{Alignment, Alignment2D, Offset},
|
||||||
|
shape,
|
||||||
|
shape::render_on_display,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use ufmt::uwrite;
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
use super::theme::bootloader::BLD_WARN_COLOR;
|
||||||
|
|
||||||
use intro::Intro;
|
use intro::Intro;
|
||||||
use menu::Menu;
|
use menu::Menu;
|
||||||
|
|
||||||
@ -43,6 +62,7 @@ const RECONNECT_MESSAGE: &str = "PLEASE RECONNECT\nTHE DEVICE";
|
|||||||
const SCREEN: Rect = ModelTTFeatures::SCREEN;
|
const SCREEN: Rect = ModelTTFeatures::SCREEN;
|
||||||
|
|
||||||
impl ModelTTFeatures {
|
impl ModelTTFeatures {
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn screen_progress(
|
fn screen_progress(
|
||||||
text: &str,
|
text: &str,
|
||||||
progress: u16,
|
progress: u16,
|
||||||
@ -70,6 +90,62 @@ impl ModelTTFeatures {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_progress(
|
||||||
|
text: &str,
|
||||||
|
progress: u16,
|
||||||
|
initialize: bool,
|
||||||
|
fg_color: Color,
|
||||||
|
bg_color: Color,
|
||||||
|
icon: Option<(Icon, Color)>,
|
||||||
|
) {
|
||||||
|
if initialize {
|
||||||
|
Self::fadeout();
|
||||||
|
}
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(bg_color), |target| {
|
||||||
|
shape::Text::new(Point::new(SCREEN.width() / 2, SCREEN.height() - 45), text)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let center = SCREEN.center() + Offset::y(-20);
|
||||||
|
|
||||||
|
let inactive_color = bg_color.blend(fg_color, 85);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(inactive_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||||
|
.with_bg(fg_color)
|
||||||
|
.with_end_angle(((progress as i32 * shape::PI4 as i32 * 8) / 1000) as i16)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_INNER + 2)
|
||||||
|
.with_bg(fg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, constant::LOADER_INNER)
|
||||||
|
.with_bg(bg_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
if let Some((icon, color)) = icon {
|
||||||
|
shape::ToifImage::new(center, icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
if initialize {
|
||||||
|
Self::fadein();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn screen_install_success_bld(msg: &str, complete_draw: bool) {
|
fn screen_install_success_bld(msg: &str, complete_draw: bool) {
|
||||||
let mut frame = ResultScreen::new(
|
let mut frame = ResultScreen::new(
|
||||||
&RESULT_FW_INSTALL,
|
&RESULT_FW_INSTALL,
|
||||||
@ -99,12 +175,13 @@ impl UIFeaturesBootloader for ModelTTFeatures {
|
|||||||
show(&mut frame, true);
|
show(&mut frame, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
fn bld_continue_label(bg_color: Color) {
|
fn bld_continue_label(bg_color: Color) {
|
||||||
display::text_center(
|
display::text_center(
|
||||||
Point::new(SCREEN.width() / 2, SCREEN.height() - 5),
|
Point::new(SCREEN.width() / 2, SCREEN.height() - 5),
|
||||||
"click to continue ...",
|
"click to continue ...",
|
||||||
Font::NORMAL,
|
Font::NORMAL,
|
||||||
WHITE,
|
BLD_FG,
|
||||||
bg_color,
|
bg_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -126,7 +203,6 @@ impl UIFeaturesBootloader for ModelTTFeatures {
|
|||||||
} else {
|
} else {
|
||||||
Self::screen_install_success_bld(reboot_msg.as_str(), complete_draw)
|
Self::screen_install_success_bld(reboot_msg.as_str(), complete_draw)
|
||||||
}
|
}
|
||||||
display::refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screen_install_fail() {
|
fn screen_install_fail() {
|
||||||
@ -250,6 +326,7 @@ impl UIFeaturesBootloader for ModelTTFeatures {
|
|||||||
Self::fadeout();
|
Self::fadeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "new_rendering"))]
|
||||||
display::rect_fill(SCREEN, BLACK);
|
display::rect_fill(SCREEN, BLACK);
|
||||||
|
|
||||||
let mut frame = WelcomeScreen::new(true);
|
let mut frame = WelcomeScreen::new(true);
|
||||||
@ -260,7 +337,6 @@ impl UIFeaturesBootloader for ModelTTFeatures {
|
|||||||
} else {
|
} else {
|
||||||
display::set_backlight(BACKLIGHT_NORMAL);
|
display::set_backlight(BACKLIGHT_NORMAL);
|
||||||
}
|
}
|
||||||
display::refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screen_wipe_progress(progress: u16, initialize: bool) {
|
fn screen_wipe_progress(progress: u16, initialize: bool) {
|
||||||
@ -317,4 +393,89 @@ impl UIFeaturesBootloader for ModelTTFeatures {
|
|||||||
);
|
);
|
||||||
show(&mut frame, true);
|
show(&mut frame, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "new_rendering")]
|
||||||
|
fn screen_boot(
|
||||||
|
warning: bool,
|
||||||
|
vendor_str: Option<&str>,
|
||||||
|
version: [u8; 4],
|
||||||
|
vendor_img: &[u8],
|
||||||
|
wait: i32,
|
||||||
|
) {
|
||||||
|
let bg_color = if warning { BLD_WARN_COLOR } else { BLD_BG };
|
||||||
|
|
||||||
|
display::sync();
|
||||||
|
|
||||||
|
render_on_display(None, Some(bg_color), |target| {
|
||||||
|
// Draw vendor image if it's valid and has size of 120x120
|
||||||
|
if let Ok(toif) = Toif::new(vendor_img) {
|
||||||
|
if (toif.width() == 120) && (toif.height() == 120) {
|
||||||
|
// Image position depends on the vendor string presence
|
||||||
|
let pos = if vendor_str.is_some() {
|
||||||
|
Point::new(SCREEN.width() / 2, 30)
|
||||||
|
} else {
|
||||||
|
Point::new(SCREEN.width() / 2, 60)
|
||||||
|
};
|
||||||
|
|
||||||
|
shape::ToifImage::new(pos, toif)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw vendor string if present
|
||||||
|
if let Some(text) = vendor_str {
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 50);
|
||||||
|
shape::Text::new(pos, text)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG) //COLOR_BL_BG
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 25);
|
||||||
|
|
||||||
|
let mut version_text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(
|
||||||
|
version_text,
|
||||||
|
"{}.{}.{}",
|
||||||
|
version[0],
|
||||||
|
version[1],
|
||||||
|
version[2]
|
||||||
|
));
|
||||||
|
|
||||||
|
shape::Text::new(pos, version_text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a message
|
||||||
|
match wait.cmp(&0) {
|
||||||
|
core::cmp::Ordering::Equal => {}
|
||||||
|
core::cmp::Ordering::Greater => {
|
||||||
|
let mut text: BootloaderString = String::new();
|
||||||
|
unwrap!(uwrite!(text, "starting in {} s", wait));
|
||||||
|
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
|
||||||
|
shape::Text::new(pos, text.as_str())
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
core::cmp::Ordering::Less => {
|
||||||
|
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
|
||||||
|
shape::Text::new(pos, "click to continue ...")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(BLD_FG)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
display::refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
component::{Component, Event, EventCtx, Never, Pad},
|
component::{Component, Event, EventCtx, Never, Pad},
|
||||||
constant::screen,
|
constant::screen,
|
||||||
display::{self, Font, Icon},
|
display::{self, toif::Toif, Font, Icon},
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Offset, Rect},
|
||||||
model_tt::theme::{
|
model_tt::theme::{
|
||||||
bootloader::{START_URL, WELCOME_COLOR},
|
bootloader::{START_URL, WELCOME_COLOR},
|
||||||
BLACK, GREY_MEDIUM, WHITE,
|
BLACK, GREY_MEDIUM, WHITE,
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Welcome {
|
pub struct Welcome {
|
||||||
@ -56,4 +58,26 @@ impl Component for Welcome {
|
|||||||
BLACK,
|
BLACK,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(screen().top_center() + Offset::y(102), "Get started with")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY_MEDIUM)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Text::new(screen().top_center() + Offset::y(126), "your Trezor at")
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(GREY_MEDIUM)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let icon = unwrap!(Toif::new(START_URL));
|
||||||
|
shape::ToifImage::new(screen().top_center() + Offset::y(135), icon)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
Component, Event, EventCtx, Paginate, Qr,
|
Component, Event, EventCtx, Paginate, Qr,
|
||||||
},
|
},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,6 +177,14 @@ impl Component for AddressDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
match self.current_page {
|
||||||
|
0 => self.qr_code.render(target),
|
||||||
|
1 => self.details.render(target),
|
||||||
|
_ => self.xpub_view.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
match self.current_page {
|
match self.current_page {
|
||||||
|
@ -18,6 +18,8 @@ use crate::{
|
|||||||
WHITE,
|
WHITE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -237,6 +239,40 @@ impl Component for Confirm<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
self.content_pad.render(target);
|
||||||
|
|
||||||
|
if let Some(info) = self.info.as_ref() {
|
||||||
|
if self.show_info {
|
||||||
|
info.close_button.render(target);
|
||||||
|
info.title.render(target);
|
||||||
|
info.text.render(target);
|
||||||
|
self.left_button.render(target);
|
||||||
|
self.right_button.render(target);
|
||||||
|
// short-circuit before painting the main components
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
info.info_button.render(target);
|
||||||
|
// pass through to the rest of the paint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.message.render(target);
|
||||||
|
self.alert.render(target);
|
||||||
|
self.left_button.render(target);
|
||||||
|
self.right_button.render(target);
|
||||||
|
match &self.title {
|
||||||
|
ConfirmTitle::Text(label) => label.render(target),
|
||||||
|
ConfirmTitle::Icon(icon) => {
|
||||||
|
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.left_button.bounds(sink);
|
self.left_button.bounds(sink);
|
||||||
|
@ -10,6 +10,8 @@ use crate::{
|
|||||||
display::{self, toif::Icon, Color, Font},
|
display::{self, toif::Icon, Color, Font},
|
||||||
event::TouchEvent,
|
event::TouchEvent,
|
||||||
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
geometry::{Alignment2D, Insets, Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -194,6 +196,18 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::IconBlend(_, _, _) => {}
|
||||||
|
_ => shape::Bar::new(self.area)
|
||||||
|
.with_bg(style.button_color)
|
||||||
|
.with_fg(style.border_color)
|
||||||
|
.with_thickness(style.border_width)
|
||||||
|
.with_radius(style.border_radius as i16)
|
||||||
|
.render(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paint_content(&self, style: &ButtonStyle) {
|
pub fn paint_content(&self, style: &ButtonStyle) {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
ButtonContent::Empty => {}
|
ButtonContent::Empty => {}
|
||||||
@ -232,6 +246,45 @@ impl Button {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::Empty => {}
|
||||||
|
ButtonContent::Text(text) => {
|
||||||
|
let width = text.map(|c| style.font.text_width(c));
|
||||||
|
let height = style.font.text_height();
|
||||||
|
let start_of_baseline = self.area.center()
|
||||||
|
+ Offset::new(-width / 2, height / 2)
|
||||||
|
+ Offset::y(Self::BASELINE_OFFSET);
|
||||||
|
text.map(|text| {
|
||||||
|
shape::Text::new(start_of_baseline, text)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ButtonContent::Icon(icon) => {
|
||||||
|
shape::ToifImage::new(self.area.center(), icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
ButtonContent::IconAndText(child) => {
|
||||||
|
child.render(target, self.area, self.style(), Self::BASELINE_OFFSET);
|
||||||
|
}
|
||||||
|
ButtonContent::IconBlend(bg, fg, offset) => {
|
||||||
|
shape::Bar::new(self.area)
|
||||||
|
.with_bg(style.background_color)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(self.area.top_left(), bg.toif)
|
||||||
|
.with_fg(style.button_color)
|
||||||
|
.render(target);
|
||||||
|
shape::ToifImage::new(self.area.top_left() + *offset, fg.toif)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Button {
|
impl Component for Button {
|
||||||
@ -324,6 +377,12 @@ impl Component for Button {
|
|||||||
self.paint_content(style);
|
self.paint_content(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let style = self.style();
|
||||||
|
self.render_background(target, style);
|
||||||
|
self.render_content(target, style);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area);
|
sink(self.area);
|
||||||
@ -583,4 +642,52 @@ impl IconText {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<'s>(
|
||||||
|
&self,
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
area: Rect,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
baseline_offset: i16,
|
||||||
|
) {
|
||||||
|
let width = style.font.text_width(self.text);
|
||||||
|
let height = style.font.text_height();
|
||||||
|
|
||||||
|
let mut use_icon = false;
|
||||||
|
let mut use_text = false;
|
||||||
|
|
||||||
|
let mut icon_pos = Point::new(
|
||||||
|
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
|
||||||
|
area.center().y,
|
||||||
|
);
|
||||||
|
let mut text_pos =
|
||||||
|
area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset);
|
||||||
|
|
||||||
|
if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) {
|
||||||
|
//display both icon and text
|
||||||
|
text_pos = Point::new(area.top_left().x + Self::ICON_SPACE, text_pos.y);
|
||||||
|
use_text = true;
|
||||||
|
use_icon = true;
|
||||||
|
} else if area.width() > (width + Self::TEXT_MARGIN) {
|
||||||
|
use_text = true;
|
||||||
|
} else {
|
||||||
|
//if we can't fit the text, retreat to centering the icon
|
||||||
|
icon_pos = area.center();
|
||||||
|
use_icon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_text {
|
||||||
|
shape::Text::new(text_pos, self.text)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_icon {
|
||||||
|
shape::ToifImage::new(icon_pos, self.icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ use crate::{
|
|||||||
translations::TR,
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label,
|
base::Never, Bar, Child, Component, ComponentExt, Empty, Event, EventCtx, Label, Split,
|
||||||
Split,
|
|
||||||
},
|
},
|
||||||
|
constant,
|
||||||
display::loader::{loader_circular_uncompress, LoaderDimensions},
|
display::loader::{loader_circular_uncompress, LoaderDimensions},
|
||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Offset, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::animation_disabled,
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -42,7 +44,7 @@ impl<U> CoinJoinProgress<U> {
|
|||||||
let style = theme::label_coinjoin_progress();
|
let style = theme::label_coinjoin_progress();
|
||||||
let label = Label::centered(TR::coinjoin__title_do_not_disconnect.into(), style)
|
let label = Label::centered(TR::coinjoin__title_do_not_disconnect.into(), style)
|
||||||
.vertically_centered();
|
.vertically_centered();
|
||||||
let bg = painter::rect_painter(style.background_color, theme::BG);
|
let bg = Bar::new(style.background_color, theme::BG, 2);
|
||||||
let inner = (bg, label);
|
let inner = (bg, label);
|
||||||
CoinJoinProgress::with_background(text, inner, indeterminate)
|
CoinJoinProgress::with_background(text, inner, indeterminate)
|
||||||
}
|
}
|
||||||
@ -122,6 +124,40 @@ where
|
|||||||
);
|
);
|
||||||
self.label.paint();
|
self.label.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.content.render(target);
|
||||||
|
|
||||||
|
let center = constant::screen().center() + Offset::y(LOADER_OFFSET);
|
||||||
|
let active_color = theme::FG;
|
||||||
|
let background_color = theme::BG;
|
||||||
|
let inactive_color = background_color.blend(active_color, 85);
|
||||||
|
|
||||||
|
let start = (self.value as i16 - 100) % 1000;
|
||||||
|
let end = (self.value as i16 + 100) % 1000;
|
||||||
|
let start = ((start as i32 * 8 * shape::PI4 as i32) / 1000) as i16;
|
||||||
|
let end = ((end as i32 * 8 * shape::PI4 as i32) / 1000) as i16;
|
||||||
|
|
||||||
|
shape::Circle::new(center, LOADER_OUTER)
|
||||||
|
.with_bg(inactive_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, LOADER_OUTER)
|
||||||
|
.with_bg(active_color)
|
||||||
|
.with_start_angle(start)
|
||||||
|
.with_end_angle(end)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, LOADER_INNER + 2)
|
||||||
|
.with_bg(active_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
shape::Circle::new(center, LOADER_INNER)
|
||||||
|
.with_bg(background_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.label.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
Child, Component, Event, EventCtx, Never,
|
Child, Component, Event, EventCtx, Never,
|
||||||
},
|
},
|
||||||
geometry::{Insets, LinearPlacement, Rect},
|
geometry::{Insets, LinearPlacement, Rect},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,6 +72,11 @@ where
|
|||||||
self.controls.paint();
|
self.controls.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.content.render(target);
|
||||||
|
self.controls.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.content.bounds(sink);
|
self.content.bounds(sink);
|
||||||
@ -196,6 +202,12 @@ where
|
|||||||
self.controls.paint();
|
self.controls.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.image.render(target);
|
||||||
|
self.paragraphs.render(target);
|
||||||
|
self.controls.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.image.bounds(sink);
|
self.image.bounds(sink);
|
||||||
|
@ -4,6 +4,8 @@ use crate::{
|
|||||||
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
|
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
|
||||||
constant::screen,
|
constant::screen,
|
||||||
geometry::{Alignment2D, Point, Rect},
|
geometry::{Alignment2D, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,4 +91,19 @@ impl<'a> Component for ErrorScreen<'a> {
|
|||||||
self.message.paint();
|
self.message.paint();
|
||||||
self.footer.paint();
|
self.footer.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.bg.render(target);
|
||||||
|
|
||||||
|
let icon = ICON_WARNING40;
|
||||||
|
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
|
||||||
|
.with_fg(WHITE)
|
||||||
|
.with_bg(FATAL_ERROR_COLOR)
|
||||||
|
.with_align(Alignment2D::TOP_CENTER)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
self.title.render(target);
|
||||||
|
self.message.render(target);
|
||||||
|
self.footer.render(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ use crate::{
|
|||||||
swipe::{Swipe, SwipeDirection},
|
swipe::{Swipe, SwipeDirection},
|
||||||
theme, ScrollBar,
|
theme, ScrollBar,
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -211,6 +213,36 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.icon.render(target);
|
||||||
|
self.controls.render(target);
|
||||||
|
self.app_name.render(target);
|
||||||
|
|
||||||
|
if self.scrollbar.page_count > 1 {
|
||||||
|
self.scrollbar.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erasing the old text content before writing the new one.
|
||||||
|
let account_name_area = self.account_name.area();
|
||||||
|
let real_area = account_name_area
|
||||||
|
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
|
||||||
|
shape::Bar::new(real_area).with_bg(theme::BG).render(target);
|
||||||
|
|
||||||
|
// Account name is optional.
|
||||||
|
// Showing it only if it differs from app name.
|
||||||
|
// (Dummy requests usually have some text as both app_name and account_name.)
|
||||||
|
let account_name = self.account_name.text();
|
||||||
|
let app_name = self.app_name.text();
|
||||||
|
if !account_name.is_empty() && account_name != app_name {
|
||||||
|
self.account_name.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.fade.take() {
|
||||||
|
// Note that this is blocking and takes some time.
|
||||||
|
display::fade_backlight(theme::BACKLIGHT_NORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.icon.bounds(sink);
|
self.icon.bounds(sink);
|
||||||
|
@ -8,6 +8,7 @@ use crate::{
|
|||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::{Alignment, Insets, Offset, Rect},
|
geometry::{Alignment, Insets, Offset, Rect},
|
||||||
model_tt::component::{Button, ButtonMsg, CancelInfoConfirmMsg},
|
model_tt::component::{Button, ButtonMsg, CancelInfoConfirmMsg},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -175,6 +176,13 @@ where
|
|||||||
self.content.paint();
|
self.content.paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.title.render(target);
|
||||||
|
self.subtitle.render(target);
|
||||||
|
self.button.render(target);
|
||||||
|
self.content.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.title.bounds(sink);
|
self.title.bounds(sink);
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
io::BinaryData,
|
||||||
strutil::TString,
|
strutil::TString,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
translations::TR,
|
translations::TR,
|
||||||
trezorhal::usb::usb_configured,
|
trezorhal::usb::usb_configured,
|
||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, Pad, TimerToken},
|
component::{Component, Event, EventCtx, Pad, TimerToken},
|
||||||
display::{self, tjpgd::jpeg_info, toif::Icon, Color, Font},
|
display::{
|
||||||
|
self,
|
||||||
|
image::{ImageInfo, ToifFormat},
|
||||||
|
toif::{Icon, Toif},
|
||||||
|
Color, Font,
|
||||||
|
},
|
||||||
event::{TouchEvent, USBEvent},
|
event::{TouchEvent, USBEvent},
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
|
||||||
layout::util::get_user_custom_image,
|
layout::util::get_user_custom_image,
|
||||||
model_tt::{constant, theme::IMAGE_HOMESCREEN},
|
model_tt::{constant, theme::IMAGE_HOMESCREEN},
|
||||||
|
shape::{self, Renderer},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,10 +26,7 @@ use crate::{
|
|||||||
trezorhal::{buffers::BufferJpegWork, uzlib::UZLIB_WINDOW_SIZE},
|
trezorhal::{buffers::BufferJpegWork, uzlib::UZLIB_WINDOW_SIZE},
|
||||||
ui::{
|
ui::{
|
||||||
constant::HEIGHT,
|
constant::HEIGHT,
|
||||||
display::{
|
display::tjpgd::BufferInput,
|
||||||
tjpgd::{jpeg_test, BufferInput},
|
|
||||||
toif::{Toif, ToifFormat},
|
|
||||||
},
|
|
||||||
model_tt::component::homescreen::render::{
|
model_tt::component::homescreen::render::{
|
||||||
HomescreenJpeg, HomescreenToif, HOMESCREEN_TOIF_SIZE,
|
HomescreenJpeg, HomescreenToif, HOMESCREEN_TOIF_SIZE,
|
||||||
},
|
},
|
||||||
@ -49,6 +53,7 @@ const LOADER_DURATION: Duration = Duration::from_millis(2000);
|
|||||||
pub struct Homescreen {
|
pub struct Homescreen {
|
||||||
label: TString<'static>,
|
label: TString<'static>,
|
||||||
notification: Option<(TString<'static>, u8)>,
|
notification: Option<(TString<'static>, u8)>,
|
||||||
|
image: BinaryData<'static>,
|
||||||
hold_to_lock: bool,
|
hold_to_lock: bool,
|
||||||
loader: Loader,
|
loader: Loader,
|
||||||
pad: Pad,
|
pad: Pad,
|
||||||
@ -69,6 +74,7 @@ impl Homescreen {
|
|||||||
Self {
|
Self {
|
||||||
label,
|
label,
|
||||||
notification,
|
notification,
|
||||||
|
image: get_homescreen_image(),
|
||||||
hold_to_lock,
|
hold_to_lock,
|
||||||
loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
|
loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
|
||||||
pad: Pad::with_background(theme::BG),
|
pad: Pad::with_background(theme::BG),
|
||||||
@ -119,6 +125,16 @@ impl Homescreen {
|
|||||||
self.loader.paint()
|
self.loader.paint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_loader<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
TR::progress__locking_device.map_translated(|t| {
|
||||||
|
shape::Text::new(TOP_CENTER + Offset::y(HOLD_Y), t)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(Font::NORMAL)
|
||||||
|
.with_fg(theme::FG);
|
||||||
|
});
|
||||||
|
self.loader.render(target)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_paint_notification(&mut self) {
|
pub fn set_paint_notification(&mut self) {
|
||||||
self.paint_notification_only = true;
|
self.paint_notification_only = true;
|
||||||
}
|
}
|
||||||
@ -210,12 +226,11 @@ impl Component for Homescreen {
|
|||||||
|
|
||||||
let notification = self.get_notification();
|
let notification = self.get_notification();
|
||||||
|
|
||||||
let res = get_user_custom_image();
|
match ImageInfo::parse(self.image) {
|
||||||
let mut show_default = true;
|
ImageInfo::Jpeg(_) => {
|
||||||
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
if let Ok(data) = res {
|
// discarded before returning to micropython.
|
||||||
if is_image_jpeg(data.as_ref()) {
|
let input = BufferInput(unsafe { self.image.data() });
|
||||||
let input = BufferInput(data.as_ref());
|
|
||||||
let mut pool = BufferJpegWork::get_cleared();
|
let mut pool = BufferJpegWork::get_cleared();
|
||||||
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
||||||
homescreen(
|
homescreen(
|
||||||
@ -224,9 +239,11 @@ impl Component for Homescreen {
|
|||||||
notification,
|
notification,
|
||||||
self.paint_notification_only,
|
self.paint_notification_only,
|
||||||
);
|
);
|
||||||
show_default = false;
|
}
|
||||||
} else if is_image_toif(data.as_ref()) {
|
ImageInfo::Toif(_) => {
|
||||||
let input = unwrap!(Toif::new(data.as_ref()));
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
|
// discarded before returning to micropython.
|
||||||
|
let input = unwrap!(Toif::new(unsafe { self.image.data() }));
|
||||||
let mut window = [0; UZLIB_WINDOW_SIZE];
|
let mut window = [0; UZLIB_WINDOW_SIZE];
|
||||||
let mut hs_img =
|
let mut hs_img =
|
||||||
HomescreenToif::new(input.decompression_context(Some(&mut window)));
|
HomescreenToif::new(input.decompression_context(Some(&mut window)));
|
||||||
@ -236,20 +253,87 @@ impl Component for Homescreen {
|
|||||||
notification,
|
notification,
|
||||||
self.paint_notification_only,
|
self.paint_notification_only,
|
||||||
);
|
);
|
||||||
show_default = false;
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_default {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let input = BufferInput(IMAGE_HOMESCREEN);
|
self.pad.render(target);
|
||||||
let mut pool = BufferJpegWork::get_cleared();
|
if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) {
|
||||||
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
self.render_loader(target);
|
||||||
homescreen(
|
} else {
|
||||||
&mut hs_img,
|
match ImageInfo::parse(self.image) {
|
||||||
&[text],
|
ImageInfo::Jpeg(_) => {
|
||||||
notification,
|
shape::JpegImage::new_image(self.pad.area.center(), self.image)
|
||||||
self.paint_notification_only,
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.render(target)
|
||||||
|
}
|
||||||
|
ImageInfo::Toif(_) => {
|
||||||
|
shape::ToifImage::new_image(self.pad.area.center(), self.image)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.render(target)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label.map(|t| {
|
||||||
|
let r = Rect::new(Point::new(6, 198), Point::new(234, 233));
|
||||||
|
shape::Bar::new(r)
|
||||||
|
.with_bg(Color::black())
|
||||||
|
.with_alpha(89)
|
||||||
|
.with_radius(3)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let style = theme::TEXT_DEMIBOLD;
|
||||||
|
let pos = Point::new(self.pad.area.center().x, LABEL_Y);
|
||||||
|
shape::Text::new(pos, t)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_font(style.text_font)
|
||||||
|
.with_fg(theme::FG)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(notif) = self.get_notification() {
|
||||||
|
const NOTIFICATION_HEIGHT: i16 = 36;
|
||||||
|
const NOTIFICATION_BORDER: i16 = 6;
|
||||||
|
const TEXT_ICON_SPACE: i16 = 8;
|
||||||
|
|
||||||
|
let banner = self
|
||||||
|
.pad
|
||||||
|
.area
|
||||||
|
.inset(Insets::sides(NOTIFICATION_BORDER))
|
||||||
|
.with_height(NOTIFICATION_HEIGHT)
|
||||||
|
.translate(Offset::y(NOTIFICATION_BORDER));
|
||||||
|
|
||||||
|
shape::Bar::new(banner)
|
||||||
|
.with_radius(2)
|
||||||
|
.with_bg(notif.color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
notif.text.map(|t| {
|
||||||
|
let style = theme::TEXT_BOLD;
|
||||||
|
let icon_width = notif.icon.toif.width() + TEXT_ICON_SPACE;
|
||||||
|
let text_pos = Point::new(
|
||||||
|
style
|
||||||
|
.text_font
|
||||||
|
.horz_center(banner.x0 + icon_width, banner.x1, t),
|
||||||
|
style.text_font.vert_center(banner.y0, banner.y1, "A"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
shape::Text::new(text_pos, t)
|
||||||
|
.with_font(style.text_font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let icon_pos = Point::new(text_pos.x - icon_width, banner.center().y);
|
||||||
|
|
||||||
|
shape::ToifImage::new(icon_pos, notif.icon.toif)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.with_align(Alignment2D::CENTER_LEFT)
|
||||||
|
.render(target);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,6 +355,7 @@ impl crate::trace::Trace for Homescreen {
|
|||||||
|
|
||||||
pub struct Lockscreen<'a> {
|
pub struct Lockscreen<'a> {
|
||||||
label: TString<'a>,
|
label: TString<'a>,
|
||||||
|
image: BinaryData<'a>,
|
||||||
bootscreen: bool,
|
bootscreen: bool,
|
||||||
coinjoin_authorized: bool,
|
coinjoin_authorized: bool,
|
||||||
}
|
}
|
||||||
@ -279,6 +364,7 @@ impl<'a> Lockscreen<'a> {
|
|||||||
pub fn new(label: TString<'a>, bootscreen: bool, coinjoin_authorized: bool) -> Self {
|
pub fn new(label: TString<'a>, bootscreen: bool, coinjoin_authorized: bool) -> Self {
|
||||||
Lockscreen {
|
Lockscreen {
|
||||||
label,
|
label,
|
||||||
|
image: get_homescreen_image(),
|
||||||
bootscreen,
|
bootscreen,
|
||||||
coinjoin_authorized,
|
coinjoin_authorized,
|
||||||
}
|
}
|
||||||
@ -343,61 +429,146 @@ impl Component for Lockscreen<'_> {
|
|||||||
texts = &texts[1..];
|
texts = &texts[1..];
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = get_user_custom_image();
|
match ImageInfo::parse(self.image) {
|
||||||
let mut show_default = true;
|
ImageInfo::Jpeg(_) => {
|
||||||
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
if let Ok(data) = res {
|
// discarded before returning to micropython.
|
||||||
if is_image_jpeg(data.as_ref()) {
|
let input = BufferInput(unsafe { self.image.data() });
|
||||||
let input = BufferInput(data.as_ref());
|
|
||||||
let mut pool = BufferJpegWork::get_cleared();
|
let mut pool = BufferJpegWork::get_cleared();
|
||||||
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
||||||
homescreen_blurred(&mut hs_img, texts);
|
homescreen_blurred(&mut hs_img, texts);
|
||||||
show_default = false;
|
}
|
||||||
} else if is_image_toif(data.as_ref()) {
|
ImageInfo::Toif(_) => {
|
||||||
let input = unwrap!(Toif::new(data.as_ref()));
|
// SAFETY: We expect no existing mutable reference. Resulting reference is
|
||||||
|
// discarded before returning to micropython.
|
||||||
|
let input = unwrap!(Toif::new(unsafe { self.image.data() }));
|
||||||
let mut window = [0; UZLIB_WINDOW_SIZE];
|
let mut window = [0; UZLIB_WINDOW_SIZE];
|
||||||
let mut hs_img =
|
let mut hs_img =
|
||||||
HomescreenToif::new(input.decompression_context(Some(&mut window)));
|
HomescreenToif::new(input.decompression_context(Some(&mut window)));
|
||||||
homescreen_blurred(&mut hs_img, texts);
|
homescreen_blurred(&mut hs_img, texts);
|
||||||
show_default = false;
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_default {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let input = BufferInput(IMAGE_HOMESCREEN);
|
let center = constant::screen().center();
|
||||||
let mut pool = BufferJpegWork::get_cleared();
|
|
||||||
let mut hs_img = HomescreenJpeg::new(input, pool.buffer.as_mut_slice());
|
match ImageInfo::parse(self.image) {
|
||||||
homescreen_blurred(&mut hs_img, texts);
|
ImageInfo::Jpeg(_) => shape::JpegImage::new_image(center, self.image)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_blur(4)
|
||||||
|
.with_dim(140)
|
||||||
|
.render(target),
|
||||||
|
ImageInfo::Toif(_) => shape::ToifImage::new_image(center, self.image)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
//.with_blur(5)
|
||||||
|
.render(target),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (locked, tap) = if self.bootscreen {
|
||||||
|
(
|
||||||
|
TR::lockscreen__title_not_connected,
|
||||||
|
TR::lockscreen__tap_to_connect,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(TR::lockscreen__title_locked, TR::lockscreen__tap_to_unlock)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut label_style = theme::TEXT_DEMIBOLD;
|
||||||
|
label_style.text_color = theme::GREY_LIGHT;
|
||||||
|
|
||||||
|
let mut texts: &[HomescreenText] = &[
|
||||||
|
HomescreenText {
|
||||||
|
text: "".into(),
|
||||||
|
style: theme::TEXT_NORMAL,
|
||||||
|
offset: Offset::new(2, COINJOIN_Y),
|
||||||
|
icon: Some(theme::ICON_COINJOIN),
|
||||||
|
},
|
||||||
|
HomescreenText {
|
||||||
|
text: locked.into(),
|
||||||
|
style: theme::TEXT_BOLD,
|
||||||
|
offset: Offset::y(LOCKED_Y),
|
||||||
|
icon: Some(theme::ICON_LOCK),
|
||||||
|
},
|
||||||
|
HomescreenText {
|
||||||
|
text: tap.into(),
|
||||||
|
style: theme::TEXT_NORMAL,
|
||||||
|
offset: Offset::y(TAP_Y),
|
||||||
|
icon: None,
|
||||||
|
},
|
||||||
|
HomescreenText {
|
||||||
|
text: self.label,
|
||||||
|
style: label_style,
|
||||||
|
offset: Offset::y(LABEL_Y),
|
||||||
|
icon: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if !self.coinjoin_authorized {
|
||||||
|
texts = &texts[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in texts.iter() {
|
||||||
|
item.text.map(|t| {
|
||||||
|
const TEXT_ICON_SPACE: i16 = 2;
|
||||||
|
|
||||||
|
let icon_width = match item.icon {
|
||||||
|
Some(icon) => icon.toif.width() + TEXT_ICON_SPACE,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let area = constant::screen();
|
||||||
|
|
||||||
|
let text_pos = Point::new(
|
||||||
|
item.style
|
||||||
|
.text_font
|
||||||
|
.horz_center(area.x0 + icon_width, area.x1, t),
|
||||||
|
0,
|
||||||
|
) + item.offset;
|
||||||
|
|
||||||
|
shape::Text::new(text_pos, t)
|
||||||
|
.with_font(item.style.text_font)
|
||||||
|
.with_fg(item.style.text_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
if let Some(icon) = item.icon {
|
||||||
|
let icon_pos = Point::new(text_pos.x - icon_width, text_pos.y);
|
||||||
|
shape::ToifImage::new(icon_pos, icon.toif)
|
||||||
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
|
.with_fg(item.style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_homescreen_format(buffer: &[u8]) -> bool {
|
pub fn check_homescreen_format(image: BinaryData, accept_toif: bool) -> bool {
|
||||||
is_image_jpeg(buffer) && jpeg_test(buffer)
|
match ImageInfo::parse(image) {
|
||||||
|
ImageInfo::Jpeg(info) => {
|
||||||
|
info.width() == HOMESCREEN_IMAGE_WIDTH
|
||||||
|
&& info.height() == HOMESCREEN_IMAGE_HEIGHT
|
||||||
|
&& info.mcu_height() <= 16
|
||||||
|
}
|
||||||
|
ImageInfo::Toif(info) => {
|
||||||
|
accept_toif
|
||||||
|
&& info.width() == HOMESCREEN_TOIF_SIZE
|
||||||
|
&& info.height() == HOMESCREEN_TOIF_SIZE
|
||||||
|
&& info.format() == ToifFormat::FullColorBE
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_image_jpeg(buffer: &[u8]) -> bool {
|
fn get_homescreen_image() -> BinaryData<'static> {
|
||||||
let jpeg = jpeg_info(buffer);
|
if let Ok(image) = get_user_custom_image() {
|
||||||
if let Some((size, mcu_height)) = jpeg {
|
if check_homescreen_format(image, true) {
|
||||||
if size.x == HOMESCREEN_IMAGE_WIDTH && size.y == HOMESCREEN_IMAGE_HEIGHT && mcu_height <= 16
|
return image;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
IMAGE_HOMESCREEN.into()
|
||||||
}
|
|
||||||
|
|
||||||
fn is_image_toif(buffer: &[u8]) -> bool {
|
|
||||||
let toif = Toif::new(buffer);
|
|
||||||
if let Ok(toif) = toif {
|
|
||||||
if toif.size().x == HOMESCREEN_TOIF_SIZE
|
|
||||||
&& toif.size().y == HOMESCREEN_TOIF_SIZE
|
|
||||||
&& toif.format() == ToifFormat::FullColorBE
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
|
@ -7,13 +7,15 @@ use crate::{
|
|||||||
model_tt::{
|
model_tt::{
|
||||||
component::{
|
component::{
|
||||||
keyboard::{
|
keyboard::{
|
||||||
common::{paint_pending_marker, MultiTapKeyboard},
|
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
||||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||||
},
|
},
|
||||||
Button, ButtonContent, ButtonMsg,
|
Button, ButtonContent, ButtonMsg,
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
@ -154,6 +156,51 @@ impl Component for Bip39Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let area = self.button.area();
|
||||||
|
let style = self.button.style();
|
||||||
|
|
||||||
|
// First, paint the button background.
|
||||||
|
self.button.render_background(target, style);
|
||||||
|
|
||||||
|
// Paint the entered content (the prefix of the suggested word).
|
||||||
|
let text = self.textbox.content();
|
||||||
|
let width = style.font.text_width(text);
|
||||||
|
// Content starts in the left-center point, offset by 16px to the right and 8px
|
||||||
|
// to the bottom.
|
||||||
|
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
|
||||||
|
shape::Text::new(text_baseline, text)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
// Paint the rest of the suggested dictionary word.
|
||||||
|
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
|
||||||
|
let word_baseline = text_baseline + Offset::new(width, 0);
|
||||||
|
let style = self.button_suggestion.style();
|
||||||
|
shape::Text::new(word_baseline, word)
|
||||||
|
.with_font(style.font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint the pending marker.
|
||||||
|
if self.multi_tap.pending_key().is_some() {
|
||||||
|
render_pending_marker(target, text_baseline, text, style.font, style.text_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint the icon.
|
||||||
|
if let ButtonContent::Icon(icon) = self.button.content() {
|
||||||
|
// Icon is painted in the right-center point, of expected size 16x16 pixels, and
|
||||||
|
// 16px from the right edge.
|
||||||
|
let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0);
|
||||||
|
shape::ToifImage::new(icon_center, icon.toif)
|
||||||
|
.with_align(Alignment2D::CENTER)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.button.bounds(sink);
|
self.button.bounds(sink);
|
||||||
|
@ -4,6 +4,8 @@ use crate::{
|
|||||||
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
|
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
|
||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::{Offset, Point, Rect},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,3 +129,24 @@ pub fn paint_pending_marker(text_baseline: Point, text: &str, font: Font, color:
|
|||||||
display::rect_fill(marker_rect, color);
|
display::rect_fill(marker_rect, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a visible "underscoring" of the last letter of a text.
|
||||||
|
pub fn render_pending_marker<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
text_baseline: Point,
|
||||||
|
text: &str,
|
||||||
|
font: Font,
|
||||||
|
color: Color,
|
||||||
|
) {
|
||||||
|
// Measure the width of the last character of input.
|
||||||
|
if let Some(last) = text.chars().last() {
|
||||||
|
let width = font.text_width(text);
|
||||||
|
let last_width = font.char_width(last);
|
||||||
|
// Draw the marker 2px under the start of the baseline of the last character.
|
||||||
|
let marker_origin = text_baseline + Offset::new(width - last_width, 2);
|
||||||
|
// Draw the marker 1px longer than the last character, and 3px thick.
|
||||||
|
let marker_rect =
|
||||||
|
Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3));
|
||||||
|
shape::Bar::new(marker_rect).with_bg(color).render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -183,6 +184,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
if self.input.inner().inner().is_empty() {
|
||||||
|
self.prompt.render(target);
|
||||||
|
} else {
|
||||||
|
self.input.render(target);
|
||||||
|
self.back.render(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
for btn in &self.keys {
|
||||||
|
btn.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.prompt.bounds(sink);
|
self.prompt.bounds(sink);
|
||||||
|
@ -8,10 +8,12 @@ use crate::{
|
|||||||
geometry::{Grid, Offset, Rect},
|
geometry::{Grid, Offset, Rect},
|
||||||
model_tt::component::{
|
model_tt::component::{
|
||||||
button::{Button, ButtonContent, ButtonMsg},
|
button::{Button, ButtonContent, ButtonMsg},
|
||||||
keyboard::common::{paint_pending_marker, MultiTapKeyboard},
|
keyboard::common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
||||||
swipe::{Swipe, SwipeDirection},
|
swipe::{Swipe, SwipeDirection},
|
||||||
theme, ScrollBar,
|
theme, ScrollBar,
|
||||||
},
|
},
|
||||||
|
shape,
|
||||||
|
shape::Renderer,
|
||||||
util::long_line_content_with_ellipsis,
|
util::long_line_content_with_ellipsis,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -299,6 +301,20 @@ impl Component for PassphraseKeyboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
self.input.render(target);
|
||||||
|
self.scrollbar.render(target);
|
||||||
|
self.confirm.render(target);
|
||||||
|
self.back.render(target);
|
||||||
|
for btn in &self.keys {
|
||||||
|
btn.render(target);
|
||||||
|
}
|
||||||
|
if self.fade.take() {
|
||||||
|
// Note that this is blocking and takes some time.
|
||||||
|
display::fade_backlight(theme::BACKLIGHT_NORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.input.bounds(sink);
|
self.input.bounds(sink);
|
||||||
@ -379,6 +395,40 @@ impl Component for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
let style = theme::label_keyboard();
|
||||||
|
|
||||||
|
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
||||||
|
- Offset::y(style.text_font.text_baseline());
|
||||||
|
|
||||||
|
let text = self.textbox.content();
|
||||||
|
|
||||||
|
shape::Bar::new(self.area).with_bg(theme::BG).render(target);
|
||||||
|
|
||||||
|
// Find out how much text can fit into the textbox.
|
||||||
|
// Accounting for the pending marker, which draws itself one pixel longer than
|
||||||
|
// the last character
|
||||||
|
let available_area_width = self.area.width() - 1;
|
||||||
|
let text_to_display =
|
||||||
|
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
|
||||||
|
|
||||||
|
shape::Text::new(text_baseline, &text_to_display)
|
||||||
|
.with_font(style.text_font)
|
||||||
|
.with_fg(style.text_color)
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
// Paint the pending marker.
|
||||||
|
if self.multi_tap.pending_key().is_some() {
|
||||||
|
render_pending_marker(
|
||||||
|
target,
|
||||||
|
text_baseline,
|
||||||
|
&text_to_display,
|
||||||
|
style.text_font,
|
||||||
|
style.text_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
sink(self.area)
|
sink(self.area)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user