use crate::{
buttons::ButtonEvent::*,
io::{self, ApduHeader, Comm, Event, Reply},
uxapp::{UxEvent, BOLOS_UX_OK},
};
use ledger_secure_sdk_sys::{
buttons::{get_button_event, ButtonEvent, ButtonsState},
seph,
};
use crate::ui::bitmaps::{Glyph, WARNING};
use crate::ui::{bagls::*, fonts::OPEN_SANS};
use crate::ui::layout;
use crate::ui::layout::{Draw, Location, StringPlace};
use numtoa::NumToA;
const MAX_CHAR_PER_LINE: usize = 17;
pub fn get_event(buttons: &mut ButtonsState) -> Option<ButtonEvent> {
let mut io_buffer = [0u8; 273];
let status = seph::io_rx(&mut io_buffer, true);
if status > 0 {
let packet_type = io_buffer[0];
match packet_type {
0x01 | 0x02 => {
if io_buffer[1] == 0x05 {
let button_info = io_buffer[4] >> 1;
return get_button_event(buttons, button_info);
}
}
_ => {}
}
}
None
}
pub fn clear_screen() {
unsafe {
ledger_secure_sdk_sys::screen_clear();
}
}
pub fn display_pending_review(comm: &mut Comm) {
clear_screen();
WARNING.draw(57, 10);
"Pending".place(Location::Custom(28), Layout::Centered, true);
"Ledger review".place(Location::Custom(42), Layout::Centered, true);
crate::ui::screen_util::screen_update();
loop {
if let Event::Button(BothButtonsRelease) = comm.next_event::<ApduHeader>() {
break;
}
}
}
pub fn popup(message: &str) {
clear_screen();
SingleMessage::new(message).show_and_wait();
}
pub struct Validator<'a> {
message: &'a str,
}
impl<'a> Validator<'a> {
pub fn new(message: &'a str) -> Self {
Validator { message }
}
pub fn ask(&self) -> bool {
clear_screen();
let mut buttons = ButtonsState::new();
let mut lines = [Label::from_const("Cancel"), Label::from(self.message)];
lines[0].bold = true;
let redraw = |lines_list: &[Label; 2]| {
clear_screen();
lines_list.place(Location::Middle, Layout::Centered, false);
UP_ARROW.display();
DOWN_ARROW.display();
crate::ui::screen_util::screen_update();
};
redraw(&lines);
let mut response = false;
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonPress) => {
UP_S_ARROW.instant_display();
}
Some(ButtonEvent::RightButtonPress) => {
DOWN_S_ARROW.instant_display();
}
Some(ButtonEvent::LeftButtonRelease) => {
UP_S_ARROW.erase();
response = false;
lines[0].bold = true;
lines[1].bold = false;
lines.place(Location::Middle, Layout::Centered, false);
redraw(&lines);
}
Some(ButtonEvent::RightButtonRelease) => {
DOWN_S_ARROW.erase();
response = true;
lines[0].bold = false;
lines[1].bold = true;
redraw(&lines);
}
Some(ButtonEvent::BothButtonsPress) => {
UP_ARROW.erase();
DOWN_ARROW.erase();
}
Some(ButtonEvent::BothButtonsRelease) => return response,
_ => (),
}
}
}
}
pub struct MessageValidator<'a> {
message: &'a [&'a str],
confirm: &'a [&'a str],
cancel: &'a [&'a str],
}
use crate::ui::layout::*;
impl<'a> MessageValidator<'a> {
pub const fn new(
message: &'a [&'a str],
confirm: &'a [&'a str],
cancel: &'a [&'a str],
) -> Self {
MessageValidator {
message,
confirm,
cancel,
}
}
pub fn ask(&self) -> bool {
clear_screen();
let page_count = &self.message.len() + 2;
let mut cur_page = 0;
let draw_icon_and_text = |icon: Icon, strings: &[&str]| {
let x = match strings.len() {
0 => 60,
_ => 18,
};
icon.set_x(x).display();
match strings.len() {
0 => {}
1 => {
strings[0].place(Location::Middle, Layout::Centered, false);
}
_ => {
strings[..2].place(Location::Middle, Layout::Centered, false);
}
}
};
let draw = |page: usize| {
clear_screen();
if page == page_count - 2 {
draw_icon_and_text(VALIDATE_14_ICON, self.confirm);
RIGHT_ARROW.display();
} else if page == page_count - 1 {
draw_icon_and_text(CROSSMARK_ICON, self.cancel);
} else {
self.message[page].place(Location::Middle, Layout::Centered, false);
RIGHT_ARROW.display();
}
if page > 0 {
LEFT_ARROW.display();
}
crate::ui::screen_util::screen_update();
};
draw(cur_page);
let mut buttons = ButtonsState::new();
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonPress) => {
LEFT_S_ARROW.instant_display();
}
Some(ButtonEvent::RightButtonPress) => {
RIGHT_S_ARROW.instant_display();
}
Some(ButtonEvent::LeftButtonRelease) => {
LEFT_S_ARROW.erase();
cur_page = cur_page.saturating_sub(1);
draw(cur_page);
}
Some(ButtonEvent::RightButtonRelease) => {
RIGHT_S_ARROW.erase();
if cur_page < page_count - 1 {
cur_page += 1;
}
draw(cur_page);
}
Some(ButtonEvent::BothButtonsRelease) => {
if cur_page == page_count - 2 {
return true;
} else if cur_page == page_count - 1 {
return false;
}
draw(cur_page);
}
_ => (),
}
}
}
}
pub struct Menu<'a> {
panels: &'a [&'a str],
}
impl<'a> Menu<'a> {
pub fn new(panels: &'a [&'a str]) -> Self {
Menu { panels }
}
pub fn show(&self) -> usize {
clear_screen();
let mut buttons = ButtonsState::new();
let mut items: [Label; layout::MAX_LINES] =
core::array::from_fn(|i| Label::from(*self.panels.get(i).unwrap_or(&"")));
items[0].bold = true;
items.place(Location::Middle, Layout::Centered, false);
UP_ARROW.display();
DOWN_ARROW.display();
crate::ui::screen_util::screen_update();
let mut index = 0;
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonPress) => {
UP_S_ARROW.instant_display();
}
Some(ButtonEvent::RightButtonPress) => {
DOWN_S_ARROW.instant_display();
}
Some(ButtonEvent::BothButtonsRelease) => return index,
Some(x) => {
match x {
ButtonEvent::LeftButtonRelease => {
index = index.saturating_sub(1);
}
ButtonEvent::RightButtonRelease => {
if index < self.panels.len() - 1 {
index += 1;
}
}
_ => (),
}
clear_screen();
UP_ARROW.display();
DOWN_ARROW.display();
let chunk = (index / layout::MAX_LINES) * layout::MAX_LINES;
for (i, item) in items.iter_mut().enumerate() {
item.text = self.panels.get(chunk + i).unwrap_or(&"");
item.bold = false;
}
items[index - chunk].bold = true;
items.place(Location::Middle, Layout::Centered, false);
crate::ui::screen_util::screen_update();
}
_ => (),
}
}
}
}
#[derive(Copy, Clone, PartialEq, Default)]
pub enum PageStyle {
#[default]
PictureNormal, PictureBold, BoldNormal, Normal, BoldCenteredNormal, }
#[derive(Copy, Clone, Default)]
pub struct Page<'a> {
style: PageStyle,
label: [&'a str; 2],
glyph: Option<&'a Glyph<'a>>,
}
impl<'a> From<([&'a str; 2], &'a Glyph<'a>)> for Page<'a> {
fn from((label, glyph): ([&'a str; 2], &'a Glyph<'a>)) -> Page<'a> {
Page::new(PageStyle::PictureNormal, [label[0], label[1]], Some(glyph))
}
}
impl<'a> From<([&'a str; 2], bool)> for Page<'a> {
fn from((label, bold): ([&'a str; 2], bool)) -> Page<'a> {
if bold {
Page::new(PageStyle::BoldNormal, [label[0], label[1]], None)
} else {
Page::new(PageStyle::Normal, [label[0], label[1]], None)
}
}
}
impl<'a> From<([&'a str; 2], bool, bool)> for Page<'a> {
fn from((label, bold, centered): ([&'a str; 2], bool, bool)) -> Page<'a> {
if centered {
if bold {
Page::new(PageStyle::BoldCenteredNormal, [label[0], label[1]], None)
} else {
Page::new(PageStyle::Normal, [label[0], label[1]], None)
}
} else {
Page::new(PageStyle::BoldNormal, [label[0], label[1]], None)
}
}
}
impl<'a> From<(&'a str, &'a Glyph<'a>)> for Page<'a> {
fn from((label, glyph): (&'a str, &'a Glyph<'a>)) -> Page<'a> {
let label = [label, ""];
Page::new(PageStyle::PictureBold, label, Some(glyph))
}
}
impl<'a> Page<'a> {
pub const fn new(style: PageStyle, label: [&'a str; 2], glyph: Option<&'a Glyph<'a>>) -> Self {
Page {
style,
label,
glyph,
}
}
pub fn place(&self) {
match self.style {
PageStyle::Normal => {
self.label.place(Location::Middle, Layout::Centered, false);
}
PageStyle::BoldCenteredNormal => {
self.label.place(Location::Middle, Layout::Centered, true);
}
PageStyle::PictureNormal => {
let icon_x = 57;
let icon_y = 10;
self.label
.place(Location::Custom(28), Layout::Centered, false);
if let Some(glyph) = self.glyph {
let icon = Icon::from(glyph);
icon.set_x(icon_x).set_y(icon_y).display();
}
}
PageStyle::PictureBold => {
let icon_x = 57;
let icon_y = 17;
self.label[0].place(Location::Custom(35), Layout::Centered, true);
self.label[1].place(Location::Custom(49), Layout::Centered, true);
if let Some(glyph) = self.glyph {
let icon = Icon::from(glyph);
icon.set_x(icon_x).set_y(icon_y).display();
}
}
PageStyle::BoldNormal => {
let padding = 1;
let max_text_lines = 3;
let total_height = (OPEN_SANS[0].height * max_text_lines) as usize
+ OPEN_SANS[1].height as usize
+ 2 * padding as usize;
let mut cur_y = Location::Middle.get_y(total_height);
self.label[0].place(Location::Custom(cur_y), Layout::Centered, true);
cur_y += OPEN_SANS[0].height as usize + 2 * padding as usize;
let mut indices = [(0, 0); 3];
let len = self.label[1].len();
for (i, indice) in indices.iter_mut().enumerate() {
let start = (i * MAX_CHAR_PER_LINE).min(len);
if start >= len {
break; }
let end = (start + MAX_CHAR_PER_LINE).min(len);
*indice = (start, end);
(&self.label[1][start..end]).place(
Location::Custom(cur_y),
Layout::Centered,
false,
);
cur_y += OPEN_SANS[0].height as usize + 2 * padding as usize;
}
}
}
}
pub fn place_and_wait(&self) {
let mut buttons = ButtonsState::new();
self.place();
crate::ui::screen_util::screen_update();
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonRelease)
| Some(ButtonEvent::RightButtonRelease)
| Some(ButtonEvent::BothButtonsRelease) => return,
_ => (),
}
}
}
}
pub enum EventOrPageIndex<T> {
Event(io::Event<T>),
Index(usize),
}
struct Temp {}
impl TryFrom<io::ApduHeader> for Temp {
type Error = io::StatusWords;
fn try_from(_header: io::ApduHeader) -> Result<Self, Self::Error> {
Ok(Self {})
}
}
#[cfg(not(feature = "io_new"))]
pub struct MultiPageMenu<'a> {
comm: &'a mut io::Comm,
pages: &'a [&'a Page<'a>],
}
#[cfg(not(feature = "io_new"))]
impl<'a> MultiPageMenu<'a> {
pub fn new(comm: &'a mut io::Comm, pages: &'a [&'a Page]) -> Self {
MultiPageMenu { comm, pages }
}
pub fn show<T: TryFrom<ApduHeader>>(&mut self) -> EventOrPageIndex<T>
where
Reply: From<<T as TryFrom<ApduHeader>>::Error>,
{
self.show_from(0)
}
pub fn show_from<T: TryFrom<ApduHeader>>(&mut self, page_index: usize) -> EventOrPageIndex<T>
where
Reply: From<<T as TryFrom<ApduHeader>>::Error>,
{
clear_screen();
self.pages[page_index].place();
LEFT_ARROW.display();
RIGHT_ARROW.display();
crate::ui::screen_util::screen_update();
let mut index = page_index;
loop {
match self.comm.next_event() {
io::Event::Button(button) => {
if UxEvent::Event.request() == BOLOS_UX_OK {
match button {
BothButtonsRelease => return EventOrPageIndex::Index(index),
b => {
match b {
LeftButtonRelease => {
if index as i16 - 1 < 0 {
index = self.pages.len() - 1;
} else {
index = index.saturating_sub(1);
}
}
RightButtonRelease => {
if index < self.pages.len() - 1 {
index += 1;
} else {
index = 0;
}
}
_ => (),
}
clear_screen();
self.pages[index].place();
LEFT_ARROW.display();
RIGHT_ARROW.display();
crate::ui::screen_util::screen_update();
}
}
}
}
io::Event::Command(ins) => return EventOrPageIndex::Event(io::Event::Command(ins)),
io::Event::Ticker => {
if UxEvent::Event.request() != BOLOS_UX_OK {
UxEvent::block_and_get_event::<Temp>(self.comm);
return EventOrPageIndex::Event(io::Event::Ticker);
}
}
};
}
}
}
pub struct SingleMessage<'a> {
message: &'a str,
}
impl<'a> SingleMessage<'a> {
pub fn new(message: &'a str) -> Self {
SingleMessage { message }
}
pub fn show(&self) {
clear_screen();
self.message
.place(Location::Middle, Layout::Centered, false);
}
pub fn show_and_wait(&self) {
let mut buttons = ButtonsState::new();
self.show();
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonRelease)
| Some(ButtonEvent::RightButtonRelease)
| Some(ButtonEvent::BothButtonsRelease) => return,
_ => (),
}
}
}
}
pub struct MessageScroller<'a> {
message: &'a str,
}
impl<'a> MessageScroller<'a> {
pub fn new(message: &'a str) -> Self {
MessageScroller { message }
}
pub fn event_loop(&self) {
clear_screen();
let mut buttons = ButtonsState::new();
let page_count = (self.message.len() - 1) / MAX_CHAR_PER_LINE + 1;
if page_count == 0 {
return;
}
let mut label = Label::from("");
let mut cur_page = 0;
let mut draw = |page: usize| {
let start = page * MAX_CHAR_PER_LINE;
let end = (start + MAX_CHAR_PER_LINE).min(self.message.len());
let chunk = &self.message[start..end];
label.erase();
label.text = chunk;
LEFT_ARROW.erase();
RIGHT_ARROW.erase();
if page > 0 {
LEFT_ARROW.display();
}
if page + 1 < page_count {
RIGHT_ARROW.display();
}
label.instant_display();
};
draw(cur_page);
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonPress) => {
LEFT_S_ARROW.instant_display();
}
Some(ButtonEvent::RightButtonPress) => {
RIGHT_S_ARROW.instant_display();
}
Some(ButtonEvent::LeftButtonRelease) => {
LEFT_S_ARROW.erase();
cur_page = cur_page.saturating_sub(1);
draw(cur_page);
}
Some(ButtonEvent::RightButtonRelease) => {
RIGHT_S_ARROW.erase();
if cur_page + 1 < page_count {
cur_page += 1;
}
draw(cur_page);
}
Some(ButtonEvent::BothButtonsRelease) => break,
Some(_) | None => (),
}
}
}
}
pub struct Field<'a> {
pub name: &'a str,
pub value: &'a str,
}
impl<'a> Field<'a> {
pub fn event_loop(&self, incoming_direction: ButtonEvent, is_first_field: bool) -> ButtonEvent {
let mut buttons = ButtonsState::new();
let chunk_max_lines = layout::MAX_LINES - 1;
let page_count = 1 + self.value.len() / (chunk_max_lines * MAX_CHAR_PER_LINE);
let mut cur_page = match incoming_direction {
ButtonEvent::LeftButtonRelease => page_count - 1,
ButtonEvent::RightButtonRelease => 0,
_ => 0,
};
let draw = |page: usize| {
clear_screen();
let mut chunks = [Label::default(); layout::MAX_LINES];
for (i, chunk) in self
.value
.as_bytes()
.chunks(MAX_CHAR_PER_LINE)
.skip(page * chunk_max_lines)
.take(chunk_max_lines)
.enumerate()
{
chunks[1 + i] = Label::from(core::str::from_utf8(chunk).unwrap_or(""));
}
let mut header_buf = [b' '; MAX_CHAR_PER_LINE + 4];
if page == 0 && MAX_CHAR_PER_LINE * chunk_max_lines > self.value.len() {
header_buf[..self.name.len()].copy_from_slice(self.name.as_bytes());
} else {
let mut buf_page = [0u8; 3];
let mut buf_count = [0u8; 3];
let page_str = (page + 1).numtoa_str(10, &mut buf_page);
let count_str = page_count.numtoa_str(10, &mut buf_count);
concatenate(
&[&self.name, " (", &page_str, "/", &count_str, ")"],
&mut header_buf,
);
}
let header = core::str::from_utf8(&header_buf)
.unwrap_or("")
.trim_end_matches(' ');
chunks[0] = Label::from(header).bold();
if !is_first_field {
LEFT_ARROW.display();
}
RIGHT_ARROW.display();
chunks.place(Location::Middle, Layout::Centered, false);
crate::ui::screen_util::screen_update();
};
draw(cur_page);
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonPress) => {
LEFT_S_ARROW.instant_display();
}
Some(ButtonEvent::RightButtonPress) => {
RIGHT_S_ARROW.instant_display();
}
Some(ButtonEvent::LeftButtonRelease) => {
LEFT_S_ARROW.erase();
if cur_page == 0 {
return ButtonEvent::LeftButtonRelease;
}
cur_page = cur_page.saturating_sub(1);
draw(cur_page);
}
Some(ButtonEvent::RightButtonRelease) => {
RIGHT_S_ARROW.erase();
if cur_page + 1 == page_count {
return ButtonEvent::RightButtonRelease;
}
if cur_page + 1 < page_count {
cur_page += 1;
}
draw(cur_page);
}
Some(_) | None => (),
}
}
}
}
pub struct MultiFieldReview<'a> {
fields: &'a [Field<'a>],
review_message: &'a [&'a str],
review_glyph: Option<&'a Glyph<'a>>,
validation_message: [&'a str; 2],
validation_glyph: Option<&'a Glyph<'a>>,
cancel_message: &'a str,
cancel_glyph: Option<&'a Glyph<'a>>,
}
fn concatenate(strings: &[&str], output: &mut [u8]) {
let mut offset = 0;
for s in strings {
let s_len = s.len();
let copy_len = core::cmp::min(s_len, output.len() - offset);
if copy_len > 0 {
output[offset..offset + copy_len].copy_from_slice(&s.as_bytes()[..copy_len]);
offset += copy_len;
} else {
break;
}
}
}
impl<'a> MultiFieldReview<'a> {
pub fn new(
fields: &'a [Field<'a>],
review_message: &'a [&'a str],
review_glyph: Option<&'a Glyph<'a>>,
validation_message: &'a str,
validation_glyph: Option<&'a Glyph<'a>>,
cancel_message: &'a str,
cancel_glyph: Option<&'a Glyph<'a>>,
) -> Self {
Self::new_with_validation_messages(
fields,
review_message,
review_glyph,
[validation_message, ""],
validation_glyph,
cancel_message,
cancel_glyph,
)
}
pub fn new_with_validation_messages(
fields: &'a [Field<'a>],
review_message: &'a [&'a str],
review_glyph: Option<&'a Glyph<'a>>,
validation_message: [&'a str; 2],
validation_glyph: Option<&'a Glyph<'a>>,
cancel_message: &'a str,
cancel_glyph: Option<&'a Glyph<'a>>,
) -> Self {
MultiFieldReview {
fields,
review_message,
review_glyph,
validation_message,
validation_glyph,
cancel_message,
cancel_glyph,
}
}
pub fn show(&self) -> bool {
let first_page_opt = match self.review_message.len() {
0 => None,
1 => Some(Page::new(
PageStyle::PictureBold,
[self.review_message[0], ""],
self.review_glyph,
)),
_ => Some(Page::new(
PageStyle::PictureNormal,
[self.review_message[0], self.review_message[1]],
self.review_glyph,
)),
};
display_first_page(&first_page_opt);
let validation_page = Page::new(
PageStyle::PictureBold,
self.validation_message,
self.validation_glyph,
);
let cancel_page = Page::new(
PageStyle::PictureBold,
[self.cancel_message, ""],
self.cancel_glyph,
);
let mut cur_page = 0usize;
let mut direction = ButtonEvent::RightButtonRelease;
loop {
match cur_page {
cancel if cancel == self.fields.len() + 1 => {
let mut buttons = ButtonsState::new();
clear_screen();
LEFT_ARROW.display();
cancel_page.place();
crate::ui::screen_util::screen_update();
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonRelease) => {
cur_page = cur_page.saturating_sub(1);
break;
}
Some(ButtonEvent::BothButtonsRelease) => return false,
_ => (),
}
}
}
validation if validation == self.fields.len() => {
let mut buttons = ButtonsState::new();
clear_screen();
LEFT_ARROW.display();
RIGHT_ARROW.display();
validation_page.place();
crate::ui::screen_util::screen_update();
loop {
match get_event(&mut buttons) {
Some(ButtonEvent::LeftButtonRelease) => {
cur_page = cur_page.saturating_sub(1);
if cur_page == 0 && self.fields.is_empty() {
display_first_page(&first_page_opt);
} else {
direction = ButtonEvent::LeftButtonRelease;
}
break;
}
Some(ButtonEvent::RightButtonRelease) => {
cur_page += 1;
break;
}
Some(ButtonEvent::BothButtonsRelease) => return true,
_ => (),
}
}
}
_ => {
direction = self.fields[cur_page]
.event_loop(direction, cur_page == 0 && first_page_opt.is_none());
match direction {
ButtonEvent::LeftButtonRelease => {
if cur_page == 0 {
display_first_page(&first_page_opt);
direction = ButtonEvent::RightButtonRelease;
} else {
cur_page -= 1;
}
}
ButtonEvent::RightButtonRelease => {
cur_page += 1;
}
_ => (),
}
}
}
}
}
}
fn display_first_page(page_opt: &Option<Page>) {
match page_opt {
Some(page) => {
clear_screen();
RIGHT_ARROW.display();
page.place();
crate::ui::screen_util::screen_update();
let mut buttons = ButtonsState::new();
loop {
if let Some(ButtonEvent::RightButtonRelease) = get_event(&mut buttons) {
return;
}
}
}
None => (),
}
}