parent
b905ac04ef
commit
2703d714c2
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file is part of the Trezor project, https://trezor.io/
|
||||
*
|
||||
* Copyright (c) SatoshiLabs
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "py/runtime.h"
|
||||
|
||||
#if MICROPY_PY_TREZORUI2
|
||||
|
||||
#include "librust.h"
|
||||
|
||||
/// def layout_new_example(text: str) -> None:
|
||||
/// """Example layout."""
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj,
|
||||
ui_layout_new_example);
|
||||
|
||||
STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorui2)},
|
||||
|
||||
{MP_ROM_QSTR(MP_QSTR_layout_new_example),
|
||||
MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)},
|
||||
};
|
||||
|
||||
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorui2_globals,
|
||||
mp_module_trezorui2_globals_table);
|
||||
|
||||
const mp_obj_module_t mp_module_trezorui2 = {
|
||||
.base = {&mp_type_module},
|
||||
.globals = (mp_obj_dict_t *)&mp_module_trezorui2_globals,
|
||||
};
|
||||
|
||||
MP_REGISTER_MODULE(MP_QSTR_trezorui2, mp_module_trezorui2,
|
||||
MICROPY_PY_TREZORUI2);
|
||||
|
||||
#endif // MICROPY_PY_TREZORUI2
|
@ -0,0 +1,38 @@
|
||||
/// Visitor passed into `Trace` types.
|
||||
pub trait Tracer {
|
||||
fn bytes(&mut self, b: &[u8]);
|
||||
fn string(&mut self, s: &str);
|
||||
fn symbol(&mut self, name: &str);
|
||||
fn open(&mut self, name: &str);
|
||||
fn field(&mut self, name: &str, value: &dyn Trace);
|
||||
fn close(&mut self);
|
||||
}
|
||||
|
||||
/// Value that can describe own structure and data using the `Tracer` interface.
|
||||
pub trait Trace {
|
||||
fn trace(&self, d: &mut dyn Tracer);
|
||||
}
|
||||
|
||||
impl Trace for &[u8] {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.bytes(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl Trace for &str {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.string(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Trace for Option<T>
|
||||
where
|
||||
T: Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn Tracer) {
|
||||
match self {
|
||||
Some(v) => v.trace(d),
|
||||
None => d.symbol("None"),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
use core::{mem, time::Duration};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::ui::geometry::Point;
|
||||
|
||||
/// Type used by components that do not return any messages.
|
||||
///
|
||||
/// Alternative to the yet-unstable `!`-type.
|
||||
pub enum Never {}
|
||||
|
||||
pub trait Component {
|
||||
type Msg;
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg>;
|
||||
fn paint(&mut self);
|
||||
}
|
||||
|
||||
pub struct Child<T> {
|
||||
component: T,
|
||||
marked_for_paint: bool,
|
||||
}
|
||||
|
||||
impl<T> Child<T> {
|
||||
pub fn new(component: T) -> Self {
|
||||
Self {
|
||||
component,
|
||||
marked_for_paint: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
&self.component
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.component
|
||||
}
|
||||
|
||||
pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U
|
||||
where
|
||||
F: FnOnce(&mut EventCtx, &mut T) -> U,
|
||||
{
|
||||
let paint_was_previously_requested = mem::replace(&mut ctx.paint_requested, false);
|
||||
let component_result = component_func(ctx, &mut self.component);
|
||||
if ctx.paint_requested {
|
||||
self.marked_for_paint = true;
|
||||
} else {
|
||||
ctx.paint_requested = paint_was_previously_requested;
|
||||
}
|
||||
component_result
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Child<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.mutate(ctx, |ctx, c| c.event(ctx, event))
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
if self.marked_for_paint {
|
||||
self.marked_for_paint = false;
|
||||
self.component.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Child<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
self.component.trace(t)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
TouchStart(Point),
|
||||
TouchMove(Point),
|
||||
TouchEnd(Point),
|
||||
Timer(TimerToken),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct TimerToken(usize);
|
||||
|
||||
impl TimerToken {
|
||||
/// Value of an invalid (or missing) token.
|
||||
pub const INVALID: TimerToken = TimerToken(0);
|
||||
|
||||
pub fn from_raw(raw: usize) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventCtx {
|
||||
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
|
||||
next_token: usize,
|
||||
paint_requested: bool,
|
||||
}
|
||||
|
||||
impl EventCtx {
|
||||
/// Maximum amount of timers requested in one event tick.
|
||||
const MAX_TIMERS: usize = 4;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
timers: Vec::new(),
|
||||
next_token: 1,
|
||||
paint_requested: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_paint(&mut self) {
|
||||
self.paint_requested = true;
|
||||
}
|
||||
|
||||
pub fn clear_paint_requests(&mut self) {
|
||||
self.paint_requested = false;
|
||||
}
|
||||
|
||||
pub fn request_timer(&mut self, deadline: Duration) -> TimerToken {
|
||||
let token = self.next_timer_token();
|
||||
if self.timers.push((token, deadline)).is_err() {
|
||||
// The timer queue is full. Let's just ignore this request.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Timer queue is full");
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
pub fn pop_timer(&mut self) -> Option<(TimerToken, Duration)> {
|
||||
self.timers.pop()
|
||||
}
|
||||
|
||||
fn next_timer_token(&mut self) -> TimerToken {
|
||||
let token = TimerToken(self.next_token);
|
||||
self.next_token += 1;
|
||||
token
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mod base;
|
||||
pub mod model_t1;
|
||||
pub mod model_tt;
|
||||
|
||||
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,224 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Offset, Rect},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub enum ButtonMsg {
|
||||
Clicked,
|
||||
}
|
||||
|
||||
pub struct Button {
|
||||
area: Rect,
|
||||
content: ButtonContent,
|
||||
styles: ButtonStyleSheet,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(area: Rect, content: ButtonContent, styles: ButtonStyleSheet) -> Self {
|
||||
Self {
|
||||
area,
|
||||
content,
|
||||
styles,
|
||||
state: State::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_text(area: Rect, text: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(area, ButtonContent::Text(text), styles)
|
||||
}
|
||||
|
||||
pub fn with_icon(area: Rect, image: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(area, ButtonContent::Icon(image), styles)
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, ctx: &mut EventCtx) {
|
||||
self.set(ctx, State::Initial)
|
||||
}
|
||||
|
||||
pub fn disable(&mut self, ctx: &mut EventCtx) {
|
||||
self.set(ctx, State::Disabled)
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(
|
||||
self.state,
|
||||
State::Initial | State::Pressed | State::Released
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
matches!(self.state, State::Disabled)
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &ButtonContent {
|
||||
&self.content
|
||||
}
|
||||
|
||||
fn style(&self) -> &ButtonStyle {
|
||||
match self.state {
|
||||
State::Initial | State::Released => self.styles.normal,
|
||||
State::Pressed => self.styles.active,
|
||||
State::Disabled => self.styles.disabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, ctx: &mut EventCtx, state: State) {
|
||||
if self.state != state {
|
||||
self.state = state;
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Button {
|
||||
type Msg = ButtonMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::TouchStart(pos) => {
|
||||
match self.state {
|
||||
State::Disabled => {
|
||||
// Do nothing.
|
||||
}
|
||||
_ => {
|
||||
// Touch started in our area, transform to `Pressed` state.
|
||||
if self.area.contains(pos) {
|
||||
self.set(ctx, State::Pressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::TouchMove(pos) => {
|
||||
match self.state {
|
||||
State::Released if self.area.contains(pos) => {
|
||||
// Touch entered our area, transform to `Pressed` state.
|
||||
self.set(ctx, State::Pressed);
|
||||
}
|
||||
State::Pressed if !self.area.contains(pos) => {
|
||||
// Touch is leaving our area, transform to `Released` state.
|
||||
self.set(ctx, State::Released);
|
||||
}
|
||||
_ => {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::TouchEnd(pos) => {
|
||||
match self.state {
|
||||
State::Initial | State::Disabled => {
|
||||
// Do nothing.
|
||||
}
|
||||
State::Pressed if self.area.contains(pos) => {
|
||||
// Touch finished in our area, we got clicked.
|
||||
self.set(ctx, State::Initial);
|
||||
|
||||
return Some(ButtonMsg::Clicked);
|
||||
}
|
||||
_ => {
|
||||
// Touch finished outside our area.
|
||||
self.set(ctx, State::Initial);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let style = self.style();
|
||||
|
||||
if style.border_width > 0 {
|
||||
// Paint the border and a smaller background on top of it.
|
||||
display::rounded_rect(
|
||||
self.area,
|
||||
style.border_color,
|
||||
style.background_color,
|
||||
style.border_radius,
|
||||
);
|
||||
display::rounded_rect(
|
||||
self.area.inset(style.border_width),
|
||||
style.button_color,
|
||||
style.border_color,
|
||||
style.border_radius,
|
||||
);
|
||||
} else {
|
||||
// We do not need to draw an explicit border in this case, just a
|
||||
// bigger background.
|
||||
display::rounded_rect(
|
||||
self.area,
|
||||
style.button_color,
|
||||
style.background_color,
|
||||
style.border_radius,
|
||||
);
|
||||
}
|
||||
|
||||
match &self.content {
|
||||
ButtonContent::Text(text) => {
|
||||
let width = display::text_width(text, style.font);
|
||||
let height = display::text_height();
|
||||
let start_of_baseline = self.area.center() + Offset::new(-width / 2, height / 2);
|
||||
display::text(
|
||||
start_of_baseline,
|
||||
text,
|
||||
style.font,
|
||||
style.text_color,
|
||||
style.button_color,
|
||||
);
|
||||
}
|
||||
ButtonContent::Icon(icon) => {
|
||||
display::icon(
|
||||
self.area.center(),
|
||||
icon,
|
||||
style.text_color,
|
||||
style.button_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Button {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Button");
|
||||
match self.content {
|
||||
ButtonContent::Text(text) => t.field("text", &text),
|
||||
ButtonContent::Icon(_) => t.symbol("icon"),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum State {
|
||||
Initial,
|
||||
Pressed,
|
||||
Released,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub enum ButtonContent {
|
||||
Text(&'static [u8]),
|
||||
Icon(&'static [u8]),
|
||||
}
|
||||
|
||||
pub struct ButtonStyleSheet {
|
||||
pub normal: &'static ButtonStyle,
|
||||
pub active: &'static ButtonStyle,
|
||||
pub disabled: &'static ButtonStyle,
|
||||
}
|
||||
|
||||
pub struct ButtonStyle {
|
||||
pub font: Font,
|
||||
pub text_color: Color,
|
||||
pub button_color: Color,
|
||||
pub background_color: Color,
|
||||
pub border_color: Color,
|
||||
pub border_radius: u8,
|
||||
pub border_width: i32,
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
geometry::{Grid, Rect},
|
||||
};
|
||||
|
||||
use super::button::{Button, ButtonMsg::Clicked};
|
||||
|
||||
pub enum DialogMsg<T> {
|
||||
Content(T),
|
||||
LeftClicked,
|
||||
RightClicked,
|
||||
}
|
||||
|
||||
pub struct Dialog<T> {
|
||||
content: Child<T>,
|
||||
left_btn: Option<Child<Button>>,
|
||||
right_btn: Option<Child<Button>>,
|
||||
}
|
||||
|
||||
impl<T: Component> Dialog<T> {
|
||||
pub fn new(
|
||||
area: Rect,
|
||||
content: impl FnOnce(Rect) -> T,
|
||||
left: impl FnOnce(Rect) -> Button,
|
||||
right: impl FnOnce(Rect) -> Button,
|
||||
) -> Self {
|
||||
let grid = Grid::new(area, 5, 2);
|
||||
let content = Child::new(content(Rect::new(
|
||||
grid.row_col(0, 0).top_left(),
|
||||
grid.row_col(4, 1).bottom_right(),
|
||||
)));
|
||||
let left_btn = Child::new(left(grid.row_col(4, 0)));
|
||||
let right_btn = Child::new(right(grid.row_col(4, 1)));
|
||||
Self {
|
||||
content,
|
||||
left_btn: Some(left_btn),
|
||||
right_btn: Some(right_btn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component> Component for Dialog<T> {
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
Some(DialogMsg::Content(msg))
|
||||
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::LeftClicked)
|
||||
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::RightClicked)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.content.paint();
|
||||
if let Some(b) = self.left_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Dialog<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Dialog");
|
||||
t.field("content", &self.content);
|
||||
t.field("left", &self.left_btn);
|
||||
t.field("right", &self.right_btn);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
use crate::ui::component::{Component, Event, EventCtx, Never};
|
||||
|
||||
pub struct Empty;
|
||||
|
||||
impl Component for Empty {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
use core::ops::Deref;
|
||||
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Align, Point, Rect},
|
||||
};
|
||||
|
||||
pub struct LabelStyle {
|
||||
pub font: Font,
|
||||
pub text_color: Color,
|
||||
pub background_color: Color,
|
||||
}
|
||||
|
||||
pub struct Label<T> {
|
||||
area: Rect,
|
||||
style: LabelStyle,
|
||||
text: T,
|
||||
}
|
||||
|
||||
impl<T> Label<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
pub fn new(origin: Point, align: Align, text: T, style: LabelStyle) -> Self {
|
||||
let width = display::text_width(&text, style.font);
|
||||
let height = display::line_height();
|
||||
let area = match align {
|
||||
// `origin` is the top-left point.
|
||||
Align::Left => Rect {
|
||||
x0: origin.x,
|
||||
y0: origin.y,
|
||||
x1: origin.x + width,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
// `origin` is the top-right point.
|
||||
Align::Right => Rect {
|
||||
x0: origin.x - width,
|
||||
y0: origin.y,
|
||||
x1: origin.x,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
// `origin` is the top-centered point.
|
||||
Align::Center => Rect {
|
||||
x0: origin.x - width / 2,
|
||||
y0: origin.y,
|
||||
x1: origin.x + width / 2,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
};
|
||||
Self { area, style, text }
|
||||
}
|
||||
|
||||
pub fn left_aligned(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Align::Left, text, style)
|
||||
}
|
||||
|
||||
pub fn right_aligned(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Align::Right, text, style)
|
||||
}
|
||||
|
||||
pub fn centered(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Align::Center, text, style)
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &T {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Label<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
display::text(
|
||||
self.area.bottom_left(),
|
||||
&self.text,
|
||||
self.style.font,
|
||||
self.style.text_color,
|
||||
self.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
mod button;
|
||||
mod dialog;
|
||||
mod empty;
|
||||
mod label;
|
||||
mod page;
|
||||
mod passphrase;
|
||||
mod pin;
|
||||
mod swipe;
|
||||
pub mod text;
|
||||
pub mod theme;
|
||||
|
||||
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet};
|
||||
pub use dialog::{Dialog, DialogMsg};
|
||||
pub use empty::Empty;
|
||||
pub use label::{Label, LabelStyle};
|
||||
pub use swipe::{Swipe, SwipeDirection};
|
||||
pub use text::{LineBreaking, PageBreaking, Text, TextLayout};
|
@ -0,0 +1,136 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display,
|
||||
geometry::{Offset, Point, Rect},
|
||||
};
|
||||
|
||||
use super::{theme, Swipe, SwipeDirection};
|
||||
|
||||
pub enum PageMsg<T> {
|
||||
Content(T),
|
||||
ChangePage(usize),
|
||||
}
|
||||
|
||||
pub struct Page<T> {
|
||||
swipe: Swipe,
|
||||
scrollbar: ScrollBar,
|
||||
page: T,
|
||||
}
|
||||
|
||||
impl<T> Page<T> {
|
||||
pub fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self {
|
||||
let scrollbar = ScrollBar::vertical_right(area, page_count, active_page);
|
||||
let mut swipe = Swipe::new(area);
|
||||
swipe.allow_up = scrollbar.has_next_page();
|
||||
swipe.allow_down = scrollbar.has_previous_page();
|
||||
Self {
|
||||
swipe,
|
||||
scrollbar,
|
||||
page,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component> Component for Page<T> {
|
||||
type Msg = PageMsg<T::Msg>;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(swipe) = self.swipe.event(ctx, event) {
|
||||
match swipe {
|
||||
SwipeDirection::Up => {
|
||||
// Scroll down, if possible.
|
||||
return Some(PageMsg::ChangePage(self.scrollbar.next_page()));
|
||||
}
|
||||
SwipeDirection::Down => {
|
||||
// Scroll up, if possible.
|
||||
return Some(PageMsg::ChangePage(self.scrollbar.previous_page()));
|
||||
}
|
||||
_ => {
|
||||
// Ignore other directions.
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(msg) = self.page.event(ctx, event) {
|
||||
return Some(PageMsg::Content(msg));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.page.paint();
|
||||
self.scrollbar.paint();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
page_count: usize,
|
||||
active_page: usize,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
pub const DOT_SIZE: Offset = Offset::new(8, 8);
|
||||
pub const DOT_INTERVAL: i32 = 14;
|
||||
|
||||
pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self {
|
||||
Self {
|
||||
area: area.cut_from_right(Self::DOT_SIZE.x),
|
||||
page_count,
|
||||
active_page,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.active_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_page(&self) -> bool {
|
||||
self.active_page > 0
|
||||
}
|
||||
|
||||
pub fn next_page(&self) -> usize {
|
||||
self.active_page.saturating_add(1).min(self.page_count - 1)
|
||||
}
|
||||
|
||||
pub fn previous_page(&self) -> usize {
|
||||
self.active_page.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let count = self.page_count as i32;
|
||||
let interval = {
|
||||
let available_height = self.area.height();
|
||||
let naive_height = count * Self::DOT_INTERVAL;
|
||||
if naive_height > available_height {
|
||||
available_height / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut dot = Point::new(
|
||||
self.area.center().x,
|
||||
self.area.center().y - (count / 2) * interval,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
display::rounded_rect(
|
||||
Rect::from_center_and_size(dot, Self::DOT_SIZE),
|
||||
if i == self.active_page {
|
||||
theme::FG
|
||||
} else {
|
||||
theme::GREY_LIGHT
|
||||
},
|
||||
theme::BG,
|
||||
theme::RADIUS,
|
||||
);
|
||||
dot.y += interval;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,319 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
||||
display,
|
||||
geometry::{Grid, Rect},
|
||||
};
|
||||
|
||||
use super::{
|
||||
button::{Button, ButtonContent, ButtonMsg::Clicked},
|
||||
swipe::{Swipe, SwipeDirection},
|
||||
theme,
|
||||
};
|
||||
|
||||
pub enum PassphraseKeyboardMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
pub struct PassphraseKeyboard {
|
||||
page_swipe: Swipe,
|
||||
textbox: Child<TextBox>,
|
||||
back_btn: Child<Button>,
|
||||
confirm_btn: Child<Button>,
|
||||
key_btns: [[Child<Button>; KEYS]; PAGES],
|
||||
key_page: usize,
|
||||
pending: Option<Pending>,
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
key: usize,
|
||||
char: usize,
|
||||
timer: TimerToken,
|
||||
}
|
||||
|
||||
const MAX_LENGTH: usize = 50;
|
||||
const STARTING_PAGE: usize = 1;
|
||||
const PAGES: usize = 4;
|
||||
const KEYS: usize = 10;
|
||||
const PENDING_DEADLINE: Duration = Duration::from_secs(1);
|
||||
|
||||
impl PassphraseKeyboard {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
let textbox_area = Grid::new(area, 5, 1).row_col(0, 0);
|
||||
let confirm_btn_area = Grid::new(area, 5, 3).cell(14);
|
||||
let back_btn_area = Grid::new(area, 5, 3).cell(12);
|
||||
let key_grid = Grid::new(area, 5, 3);
|
||||
|
||||
let text = Vec::new();
|
||||
let page_swipe = Swipe::horizontal(area);
|
||||
let textbox = Child::new(TextBox::new(textbox_area, text));
|
||||
let confirm_btn = Child::new(Button::with_text(
|
||||
confirm_btn_area,
|
||||
b"Confirm",
|
||||
theme::button_confirm(),
|
||||
));
|
||||
let back_btn = Child::new(Button::with_text(
|
||||
back_btn_area,
|
||||
b"Back",
|
||||
theme::button_clear(),
|
||||
));
|
||||
let key_btns = Self::generate_keyboard(&key_grid);
|
||||
|
||||
Self {
|
||||
textbox,
|
||||
page_swipe,
|
||||
confirm_btn,
|
||||
back_btn,
|
||||
key_btns,
|
||||
key_page: STARTING_PAGE,
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_keyboard(grid: &Grid) -> [[Child<Button>; KEYS]; PAGES] {
|
||||
[
|
||||
Self::generate_key_page(grid, 0),
|
||||
Self::generate_key_page(grid, 1),
|
||||
Self::generate_key_page(grid, 2),
|
||||
Self::generate_key_page(grid, 3),
|
||||
]
|
||||
}
|
||||
|
||||
fn generate_key_page(grid: &Grid, page: usize) -> [Child<Button>; KEYS] {
|
||||
[
|
||||
Self::generate_key(grid, page, 0),
|
||||
Self::generate_key(grid, page, 1),
|
||||
Self::generate_key(grid, page, 2),
|
||||
Self::generate_key(grid, page, 3),
|
||||
Self::generate_key(grid, page, 4),
|
||||
Self::generate_key(grid, page, 5),
|
||||
Self::generate_key(grid, page, 6),
|
||||
Self::generate_key(grid, page, 7),
|
||||
Self::generate_key(grid, page, 8),
|
||||
Self::generate_key(grid, page, 9),
|
||||
]
|
||||
}
|
||||
|
||||
fn generate_key(grid: &Grid, page: usize, key: usize) -> Child<Button> {
|
||||
#[rustfmt::skip]
|
||||
const KEYBOARD: [[&str; KEYS]; PAGES] = [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
[" ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"],
|
||||
[" ", "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"],
|
||||
["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="],
|
||||
];
|
||||
|
||||
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
||||
// second row.
|
||||
let area = grid.cell(if key < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
key + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
key + 1 + 3
|
||||
});
|
||||
let text = KEYBOARD[page][key].as_bytes();
|
||||
if text == b" " {
|
||||
let icon = theme::ICON_SPACE;
|
||||
Child::new(Button::with_icon(area, icon, theme::button_default()))
|
||||
} else {
|
||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_page_swipe(&mut self, swipe: SwipeDirection) {
|
||||
self.key_page = match swipe {
|
||||
SwipeDirection::Left => (self.key_page as isize + 1) as usize % PAGES,
|
||||
SwipeDirection::Right => (self.key_page as isize - 1) as usize % PAGES,
|
||||
_ => self.key_page,
|
||||
};
|
||||
self.pending.take();
|
||||
}
|
||||
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx) {
|
||||
self.pending.take();
|
||||
self.textbox.mutate(ctx, |ctx, t| t.delete_last(ctx));
|
||||
self.after_edit(ctx);
|
||||
}
|
||||
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize) {
|
||||
let content = self.key_content(self.key_page, key);
|
||||
|
||||
let char = match &self.pending {
|
||||
Some(pending) if pending.key == key => {
|
||||
// This key is pending. Cycle the last inserted character through the
|
||||
// key content.
|
||||
let char = (pending.char + 1) % content.len();
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.replace_last(ctx, content[char]));
|
||||
char
|
||||
}
|
||||
_ => {
|
||||
// This key is not pending. Append the first character in the key.
|
||||
let char = 0;
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.append(ctx, content[char]));
|
||||
char
|
||||
}
|
||||
};
|
||||
|
||||
// If the key has more then one character, we need to set it as pending, so we
|
||||
// can cycle through on the repeated clicks. We also request a timer so we can
|
||||
// reset the pending state after a deadline.
|
||||
self.pending = if content.len() > 1 {
|
||||
Some(Pending {
|
||||
key,
|
||||
char,
|
||||
timer: ctx.request_timer(PENDING_DEADLINE),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let is_pending = self.pending.is_some();
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.toggle_pending_marker(ctx, is_pending));
|
||||
|
||||
self.after_edit(ctx);
|
||||
}
|
||||
|
||||
fn on_timeout(&mut self) {
|
||||
self.pending.take();
|
||||
}
|
||||
|
||||
fn key_content(&self, page: usize, key: usize) -> &'static [u8] {
|
||||
match self.key_btns[page][key].inner().content() {
|
||||
ButtonContent::Text(text) => text,
|
||||
ButtonContent::Icon(_) => b" ",
|
||||
}
|
||||
}
|
||||
|
||||
fn after_edit(&mut self, ctx: &mut EventCtx) {
|
||||
if self.textbox.inner().is_empty() {
|
||||
self.back_btn.mutate(ctx, |ctx, b| b.disable(ctx));
|
||||
} else {
|
||||
self.back_btn.mutate(ctx, |ctx, b| b.enable(ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PassphraseKeyboard {
|
||||
type Msg = PassphraseKeyboardMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if matches!((event, &self.pending), (Event::Timer(t), Some(p)) if p.timer == t) {
|
||||
// Our pending timer triggered, reset the pending state.
|
||||
self.on_timeout();
|
||||
return None;
|
||||
}
|
||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||
// We have detected a horizontal swipe. Change the keyboard page.
|
||||
self.on_page_swipe(swipe);
|
||||
return None;
|
||||
}
|
||||
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
|
||||
// Confirm button was clicked, we're done.
|
||||
return Some(PassphraseKeyboardMsg::Confirmed);
|
||||
}
|
||||
if let Some(Clicked) = self.back_btn.event(ctx, event) {
|
||||
// Backspace button was clicked. If we have any content in the textbox, let's
|
||||
// delete the last character. Otherwise cancel.
|
||||
if self.textbox.inner().is_empty() {
|
||||
return Some(PassphraseKeyboardMsg::Cancelled);
|
||||
} else {
|
||||
self.on_backspace_click(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
for (key, btn) in self.key_btns[self.key_page].iter_mut().enumerate() {
|
||||
if let Some(Clicked) = btn.event(ctx, event) {
|
||||
// Key button was clicked. If this button is pending, let's cycle the pending
|
||||
// character in textbox. If not, let's just append the first character.
|
||||
self.on_key_click(ctx, key);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.textbox.paint();
|
||||
self.confirm_btn.paint();
|
||||
self.back_btn.paint();
|
||||
for btn in &mut self.key_btns[self.key_page] {
|
||||
btn.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TextBox {
|
||||
area: Rect,
|
||||
text: Vec<u8, MAX_LENGTH>,
|
||||
pending: bool,
|
||||
}
|
||||
|
||||
impl TextBox {
|
||||
fn new(area: Rect, text: Vec<u8, MAX_LENGTH>) -> Self {
|
||||
Self {
|
||||
area,
|
||||
text,
|
||||
pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
fn toggle_pending_marker(&mut self, ctx: &mut EventCtx, pending: bool) {
|
||||
self.pending = pending;
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn delete_last(&mut self, ctx: &mut EventCtx) {
|
||||
self.text.pop();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn replace_last(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
self.text.pop();
|
||||
if self.text.push(char).is_err() {
|
||||
// Should not happen unless `self.text` has zero capacity.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox has zero capacity");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn append(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
if self.text.push(char).is_err() {
|
||||
// `self.text` is full, ignore this change.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox is full");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TextBox {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let style = theme::label_default();
|
||||
|
||||
display::text(
|
||||
self.area.bottom_left(),
|
||||
&self.text,
|
||||
style.font,
|
||||
style.text_color,
|
||||
style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Never},
|
||||
display,
|
||||
geometry::{Grid, Offset, Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
button::{Button, ButtonContent, ButtonMsg::Clicked},
|
||||
label::{Label, LabelStyle},
|
||||
theme,
|
||||
};
|
||||
|
||||
pub enum PinDialogMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
const MAX_LENGTH: usize = 9;
|
||||
const DIGIT_COUNT: usize = 10; // 0..10
|
||||
|
||||
pub struct PinDialog {
|
||||
digits: Vec<u8, MAX_LENGTH>,
|
||||
major_prompt: Label<&'static [u8]>,
|
||||
minor_prompt: Label<&'static [u8]>,
|
||||
dots: Child<PinDots>,
|
||||
reset_btn: Child<Button>,
|
||||
cancel_btn: Child<Button>,
|
||||
confirm_btn: Child<Button>,
|
||||
digit_btns: [Child<Button>; DIGIT_COUNT],
|
||||
}
|
||||
|
||||
impl PinDialog {
|
||||
pub fn new(area: Rect, major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
|
||||
let digits = Vec::new();
|
||||
|
||||
// Prompts and PIN dots display.
|
||||
let grid = if minor_prompt.is_empty() {
|
||||
// Make the major prompt bigger if the minor one is empty.
|
||||
Grid::new(area, 5, 1)
|
||||
} else {
|
||||
Grid::new(area, 6, 1)
|
||||
};
|
||||
let major_prompt = Label::centered(
|
||||
grid.row_col(0, 0).center(),
|
||||
major_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let minor_prompt = Label::centered(
|
||||
grid.row_col(0, 1).center(),
|
||||
minor_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let dots = Child::new(PinDots::new(
|
||||
grid.row_col(0, 0),
|
||||
digits.len(),
|
||||
theme::label_default(),
|
||||
));
|
||||
|
||||
// Control buttons.
|
||||
let grid = Grid::new(area, 5, 3);
|
||||
let reset_btn = Child::new(Button::with_text(
|
||||
grid.row_col(4, 0),
|
||||
b"Reset",
|
||||
theme::button_clear(),
|
||||
));
|
||||
let cancel_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 0),
|
||||
theme::ICON_CANCEL,
|
||||
theme::button_cancel(),
|
||||
));
|
||||
let confirm_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 2),
|
||||
theme::ICON_CONFIRM,
|
||||
theme::button_clear(),
|
||||
));
|
||||
|
||||
// PIN digit buttons.
|
||||
let digit_btns = Self::generate_digit_buttons(&grid);
|
||||
|
||||
Self {
|
||||
digits,
|
||||
major_prompt,
|
||||
minor_prompt,
|
||||
dots,
|
||||
reset_btn,
|
||||
cancel_btn,
|
||||
confirm_btn,
|
||||
digit_btns,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_digit_buttons(grid: &Grid) -> [Child<Button>; DIGIT_COUNT] {
|
||||
// Generate a random sequence of digits from 0 to 9.
|
||||
let mut digits = [b"0", b"1", b"2", b"3", b"4", b"5", b"6", b"7", b"8", b"9"];
|
||||
random::shuffle(&mut digits);
|
||||
|
||||
// Assign the digits to buttons on a 5x3 grid, starting from the second row.
|
||||
let btn = |i| {
|
||||
let area = grid.cell(if i < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
i + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
i + 1 + 3
|
||||
});
|
||||
let text: &[u8; 1] = digits[i];
|
||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
||||
};
|
||||
[
|
||||
btn(0),
|
||||
btn(1),
|
||||
btn(2),
|
||||
btn(3),
|
||||
btn(4),
|
||||
btn(5),
|
||||
btn(6),
|
||||
btn(7),
|
||||
btn(8),
|
||||
btn(9),
|
||||
]
|
||||
}
|
||||
|
||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||
for btn in &mut self.digit_btns {
|
||||
let is_full = self.digits.is_full();
|
||||
btn.mutate(ctx, |ctx, btn| {
|
||||
if is_full {
|
||||
btn.disable(ctx);
|
||||
} else {
|
||||
btn.enable(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
if self.digits.is_empty() {
|
||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
} else {
|
||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
}
|
||||
let digit_count = self.digits.len();
|
||||
self.dots
|
||||
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
||||
}
|
||||
|
||||
pub fn pin(&self) -> &[u8] {
|
||||
&self.digits
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinDialog {
|
||||
type Msg = PinDialogMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
|
||||
return Some(PinDialogMsg::Confirmed);
|
||||
}
|
||||
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
|
||||
return Some(PinDialogMsg::Cancelled);
|
||||
}
|
||||
if let Some(Clicked) = self.reset_btn.event(ctx, event) {
|
||||
self.digits.clear();
|
||||
self.pin_modified(ctx);
|
||||
return None;
|
||||
}
|
||||
for btn in &mut self.digit_btns {
|
||||
if let Some(Clicked) = btn.event(ctx, event) {
|
||||
if let ButtonContent::Text(text) = btn.inner().content() {
|
||||
if self.digits.extend_from_slice(text).is_err() {
|
||||
// `self.pin` is full and wasn't able to accept all of
|
||||
// `text`. Should not happen.
|
||||
}
|
||||
self.pin_modified(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
if self.digits.is_empty() {
|
||||
self.cancel_btn.paint();
|
||||
self.major_prompt.paint();
|
||||
self.minor_prompt.paint();
|
||||
} else {
|
||||
self.reset_btn.paint();
|
||||
self.dots.paint();
|
||||
}
|
||||
self.confirm_btn.paint();
|
||||
for btn in &mut self.digit_btns {
|
||||
btn.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PinDots {
|
||||
area: Rect,
|
||||
style: LabelStyle,
|
||||
digit_count: usize,
|
||||
}
|
||||
|
||||
impl PinDots {
|
||||
const DOT: i32 = 10;
|
||||
const PADDING: i32 = 4;
|
||||
|
||||
fn new(area: Rect, digit_count: usize, style: LabelStyle) -> Self {
|
||||
Self {
|
||||
area,
|
||||
style,
|
||||
digit_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &mut EventCtx, digit_count: usize) {
|
||||
if digit_count != self.digit_count {
|
||||
self.digit_count = digit_count;
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinDots {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Clear the area with the background color.
|
||||
display::rect(self.area, self.style.background_color);
|
||||
|
||||
// Draw a dot for each PIN digit.
|
||||
for i in 0..self.digit_count {
|
||||
let pos = Point {
|
||||
x: self.area.x0 + i as i32 * (Self::DOT + Self::PADDING),
|
||||
y: self.area.center().y,
|
||||
};
|
||||
let size = Offset::new(Self::DOT, Self::DOT);
|
||||
display::rounded_rect(
|
||||
Rect::from_top_left_and_size(pos, size),
|
||||
self.style.text_color,
|
||||
self.style.background_color,
|
||||
4,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Point, Rect},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub enum SwipeDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct Swipe {
|
||||
area: Rect,
|
||||
pub allow_up: bool,
|
||||
pub allow_down: bool,
|
||||
pub allow_left: bool,
|
||||
pub allow_right: bool,
|
||||
backlight_start: i32,
|
||||
backlight_end: i32,
|
||||
origin: Option<Point>,
|
||||
}
|
||||
|
||||
impl Swipe {
|
||||
const DISTANCE: i32 = 120;
|
||||
const THRESHOLD: f32 = 0.3;
|
||||
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
area,
|
||||
allow_up: false,
|
||||
allow_down: false,
|
||||
allow_left: false,
|
||||
allow_right: false,
|
||||
backlight_start: theme::BACKLIGHT_NORMAL,
|
||||
backlight_end: theme::BACKLIGHT_NONE,
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(area: Rect) -> Self {
|
||||
Self::new(area).up().down()
|
||||
}
|
||||
|
||||
pub fn horizontal(area: Rect) -> Self {
|
||||
Self::new(area).left().right()
|
||||
}
|
||||
|
||||
pub fn up(mut self) -> Self {
|
||||
self.allow_up = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn down(mut self) -> Self {
|
||||
self.allow_down = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left(mut self) -> Self {
|
||||
self.allow_left = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right(mut self) -> Self {
|
||||
self.allow_right = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn ratio(&self, dist: i32) -> f32 {
|
||||
(dist as f32 / Self::DISTANCE as f32).min(1.0)
|
||||
}
|
||||
|
||||
fn backlight(&self, ratio: f32) {
|
||||
let start = self.backlight_start as f32;
|
||||
let end = self.backlight_end as f32;
|
||||
let value = start + ratio * (end - start);
|
||||
display::backlight(value as i32);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Swipe {
|
||||
type Msg = SwipeDirection;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match (event, self.origin) {
|
||||
(Event::TouchStart(pos), _) if self.area.contains(pos) => {
|
||||
// Mark the starting position of this touch.
|
||||
self.origin.replace(pos);
|
||||
}
|
||||
(Event::TouchMove(pos), Some(origin)) => {
|
||||
// Consider our allowed directions and the touch distance and modify the display
|
||||
// backlight accordingly.
|
||||
let ofs = pos - origin;
|
||||
let abs = ofs.abs();
|
||||
if abs.x > abs.y && (self.allow_left || self.allow_right) {
|
||||
// Horizontal direction.
|
||||
if (ofs.x < 0 && self.allow_left) || (ofs.x > 0 && self.allow_right) {
|
||||
self.backlight(self.ratio(abs.x));
|
||||
}
|
||||
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
|
||||
// Vertical direction.
|
||||
if (ofs.y < 0 && self.allow_up) || (ofs.y > 0 && self.allow_down) {
|
||||
self.backlight(self.ratio(abs.y));
|
||||
}
|
||||
};
|
||||
}
|
||||
(Event::TouchEnd(pos), Some(origin)) => {
|
||||
// Touch interaction is over, reset the position.
|
||||
self.origin.take();
|
||||
|
||||
// Compare the touch distance with our allowed directions and determine if it
|
||||
// constitutes a valid swipe.
|
||||
let ofs = pos - origin;
|
||||
let abs = ofs.abs();
|
||||
if abs.x > abs.y && (self.allow_left || self.allow_right) {
|
||||
// Horizontal direction.
|
||||
if self.ratio(abs.x) >= Self::THRESHOLD {
|
||||
if ofs.x < 0 && self.allow_left {
|
||||
return Some(SwipeDirection::Left);
|
||||
} else if ofs.x > 0 && self.allow_right {
|
||||
return Some(SwipeDirection::Right);
|
||||
}
|
||||
}
|
||||
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
|
||||
// Vertical direction.
|
||||
if self.ratio(abs.y) >= Self::THRESHOLD {
|
||||
if ofs.y < 0 && self.allow_up {
|
||||
return Some(SwipeDirection::Up);
|
||||
} else if ofs.y > 0 && self.allow_down {
|
||||
return Some(SwipeDirection::Down);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Swipe did not happen, reset the backlight.
|
||||
self.backlight(0.0);
|
||||
}
|
||||
_ => {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {}
|
||||
}
|
@ -0,0 +1,647 @@
|
||||
use core::{
|
||||
iter::{Enumerate, Peekable},
|
||||
slice,
|
||||
};
|
||||
|
||||
use heapless::LinearMap;
|
||||
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display,
|
||||
display::{Color, Font},
|
||||
geometry::{Offset, Point, Rect},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub const MAX_ARGUMENTS: usize = 6;
|
||||
|
||||
pub struct Text<F, T> {
|
||||
layout: TextLayout,
|
||||
format: F,
|
||||
args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>,
|
||||
}
|
||||
|
||||
impl<F, T> Text<F, T> {
|
||||
pub fn new(area: Rect, format: F) -> Self {
|
||||
Self {
|
||||
layout: TextLayout::new(area),
|
||||
format,
|
||||
args: LinearMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(mut self, format: F) -> Self {
|
||||
self.format = format;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with(mut self, key: &'static [u8], value: T) -> Self {
|
||||
if self.args.insert(key, value).is_err() {
|
||||
// Map is full, ignore.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Text args map is full");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_text_font(mut self, text_font: Font) -> Self {
|
||||
self.layout.text_font = text_font;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_text_color(mut self, text_color: Color) -> Self {
|
||||
self.layout.text_color = text_color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self {
|
||||
self.layout.line_breaking = line_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self {
|
||||
self.layout.page_breaking = page_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn layout_mut(&mut self) -> &mut TextLayout {
|
||||
&mut self.layout
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn layout_content(&self, sink: &mut dyn LayoutSink) {
|
||||
self.layout.layout_formatted(
|
||||
self.format.as_ref(),
|
||||
|arg| match arg {
|
||||
Token::Literal(literal) => Some(Op::Text(literal)),
|
||||
Token::Argument(b"mono") => Some(Op::Font(theme::FONT_MONO)),
|
||||
Token::Argument(b"bold") => Some(Op::Font(theme::FONT_BOLD)),
|
||||
Token::Argument(b"normal") => Some(Op::Font(theme::FONT_NORMAL)),
|
||||
Token::Argument(argument) => self
|
||||
.args
|
||||
.get(argument)
|
||||
.map(|value| Op::Text(value.as_ref())),
|
||||
},
|
||||
sink,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Component for Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.layout_content(&mut TextRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod trace {
|
||||
use super::*;
|
||||
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
|
||||
|
||||
impl<'a> LayoutSink for TraceSink<'a> {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &[u8]) {
|
||||
self.0.bytes(text);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("-");
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
}
|
||||
|
||||
fn line_break(&mut self, _cursor: Point) {
|
||||
self.0.string("\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TraceText<'a, F, T>(pub &'a Text<F, T>);
|
||||
|
||||
impl<'a, F, T> crate::trace::Trace for TraceText<'a, F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
self.0.layout_content(&mut TraceSink(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, T> crate::trace::Trace for Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Text");
|
||||
t.field("content", &trace::TraceText(self));
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum LineBreaking {
|
||||
/// Break line only at whitespace, if possible. If we don't find any
|
||||
/// whitespace, break words.
|
||||
BreakAtWhitespace,
|
||||
/// Break words, adding a hyphen before the line-break. Does not use any
|
||||
/// smart algorithm, just char-by-char.
|
||||
BreakWordsAndInsertHyphen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum PageBreaking {
|
||||
/// Stop after hitting the bottom-right edge of the bounds.
|
||||
Cut,
|
||||
/// Before stopping at the bottom-right edge, insert ellipsis to signify
|
||||
/// more content is available, but only if no hyphen has been inserted yet.
|
||||
CutAndInsertEllipsis,
|
||||
}
|
||||
|
||||
/// Visual instructions for laying out a formatted block of text.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TextLayout {
|
||||
/// Bounding box restricting the layout dimensions.
|
||||
pub bounds: Rect,
|
||||
|
||||
/// Background color.
|
||||
pub background_color: Color,
|
||||
/// Text color. Can be overridden by `Op::Color`.
|
||||
pub text_color: Color,
|
||||
/// Text font ID. Can be overridden by `Op::Font`.
|
||||
pub text_font: Font,
|
||||
|
||||
/// Specifies which line-breaking strategy to use.
|
||||
pub line_breaking: LineBreaking,
|
||||
/// Font used for drawing the word-breaking hyphen.
|
||||
pub hyphen_font: Font,
|
||||
/// Foreground color used for drawing the hyphen.
|
||||
pub hyphen_color: Color,
|
||||
|
||||
/// Specifies what to do at the end of the page.
|
||||
pub page_breaking: PageBreaking,
|
||||
/// Font used for drawing the ellipsis.
|
||||
pub ellipsis_font: Font,
|
||||
/// Foreground color used for drawing the ellipsis.
|
||||
pub ellipsis_color: Color,
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
pub fn new(bounds: Rect) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
background_color: theme::BG,
|
||||
text_color: theme::FG,
|
||||
text_font: theme::FONT_NORMAL,
|
||||
line_breaking: LineBreaking::BreakAtWhitespace,
|
||||
hyphen_font: theme::FONT_BOLD,
|
||||
hyphen_color: theme::GREY_LIGHT,
|
||||
page_breaking: PageBreaking::CutAndInsertEllipsis,
|
||||
ellipsis_font: theme::FONT_BOLD,
|
||||
ellipsis_color: theme::GREY_LIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_cursor(&self) -> Point {
|
||||
Point::new(
|
||||
self.bounds.top_left().x,
|
||||
self.bounds.top_left().y + self.text_font.line_height(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn layout_formatted<'f, 'o, F, I>(
|
||||
self,
|
||||
format: &'f [u8],
|
||||
resolve: F,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit
|
||||
where
|
||||
F: Fn(Token<'f>) -> I,
|
||||
I: IntoIterator<Item = Op<'o>>,
|
||||
{
|
||||
let mut cursor = self.initial_cursor();
|
||||
|
||||
self.layout_op_stream(
|
||||
&mut Tokenizer::new(format).flat_map(resolve),
|
||||
&mut cursor,
|
||||
sink,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn layout_op_stream<'o>(
|
||||
mut self,
|
||||
ops: &mut dyn Iterator<Item = Op<'o>>,
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let mut total_processed_chars = 0;
|
||||
|
||||
for op in ops {
|
||||
match op {
|
||||
Op::Color(color) => {
|
||||
self.text_color = color;
|
||||
}
|
||||
Op::Font(font) => {
|
||||
self.text_font = font;
|
||||
}
|
||||
Op::Text(text) => match self.layout_text(text, cursor, sink) {
|
||||
LayoutFit::Fitting { processed_chars } => {
|
||||
total_processed_chars += processed_chars;
|
||||
}
|
||||
LayoutFit::OutOfBounds { processed_chars } => {
|
||||
total_processed_chars += processed_chars;
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: total_processed_chars,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: total_processed_chars,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout_text(
|
||||
&self,
|
||||
text: &[u8],
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let mut remaining_text = text;
|
||||
|
||||
while !remaining_text.is_empty() {
|
||||
let span = Span::fit_horizontally(
|
||||
remaining_text,
|
||||
self.bounds.x1 - cursor.x,
|
||||
self.text_font,
|
||||
self.hyphen_font,
|
||||
self.line_breaking,
|
||||
);
|
||||
|
||||
// Report the span at the cursor position.
|
||||
sink.text(*cursor, self, &remaining_text[..span.length]);
|
||||
|
||||
// Continue with the rest of the remaining_text.
|
||||
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
|
||||
|
||||
// Advance the cursor horizontally.
|
||||
cursor.x += span.advance.x;
|
||||
|
||||
if span.advance.y > 0 {
|
||||
// We're advancing to the next line.
|
||||
|
||||
// Check if we should be appending a hyphen at this point.
|
||||
if span.insert_hyphen_before_line_break {
|
||||
sink.hyphen(*cursor, self);
|
||||
}
|
||||
// Check the amount of vertical space we have left.
|
||||
if cursor.y + span.advance.y > self.bounds.y1 {
|
||||
if !remaining_text.is_empty() {
|
||||
// Append ellipsis to indicate more content is available, but only if we
|
||||
// haven't already appended a hyphen.
|
||||
let should_append_ellipsis =
|
||||
matches!(self.page_breaking, PageBreaking::CutAndInsertEllipsis)
|
||||
&& !span.insert_hyphen_before_line_break;
|
||||
if should_append_ellipsis {
|
||||
sink.ellipsis(*cursor, self);
|
||||
}
|
||||
// TODO: This does not work in case we are the last
|
||||
// fitting text token on the line, with more text tokens
|
||||
// following and `text.is_empty() == true`.
|
||||
}
|
||||
|
||||
// Report we are out of bounds and quit.
|
||||
sink.out_of_bounds();
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: text.len() - remaining_text.len(),
|
||||
};
|
||||
} else {
|
||||
// Advance the cursor to the beginning of the next line.
|
||||
cursor.x = self.bounds.x0;
|
||||
cursor.y += span.advance.y;
|
||||
|
||||
// Report a line break. While rendering works using the cursor coordinates, we
|
||||
// use explicit line-break reporting in the `Trace` impl.
|
||||
sink.line_break(*cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LayoutFit {
|
||||
Fitting { processed_chars: usize },
|
||||
OutOfBounds { processed_chars: usize },
|
||||
}
|
||||
|
||||
/// Visitor for text segment operations.
|
||||
pub trait LayoutSink {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &[u8]) {}
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn line_break(&mut self, _cursor: Point) {}
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct TextNoop;
|
||||
|
||||
impl LayoutSink for TextNoop {}
|
||||
|
||||
pub struct TextRenderer;
|
||||
|
||||
impl LayoutSink for TextRenderer {
|
||||
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &[u8]) {
|
||||
display::text(
|
||||
cursor,
|
||||
text,
|
||||
layout.text_font,
|
||||
layout.text_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
b"-",
|
||||
layout.hyphen_font,
|
||||
layout.hyphen_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
b"...",
|
||||
layout.ellipsis_font,
|
||||
layout.ellipsis_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Token<'a> {
|
||||
/// Process literal text content.
|
||||
Literal(&'a [u8]),
|
||||
/// Process argument with specified descriptor.
|
||||
Argument(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Processes a format string into an iterator of `Token`s.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let parser = Tokenizer::new("Nice to meet {you}, where you been?");
|
||||
/// assert!(matches!(parser.next(), Some(Token::Literal("Nice to meet "))));
|
||||
/// assert!(matches!(parser.next(), Some(Token::Argument("you"))));
|
||||
/// assert!(matches!(parser.next(), Some(Token::Literal(", where you been?"))));
|
||||
/// ```
|
||||
pub struct Tokenizer<'a> {
|
||||
input: &'a [u8],
|
||||
inner: Peekable<Enumerate<slice::Iter<'a, u8>>>,
|
||||
}
|
||||
|
||||
impl<'a> Tokenizer<'a> {
|
||||
/// Create a new tokenizer for bytes of a formatting string `input`,
|
||||
/// returning an iterator.
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
Self {
|
||||
input,
|
||||
inner: input.iter().enumerate().peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Tokenizer<'a> {
|
||||
type Item = Token<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
const ASCII_OPEN_BRACE: u8 = b'{';
|
||||
const ASCII_CLOSED_BRACE: u8 = b'}';
|
||||
|
||||
match self.inner.next() {
|
||||
// Argument token is starting. Read until we find '}', then parse the content between
|
||||
// the braces and return the token. If we encounter the end of string before the closing
|
||||
// brace, quit.
|
||||
Some((open, &ASCII_OPEN_BRACE)) => loop {
|
||||
match self.inner.next() {
|
||||
Some((close, &ASCII_CLOSED_BRACE)) => {
|
||||
break Some(Token::Argument(&self.input[open + 1..close]));
|
||||
}
|
||||
None => {
|
||||
break None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
// Literal token is starting. Read until we find '{' or the end of string, and return
|
||||
// the token. Use `peek()` for matching the opening brace, se we can keep it
|
||||
// in the iterator for the above code.
|
||||
Some((start, _)) => loop {
|
||||
match self.inner.peek() {
|
||||
Some(&(open, &ASCII_OPEN_BRACE)) => {
|
||||
break Some(Token::Literal(&self.input[start..open]));
|
||||
}
|
||||
None => {
|
||||
break Some(Token::Literal(&self.input[start..]));
|
||||
}
|
||||
_ => {
|
||||
self.inner.next();
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Op<'a> {
|
||||
/// Render text with current color and font.
|
||||
Text(&'a [u8]),
|
||||
/// Set current text color.
|
||||
Color(Color),
|
||||
/// Set currently used font.
|
||||
Font(Font),
|
||||
}
|
||||
|
||||
impl<'a> Op<'a> {
|
||||
fn skip_n_text_bytes(
|
||||
ops: impl Iterator<Item = Op<'a>>,
|
||||
skip_bytes: usize,
|
||||
) -> impl Iterator<Item = Op<'a>> {
|
||||
let mut skipped = 0;
|
||||
|
||||
ops.filter_map(move |op| match op {
|
||||
Op::Text(text) if skipped < skip_bytes => {
|
||||
skipped = skipped.saturating_add(text.len());
|
||||
if skipped > skip_bytes {
|
||||
let leave_bytes = skipped - skip_bytes;
|
||||
Some(Op::Text(&text[..text.len() - leave_bytes]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
op_to_pass_through => Some(op_to_pass_through),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Span {
|
||||
/// How many characters from the input text this span is laying out.
|
||||
length: usize,
|
||||
/// How many chars from the input text should we skip before fitting the
|
||||
/// next span?
|
||||
skip_next_chars: usize,
|
||||
/// By how much to offset the cursor after this span. If the vertical offset
|
||||
/// is bigger than zero, it means we are breaking the line.
|
||||
advance: Offset,
|
||||
/// If we are breaking the line, should we insert a hyphen right after this
|
||||
/// span to indicate a word-break?
|
||||
insert_hyphen_before_line_break: bool,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
fn fit_horizontally(
|
||||
text: &[u8],
|
||||
max_width: i32,
|
||||
text_font: Font,
|
||||
hyphen_font: Font,
|
||||
breaking: LineBreaking,
|
||||
) -> Self {
|
||||
const ASCII_LF: u8 = b'\n';
|
||||
const ASCII_CR: u8 = b'\r';
|
||||
const ASCII_SPACE: u8 = b' ';
|
||||
const ASCII_HYPHEN: u8 = b'-';
|
||||
|
||||
fn is_whitespace(ch: u8) -> bool {
|
||||
ch == ASCII_SPACE || ch == ASCII_LF || ch == ASCII_CR
|
||||
}
|
||||
|
||||
let hyphen_width = hyphen_font.text_width(&[ASCII_HYPHEN]);
|
||||
|
||||
// The span we return in case the line has to break. We mutate it in the
|
||||
// possible break points, and its initial value is returned in case no text
|
||||
// at all is fitting the constraints: zero length, zero width, full line
|
||||
// break.
|
||||
let mut line = Self {
|
||||
length: 0,
|
||||
advance: Offset::new(0, text_font.line_height()),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
};
|
||||
|
||||
let mut span_width = 0;
|
||||
let mut found_any_whitespace = false;
|
||||
|
||||
for (i, &ch) in text.iter().enumerate() {
|
||||
let char_width = text_font.text_width(&[ch]);
|
||||
|
||||
// Consider if we could be breaking the line at this position.
|
||||
if is_whitespace(ch) {
|
||||
// Break before the whitespace, without hyphen.
|
||||
line.length = i;
|
||||
line.advance.x = span_width;
|
||||
line.insert_hyphen_before_line_break = false;
|
||||
line.skip_next_chars = 1;
|
||||
if ch == ASCII_CR {
|
||||
// We'll be breaking the line, but advancing the cursor only by a half of the
|
||||
// regular line height.
|
||||
line.advance.y = text_font.line_height() / 2;
|
||||
}
|
||||
if ch == ASCII_LF || ch == ASCII_CR {
|
||||
// End of line, break immediately.
|
||||
return line;
|
||||
}
|
||||
found_any_whitespace = true;
|
||||
} else if span_width + char_width > max_width {
|
||||
// Return the last breakpoint.
|
||||
return line;
|
||||
} else {
|
||||
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
|
||||
let can_break_word = matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen)
|
||||
|| !found_any_whitespace;
|
||||
if have_space_for_break && can_break_word {
|
||||
// Break after this character, append hyphen.
|
||||
line.length = i + 1;
|
||||
line.advance.x = span_width + char_width;
|
||||
line.insert_hyphen_before_line_break = true;
|
||||
line.skip_next_chars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
span_width += char_width;
|
||||
}
|
||||
|
||||
// The whole text is fitting.
|
||||
Self {
|
||||
length: text.len(),
|
||||
advance: Offset::new(span_width, 0),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tokenizer_yields_expected_tokens() {
|
||||
use std::array::IntoIter;
|
||||
|
||||
assert!(Tokenizer::new(b"").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x\0y").eq(IntoIter::new([Token::Literal("x\0y".as_bytes())])));
|
||||
assert!(Tokenizer::new(b"{").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x{y").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"{}").eq(IntoIter::new([Token::Argument(b"")])));
|
||||
assert!(Tokenizer::new(b"x{}y{").eq(IntoIter::new([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Literal(b"y"),
|
||||
])));
|
||||
assert!(Tokenizer::new(b"{\0}").eq(IntoIter::new([Token::Argument("\0".as_bytes()),])));
|
||||
assert!(Tokenizer::new(b"{{y}").eq(IntoIter::new([Token::Argument(b"{y"),])));
|
||||
assert!(Tokenizer::new(b"{{{{xyz").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{}{{}}}}").eq(IntoIter::new([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Argument(b"{"),
|
||||
Token::Literal(b"}}}"),
|
||||
])));
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
use crate::ui::{
|
||||
component::model_tt::{ButtonStyle, ButtonStyleSheet, LabelStyle},
|
||||
display::{Color, Font},
|
||||
};
|
||||
|
||||
// Font constants.
|
||||
pub const FONT_NORMAL: Font = Font::new(-1);
|
||||
pub const FONT_BOLD: Font = Font::new(-2);
|
||||
pub const FONT_MONO: Font = Font::new(-3);
|
||||
|
||||
// Typical backlight values.
|
||||
pub const BACKLIGHT_NORMAL: i32 = 150;
|
||||
pub const BACKLIGHT_LOW: i32 = 45;
|
||||
pub const BACKLIGHT_DIM: i32 = 5;
|
||||
pub const BACKLIGHT_NONE: i32 = 2;
|
||||
pub const BACKLIGHT_MAX: i32 = 255;
|
||||
|
||||
// Color palette.
|
||||
pub const WHITE: Color = Color::rgb(255, 255, 255);
|
||||
pub const BLACK: Color = Color::rgb(0, 0, 0);
|
||||
pub const FG: Color = WHITE; // Default foreground (text & icon) color.
|
||||
pub const BG: Color = BLACK; // Default background color.
|
||||
pub const RED: Color = Color::rgb(205, 73, 73); // dark-coral
|
||||
pub const YELLOW: Color = Color::rgb(193, 144, 9); // ochre
|
||||
pub const GREEN: Color = Color::rgb(57, 168, 20); // grass-green
|
||||
pub const BLUE: Color = Color::rgb(0, 86, 190); // blue
|
||||
pub const GREY_LIGHT: Color = Color::rgb(168, 168, 168); // greyish
|
||||
pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // black
|
||||
|
||||
// Commonly used corner radius (i.e. for buttons).
|
||||
pub const RADIUS: u8 = 4;
|
||||
|
||||
// Size of icons in the UI (i.e. inside buttons).
|
||||
pub const ICON_SIZE: i32 = 16;
|
||||
|
||||
// UI icons.
|
||||
pub const ICON_CANCEL: &[u8] = include_res!("cancel.toif");
|
||||
pub const ICON_CONFIRM: &[u8] = include_res!("confirm.toif");
|
||||
pub const ICON_SPACE: &[u8] = include_res!("space.toif");
|
||||
|
||||
pub fn label_default() -> LabelStyle {
|
||||
LabelStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: FG,
|
||||
background_color: BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_default() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: FG,
|
||||
button_color: GREY_DARK,
|
||||
background_color: BG,
|
||||
border_color: BG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
active: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: BG,
|
||||
button_color: FG,
|
||||
background_color: BG,
|
||||
border_color: FG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
disabled: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: GREY_LIGHT,
|
||||
button_color: GREY_DARK,
|
||||
background_color: BG,
|
||||
border_color: BG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_confirm() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
||||
|
||||
pub fn button_cancel() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
||||
|
||||
pub fn button_clear() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
use crate::trezorhal::display;
|
||||
|
||||
use super::geometry::{Offset, Point, Rect};
|
||||
|
||||
pub fn width() -> i32 {
|
||||
display::width()
|
||||
}
|
||||
|
||||
pub fn height() -> i32 {
|
||||
display::height()
|
||||
}
|
||||
|
||||
pub fn size() -> Offset {
|
||||
Offset::new(width(), height())
|
||||
}
|
||||
|
||||
pub fn screen() -> Rect {
|
||||
Rect::from_top_left_and_size(Point::zero(), size())
|
||||
}
|
||||
|
||||
pub fn backlight(val: i32) -> i32 {
|
||||
display::backlight(val)
|
||||
}
|
||||
|
||||
pub fn rect(r: Rect, fg_color: Color) {
|
||||
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
|
||||
}
|
||||
|
||||
pub fn rounded_rect(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
|
||||
display::bar_radius(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
radius,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
|
||||
let toif_info = display::toif_info(data).unwrap();
|
||||
assert!(toif_info.grayscale);
|
||||
|
||||
let r = Rect::from_center_and_size(
|
||||
center,
|
||||
Offset::new(toif_info.width as _, toif_info.height as _),
|
||||
);
|
||||
display::icon(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
&data[12..], // skip TOIF header
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) {
|
||||
display::text(
|
||||
baseline.x,
|
||||
baseline.y,
|
||||
text,
|
||||
font.0,
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text_width(text: &[u8], font: Font) -> i32 {
|
||||
display::text_width(text, font.0)
|
||||
}
|
||||
|
||||
pub fn text_height() -> i32 {
|
||||
const TEXT_HEIGHT: i32 = 16;
|
||||
TEXT_HEIGHT
|
||||
}
|
||||
|
||||
pub fn line_height() -> i32 {
|
||||
const LINE_HEIGHT: i32 = 26;
|
||||
LINE_HEIGHT
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Font(i32);
|
||||
|
||||
impl Font {
|
||||
pub const fn new(id: i32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn text_width(self, text: &[u8]) -> i32 {
|
||||
text_width(text, self)
|
||||
}
|
||||
|
||||
pub fn line_height(self) -> i32 {
|
||||
line_height()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Color(u16);
|
||||
|
||||
impl Color {
|
||||
pub const fn from_u16(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r = (r as u16 & 0xF8) << 8;
|
||||
let g = (g as u16 & 0xFC) << 3;
|
||||
let b = (b as u16 & 0xF8) >> 3;
|
||||
Self(r | g | b)
|
||||
}
|
||||
|
||||
pub const fn r(self) -> u8 {
|
||||
(self.0 >> 8) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub const fn g(self) -> u8 {
|
||||
(self.0 >> 3) as u8 & 0xFC
|
||||
}
|
||||
|
||||
pub const fn b(self) -> u8 {
|
||||
(self.0 << 3) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub fn blend(self, other: Self, t: f32) -> Self {
|
||||
Self::rgb(
|
||||
lerp(self.r(), other.r(), t),
|
||||
lerp(self.g(), other.g(), t),
|
||||
lerp(self.b(), other.b(), t),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_u16(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Color {
|
||||
fn from(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for u16 {
|
||||
fn from(val: Color) -> Self {
|
||||
val.to_u16()
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(a: u8, b: u8, t: f32) -> u8 {
|
||||
(a as f32 + t * (b - a) as f32) as u8
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
use core::ops::{Add, Sub};
|
||||
|
||||
/// Relative offset in 2D space, used for representing translation and
|
||||
/// dimensions of objects. Absolute positions on the screen are represented by
|
||||
/// the `Point` type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Offset {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn uniform(a: i32) -> Self {
|
||||
Self::new(a, a)
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn abs(self) -> Self {
|
||||
Self::new(self.x.abs(), self.y.abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Offset {
|
||||
type Output = Offset;
|
||||
|
||||
fn add(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Offset {
|
||||
type Output = Offset;
|
||||
|
||||
fn sub(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in 2D space defined by the the `x` and `y` coordinate. Relative
|
||||
/// coordinates, vectors, and offsets are represented by the `Offset` type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Point {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn center(self, rhs: Self) -> Self {
|
||||
Self::new((self.x + rhs.x) / 2, (self.y + rhs.y) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn sub(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Point> for Point {
|
||||
type Output = Offset;
|
||||
|
||||
fn sub(self, rhs: Point) -> Self::Output {
|
||||
Offset::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangle in 2D space defined by the top-left point `x0`,`y0` and the
|
||||
/// bottom-right point `x1`,`y1`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x0: i32,
|
||||
pub y0: i32,
|
||||
pub x1: i32,
|
||||
pub y1: i32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub const fn new(p0: Point, p1: Point) -> Self {
|
||||
Self {
|
||||
x0: p0.x,
|
||||
y0: p0.y,
|
||||
x1: p1.x,
|
||||
y1: p1.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_top_left_and_size(p0: Point, size: Offset) -> Self {
|
||||
Self::new(p0, p0 + size)
|
||||
}
|
||||
|
||||
pub fn from_center_and_size(p: Point, size: Offset) -> Self {
|
||||
Self {
|
||||
x0: p.x - size.x / 2,
|
||||
y0: p.y - size.y / 2,
|
||||
x1: p.x + size.x / 2,
|
||||
y1: p.y + size.y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.x1 - self.x0
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i32 {
|
||||
self.y1 - self.y0
|
||||
}
|
||||
|
||||
pub fn top_left(&self) -> Point {
|
||||
Point::new(self.x0, self.y0)
|
||||
}
|
||||
|
||||
pub fn top_right(&self) -> Point {
|
||||
Point::new(self.x1, self.y0)
|
||||
}
|
||||
|
||||
pub fn bottom_left(&self) -> Point {
|
||||
Point::new(self.x0, self.y1)
|
||||
}
|
||||
|
||||
pub fn bottom_right(&self) -> Point {
|
||||
Point::new(self.x1, self.y1)
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Point {
|
||||
self.top_left().center(self.bottom_right())
|
||||
}
|
||||
|
||||
pub fn contains(&self, point: Point) -> bool {
|
||||
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1
|
||||
}
|
||||
|
||||
pub fn inset(&self, uniform: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x0 + uniform,
|
||||
y0: self.y0 + uniform,
|
||||
x1: self.x1 - uniform,
|
||||
y1: self.y1 - uniform,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cut_from_left(&self, width: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x0,
|
||||
y0: self.y0,
|
||||
x1: self.x0 + width,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cut_from_right(&self, width: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x1 - width,
|
||||
y0: self.y0,
|
||||
x1: self.x1,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Align {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
|
||||
pub struct Grid {
|
||||
/// Number of rows (cells on the y-axis) in the grid.
|
||||
pub rows: usize,
|
||||
/// Number of columns (cells on the x-axis) in the grid.
|
||||
pub cols: usize,
|
||||
/// Padding between cells.
|
||||
pub spacing: i32,
|
||||
/// Total area covered by this grid.
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn new(area: Rect, rows: usize, cols: usize) -> Self {
|
||||
Self {
|
||||
rows,
|
||||
cols,
|
||||
spacing: 0,
|
||||
area,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn row_col(&self, row: usize, col: usize) -> Rect {
|
||||
let cell_width = self.area.width() / self.cols as i32;
|
||||
let cell_height = self.area.height() / self.rows as i32;
|
||||
let x = col as i32 * cell_width;
|
||||
let y = row as i32 * cell_height;
|
||||
Rect {
|
||||
x0: self.area.x0 + x,
|
||||
y0: self.area.y0 + y,
|
||||
x1: self.area.x0 + x + (cell_width - self.spacing),
|
||||
y1: self.area.y0 + y + (cell_height - self.spacing),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cell(&self, index: usize) -> Rect {
|
||||
self.row_col(index / self.cols, index % self.cols)
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
micropython::{buffer::Buffer, obj::Obj},
|
||||
ui::{
|
||||
component::{
|
||||
model_tt::{theme, Button, Dialog, DialogMsg, Text},
|
||||
Child,
|
||||
},
|
||||
display,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
use super::obj::LayoutObj;
|
||||
|
||||
impl<T> TryFrom<DialogMsg<T>> for Obj
|
||||
where
|
||||
Obj: TryFrom<T>,
|
||||
Error: From<<T as TryInto<Obj>>::Error>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(val: DialogMsg<T>) -> Result<Self, Self::Error> {
|
||||
match val {
|
||||
DialogMsg::Content(c) => Ok(c.try_into()?),
|
||||
DialogMsg::LeftClicked => 1.try_into(),
|
||||
DialogMsg::RightClicked => 2.try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
|
||||
let block = move || {
|
||||
let param: Buffer = param.try_into()?;
|
||||
let layout = LayoutObj::new(Child::new(Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
Text::new(area, param)
|
||||
.with(b"some", "a few")
|
||||
.with(b"param", "xx")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
)))?;
|
||||
Ok(layout.into())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::trace::{Trace, Tracer};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Tracer for Vec<u8> {
|
||||
fn bytes(&mut self, b: &[u8]) {
|
||||
self.extend(b)
|
||||
}
|
||||
|
||||
fn string(&mut self, s: &str) {
|
||||
self.extend(s.as_bytes())
|
||||
}
|
||||
|
||||
fn symbol(&mut self, name: &str) {
|
||||
self.extend(name.as_bytes())
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.extend(b"<");
|
||||
self.extend(name.as_bytes());
|
||||
self.extend(b" ");
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.extend(name.as_bytes());
|
||||
self.extend(b":");
|
||||
value.trace(self);
|
||||
self.extend(b" ");
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.extend(b">")
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let layout = Child::new(Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
Text::new(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
));
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Dialog content:<Text content:Testing text layout, with
|
||||
some text, and some more
|
||||
text. And parameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
mod example;
|
||||
mod obj;
|
@ -0,0 +1,357 @@
|
||||
use core::{
|
||||
cell::RefCell,
|
||||
convert::{TryFrom, TryInto},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
micropython::{
|
||||
gc::Gc,
|
||||
map::Map,
|
||||
obj::{Obj, ObjBase},
|
||||
qstr::Qstr,
|
||||
typ::Type,
|
||||
},
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
||||
geometry::Point,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
/// Conversion trait implemented by components that know how to convert their
|
||||
/// message values into MicroPython `Obj`s. We can automatically implement
|
||||
/// `ComponentMsgObj` for components whose message types implement `TryInto`.
|
||||
pub trait ComponentMsgObj: Component {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>;
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for T
|
||||
where
|
||||
T: Component,
|
||||
T::Msg: TryInto<Obj, Error = Error>,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
msg.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to store any type of component in a layout, we need to access it
|
||||
/// through an object-safe trait. `Component` itself is not object-safe because
|
||||
/// of `Component::Msg` associated type. `ObjComponent` is a simple object-safe
|
||||
/// wrapping trait that is implemented for all components where `Component::Msg`
|
||||
/// can be converted to `Obj` through the `ComponentMsgObj` trait.
|
||||
pub trait ObjComponent {
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error>;
|
||||
fn obj_paint(&mut self);
|
||||
}
|
||||
|
||||
impl<T> ObjComponent for Child<T>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
{
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error> {
|
||||
if let Some(msg) = self.event(ctx, event) {
|
||||
self.inner().msg_try_into_obj(msg)
|
||||
} else {
|
||||
Ok(Obj::const_none())
|
||||
}
|
||||
}
|
||||
|
||||
fn obj_paint(&mut self) {
|
||||
self.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod maybe_trace {
|
||||
pub trait ObjComponentTrace: super::ObjComponent + crate::trace::Trace {}
|
||||
impl<T> ObjComponentTrace for T where T: super::ObjComponent + crate::trace::Trace {}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
mod maybe_trace {
|
||||
pub trait ObjComponentTrace: super::ObjComponent {}
|
||||
impl<T> ObjComponentTrace for T where T: super::ObjComponent {}
|
||||
}
|
||||
|
||||
/// Trait that combines `ObjComponent` with `Trace` if `ui_debug` is enabled,
|
||||
/// and pure `ObjComponent` otherwise.
|
||||
use maybe_trace::ObjComponentTrace;
|
||||
|
||||
/// `LayoutObj` is a GC-allocated object exported to MicroPython, with type
|
||||
/// `LayoutObj::obj_type()`. It wraps a root component through the
|
||||
/// `ObjComponent` trait.
|
||||
#[repr(C)]
|
||||
pub struct LayoutObj {
|
||||
base: ObjBase,
|
||||
inner: RefCell<LayoutObjInner>,
|
||||
}
|
||||
|
||||
struct LayoutObjInner {
|
||||
root: Gc<dyn ObjComponentTrace>,
|
||||
event_ctx: EventCtx,
|
||||
timer_fn: Obj,
|
||||
}
|
||||
|
||||
impl LayoutObj {
|
||||
/// Create a new `LayoutObj`, wrapping a root component.
|
||||
pub fn new(root: impl ObjComponentTrace + 'static) -> Result<Gc<Self>, Error> {
|
||||
// SAFETY: We are coercing GC-allocated sized ptr into an unsized one.
|
||||
let root =
|
||||
unsafe { Gc::from_raw(Gc::into_raw(Gc::new(root)?) as *mut dyn ObjComponentTrace) };
|
||||
|
||||
Gc::new(Self {
|
||||
base: Self::obj_type().as_base(),
|
||||
inner: RefCell::new(LayoutObjInner {
|
||||
root,
|
||||
event_ctx: EventCtx::new(),
|
||||
timer_fn: Obj::const_none(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Timer callback is expected to be a callable object of the following
|
||||
/// form: `def timer(token: int, deadline_in_ms: int)`.
|
||||
fn obj_set_timer_fn(&self, timer_fn: Obj) {
|
||||
self.inner.borrow_mut().timer_fn = timer_fn;
|
||||
}
|
||||
|
||||
/// Run an event pass over the component tree. After the traversal, any
|
||||
/// pending timers are drained into `self.timer_callback`. Returns `Err`
|
||||
/// in case the timer callback raises or one of the components returns
|
||||
/// an error, `Ok` with the message otherwise.
|
||||
fn obj_event(&self, event: Event) -> Result<Obj, Error> {
|
||||
let inner = &mut *self.inner.borrow_mut();
|
||||
|
||||
// Clear the upwards-propagating paint request flag from the last event pass.
|
||||
inner.event_ctx.clear_paint_requests();
|
||||
|
||||
// Send the event down the component tree. Bail out in case of failure.
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
let msg = unsafe { Gc::as_mut(&mut inner.root) }.obj_event(&mut inner.event_ctx, event)?;
|
||||
|
||||
// All concerning `Child` wrappers should have already marked themselves for
|
||||
// painting by now, and we're prepared for a paint pass.
|
||||
|
||||
// Drain any pending timers into the callback.
|
||||
while let Some((token, deadline)) = inner.event_ctx.pop_timer() {
|
||||
let token = token.try_into();
|
||||
let deadline = deadline.try_into();
|
||||
if let (Ok(token), Ok(deadline)) = (token, deadline) {
|
||||
inner.timer_fn.call_with_n_args(&[token, deadline])?;
|
||||
} else {
|
||||
// Failed to convert token or deadline into `Obj`, skip.
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Run a paint pass over the component tree.
|
||||
fn obj_paint_if_requested(&self) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_paint();
|
||||
}
|
||||
|
||||
/// Run a tracing pass over the component tree. Passed `callback` is called
|
||||
/// with each piece of tracing information. Panics in case the callback
|
||||
/// raises an exception.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
fn obj_trace(&self, callback: Obj) {
|
||||
use crate::trace::{Trace, Tracer};
|
||||
|
||||
struct CallbackTracer(Obj);
|
||||
|
||||
impl Tracer for CallbackTracer {
|
||||
fn bytes(&mut self, b: &[u8]) {
|
||||
self.0.call_with_n_args(&[b.try_into().unwrap()]).unwrap();
|
||||
}
|
||||
|
||||
fn string(&mut self, s: &str) {
|
||||
self.0.call_with_n_args(&[s.try_into().unwrap()]).unwrap();
|
||||
}
|
||||
|
||||
fn symbol(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
value.trace(self);
|
||||
}
|
||||
|
||||
fn close(&mut self) {}
|
||||
}
|
||||
|
||||
self.inner
|
||||
.borrow()
|
||||
.root
|
||||
.trace(&mut CallbackTracer(callback));
|
||||
}
|
||||
|
||||
fn obj_type() -> &'static Type {
|
||||
static TYPE: Type = obj_type! {
|
||||
name: Qstr::MP_QSTR_Layout,
|
||||
locals: &obj_dict!(obj_map! {
|
||||
Qstr::MP_QSTR_set_timer_fn => obj_fn_2!(ui_layout_set_timer_fn).as_obj(),
|
||||
Qstr::MP_QSTR_touch_start => obj_fn_3!(ui_layout_touch_start).as_obj(),
|
||||
Qstr::MP_QSTR_touch_move => obj_fn_3!(ui_layout_touch_move).as_obj(),
|
||||
Qstr::MP_QSTR_touch_end => obj_fn_3!(ui_layout_touch_end).as_obj(),
|
||||
Qstr::MP_QSTR_timer => obj_fn_2!(ui_layout_timer).as_obj(),
|
||||
Qstr::MP_QSTR_paint => obj_fn_1!(ui_layout_paint).as_obj(),
|
||||
Qstr::MP_QSTR_trace => obj_fn_2!(ui_layout_trace).as_obj(),
|
||||
}),
|
||||
};
|
||||
&TYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gc<LayoutObj>> for Obj {
|
||||
fn from(val: Gc<LayoutObj>) -> Self {
|
||||
// SAFETY:
|
||||
// - We are GC-allocated.
|
||||
// - We are `repr(C)`.
|
||||
// - We have a `base` as the first field with the correct type.
|
||||
unsafe { Obj::from_ptr(Gc::into_raw(val).cast()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Obj> for Gc<LayoutObj> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Obj) -> Result<Self, Self::Error> {
|
||||
if LayoutObj::obj_type().is_type_of(value) {
|
||||
// SAFETY: We assume that if `value` is an object pointer with the correct type,
|
||||
// it is always GC-allocated.
|
||||
let this = unsafe { Gc::from_raw(value.as_ptr().cast()) };
|
||||
Ok(this)
|
||||
} else {
|
||||
Err(Error::TypeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Obj> for TimerToken {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Obj) -> Result<Self, Self::Error> {
|
||||
let raw: usize = value.try_into()?;
|
||||
let this = Self::from_raw(raw);
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TimerToken> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: TimerToken) -> Result<Self, Self::Error> {
|
||||
value.into_raw().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Duration> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Duration) -> Result<Self, Self::Error> {
|
||||
let millis: usize = value.as_millis().try_into()?;
|
||||
millis.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Never> for Obj {
|
||||
fn from(_: Never) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_set_timer_fn(this: Obj, timer_fn: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_set_timer_fn(timer_fn);
|
||||
Ok(Obj::const_true())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_start(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchStart(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_move(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchMove(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_end(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchEnd(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_timer(this: Obj, token: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::Timer(token.try_into()?);
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_paint(this: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_paint_if_requested();
|
||||
Ok(Obj::const_true())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ui_debug_layout_type() -> &'static Type {
|
||||
LayoutObj::obj_type()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
extern "C" fn ui_layout_trace(this: Obj, callback: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_trace(callback);
|
||||
Ok(Obj::const_none())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
extern "C" fn ui_layout_trace(_this: Obj, _callback: Obj) -> Obj {
|
||||
Obj::const_none()
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
macro_rules! include_res {
|
||||
($filename:expr) => {
|
||||
include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../src/trezor/res/",
|
||||
$filename,
|
||||
))
|
||||
};
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod component;
|
||||
pub mod display;
|
||||
pub mod geometry;
|
||||
pub mod layout;
|
@ -0,0 +1,6 @@
|
||||
from typing import *
|
||||
|
||||
|
||||
# extmod/rustmods/modtrezorui2.c
|
||||
def layout_new_example(text: str) -> None:
|
||||
"""Example layout."""
|
Loading…
Reference in new issue