diff --git a/ledger_device_ui_sdk/src/ui.rs b/ledger_device_ui_sdk/src/ui.rs index 3e6377da..4abef82f 100644 --- a/ledger_device_ui_sdk/src/ui.rs +++ b/ledger_device_ui_sdk/src/ui.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use core::str::from_utf8; + use ledger_secure_sdk_sys::{seph, buttons::{get_button_event, ButtonEvent, ButtonsState}}; use ledger_device_sdk::{io, buttons::ButtonEvent::*}; @@ -12,7 +14,7 @@ use crate::layout::{Draw, Location, StringPlace}; use numtoa::NumToA; -const MAX_CHAR_PER_LINE: usize = 18; +const MAX_CHAR_PER_LINE: usize = 17; /// Handles communication to filter /// out actual events, and converts key @@ -311,7 +313,7 @@ impl<'a> Menu<'a> { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq)] pub enum PageStyle { PictureNormal, // Picture (should be 16x16) with two lines of text (page layout depends on device). PictureBold, // Icon on top with one line of text on the bottom. @@ -331,7 +333,7 @@ pub struct Page<'a> { // new_picture_normal 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, Some(glyph)) + Page::new(PageStyle::PictureNormal, [label[0], label[1]], Some(glyph)) } } @@ -339,11 +341,9 @@ impl<'a> From<([&'a str; 2], &'a Glyph<'a>)> for Page<'a> { 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, None) - } - else - { - Page::new(PageStyle::Normal, label, None) + Page::new(PageStyle::BoldNormal, [label[0], label[1]], None) + } else { + Page::new(PageStyle::Normal, [label[0], label[1]], None) } } } @@ -370,26 +370,10 @@ impl<'a> Page<'a> { } pub fn place(&self) { - let mut label_bytes = [0u8; MAX_CHAR_PER_LINE]; - let mut label_str: &str = ""; - // Notes: - // * chunk_count and chunk_idx are only used for multi-page fields - // and are set to 0 by default. MultiFieldReview is the only place where - // these values are set. - // * Only pages of style BoldNormal will display the chunk count and index. - if self.chunk_count > 1 { - // Convert the chunk count to a string - let mut chunk_count_buf = [0u8;3]; - let chunk_count_str = self.chunk_count.numtoa_str(10,&mut chunk_count_buf); - // Convert the chunk index to a string - let mut chunk_idx_buf = [0u8;3]; - let chunk_idx_str = self.chunk_idx.numtoa_str(10,&mut chunk_idx_buf); - // Add the chunk count and index to the label - concatenate(&[self.label[0], " (", chunk_idx_str, "/", chunk_count_str, ")"], &mut label_bytes); - label_str = core::str::from_utf8(&mut label_bytes).unwrap(); - } - match self.style { + PageStyle::Normal => { + self.label.place(Location::Middle, Layout::Centered, false); + } PageStyle::PictureNormal => { let mut icon_x = 16; let mut icon_y = 8; @@ -430,21 +414,65 @@ impl<'a> Page<'a> { } PageStyle::BoldNormal => { let padding = 1; - let total_height = OPEN_SANS[0].height as usize + let mut max_text_lines = 3; + if cfg!(target_os = "nanos") + { + max_text_lines = 1; + } + 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); + + // Display the chunk count and index if needed if self.chunk_count > 1 { - label_str.place(Location::Custom(cur_y), Layout::Centered, true); - } - else { + let mut label_bytes = [0u8; MAX_CHAR_PER_LINE]; + // Convert the chunk count to a string + let mut chunk_count_buf = [0u8; 3]; + let chunk_count_str = self.chunk_count.numtoa_str(10, &mut chunk_count_buf); + // Convert the chunk index to a string + let mut chunk_idx_buf = [0u8; 3]; + let chunk_idx_str = self.chunk_idx.numtoa_str(10, &mut chunk_idx_buf); + // Add the chunk count and index to the label + concatenate( + &[ + self.label[0], + " (", + chunk_idx_str, + "/", + chunk_count_str, + ")", + ], + &mut label_bytes, + ); + from_utf8(&mut label_bytes).unwrap().trim_matches(char::from(0)).place(Location::Custom(cur_y), Layout::Centered, true); + } else { self.label[0].place(Location::Custom(cur_y), Layout::Centered, true); } cur_y += OPEN_SANS[0].height as usize + 2 * padding as usize; - self.label[1].place(Location::Custom(cur_y), Layout::Centered, false); - } - PageStyle::Normal => { - self.label.place(Location::Middle, Layout::Centered, false); + + // If the device is a Nano S, display the second label as + // a single line of text + if cfg!(target_os = "nanos") + { + self.label[1].place(Location::Custom(cur_y), Layout::Centered, false); + } + // Otherwise, display the second label as up to 3 lines of text + else { + let mut indices = [(0, 0); 3]; + let len = self.label[1].len(); + for i in 0..3 { + let start = (i * MAX_CHAR_PER_LINE).min(len); + if start >= len { + break; // Break if we reach the end of the string + } + let end = (start + MAX_CHAR_PER_LINE).min(len); + indices[i] = (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; + + } + } } } } @@ -463,27 +491,26 @@ impl<'a> Page<'a> { } } } - } pub enum EventOrPageIndex { Event(io::Event), - Index(usize) + Index(usize), } pub struct MultiPageMenu<'a> { - comm : &'a mut io::Comm, + comm: &'a mut io::Comm, pages: &'a [&'a Page<'a>], } impl<'a> MultiPageMenu<'a> { - pub fn new(comm: &'a mut io::Comm, pages: &'a [&'a Page]) -> Self { + pub fn new(comm: &'a mut io::Comm, pages: &'a [&'a Page]) -> Self { MultiPageMenu { comm, pages } } pub fn show(&mut self) -> EventOrPageIndex { clear_screen(); - + self.pages[0].place(); LEFT_ARROW.display(); @@ -495,33 +522,31 @@ impl<'a> MultiPageMenu<'a> { loop { match self.comm.next_event() { - io::Event::Button(button) => { - 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); - } + io::Event::Button(button) => 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; - } + } + 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::screen_util::screen_update(); + _ => (), } + clear_screen(); + self.pages[index].place(); + LEFT_ARROW.display(); + RIGHT_ARROW.display(); + crate::screen_util::screen_update(); } }, io::Event::Command(ins) => return EventOrPageIndex::Event(io::Event::Command(ins)), @@ -649,7 +674,7 @@ pub struct Field<'a> { } pub struct MultiFieldReview<'a> { fields: &'a [Field<'a>], - review_message: &'a[&'a str], + review_message: &'a [&'a str], review_glyph: Option<&'a Glyph<'a>>, validation_message: &'a str, validation_glyph: Option<&'a Glyph<'a>>, @@ -675,14 +700,18 @@ fn concatenate(strings: &[&str], output: &mut [u8]) { } } +const MAX_REVIEW_PAGES: usize = 48; + 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 { + 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 { MultiFieldReview { fields, review_message, @@ -694,37 +723,60 @@ impl<'a> MultiFieldReview<'a> { } } - pub fn show(&self) -> bool { + pub fn show(&self) -> bool { let mut buttons = ButtonsState::new(); let first_page = match self.review_message.len() { - 0 => { - Page::new(PageStyle::PictureNormal, ["", ""], self.review_glyph) - } - 1 => { - Page::new(PageStyle::PictureBold, [self.review_message[0], ""], self.review_glyph) - } - _ => { - Page::new(PageStyle::PictureNormal, [self.review_message[0], self.review_message[1]], self.review_glyph) - } + 0 => Page::new(PageStyle::PictureNormal, ["", ""], self.review_glyph), + 1 => Page::new( + PageStyle::PictureBold, + [self.review_message[0], ""], + self.review_glyph, + ), + _ => Page::new( + PageStyle::PictureNormal, + [self.review_message[0], self.review_message[1]], + self.review_glyph, + ), }; - 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 review_pages: [Page; 32] = [Page::new(PageStyle::Normal, ["", ""], None); 32]; + + 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 review_pages: [Page; MAX_REVIEW_PAGES] = [Page::new(PageStyle::Normal, ["", ""], None); MAX_REVIEW_PAGES]; let mut total_page_count = 0; - + + let mut max_chars_per_page = MAX_CHAR_PER_LINE * 3; + if cfg!(target_os = "nanos") { + max_chars_per_page = MAX_CHAR_PER_LINE; + } + // Determine each field page count for field in self.fields { - let field_page_count = (field.value.len() - 1) / MAX_CHAR_PER_LINE + 1; - // Create pages for each chunk of the field + let field_page_count = (field.value.len() - 1) / max_chars_per_page + 1; + // Create pages for each chunk of the field for i in 0..field_page_count { - let start = i * MAX_CHAR_PER_LINE; - let end = (start + MAX_CHAR_PER_LINE).min(field.value.len()); + let start = i * max_chars_per_page; + let end = (start + max_chars_per_page).min(field.value.len()); let chunk = &field.value[start..end]; + review_pages[total_page_count] = Page::new(PageStyle::BoldNormal, [field.name, chunk], None); review_pages[total_page_count].chunk_count = field_page_count as u8; review_pages[total_page_count].chunk_idx = (i + 1) as u8; - total_page_count += 1; + // Check if we have reached the maximum number of pages + // We need to keep 2 pages for the validation and cancel pages + total_page_count = if total_page_count < MAX_REVIEW_PAGES - 2 { + total_page_count + 1 + } else { + break; + }; } } @@ -739,6 +791,7 @@ impl<'a> MultiFieldReview<'a> { RIGHT_ARROW.display(); let mut cur_page = 0; + let mut refresh : bool = true; review_pages[cur_page].place(); loop { @@ -749,34 +802,37 @@ impl<'a> MultiFieldReview<'a> { if cur_page > 0 { cur_page -= 1; } + refresh = true; } ButtonEvent::RightButtonRelease => { if cur_page < total_page_count { cur_page += 1; } + refresh = true; } - ButtonEvent::BothButtonsRelease => - { + ButtonEvent::BothButtonsRelease => { if cur_page == total_page_count { // Cancel return false; - } - else if cur_page == total_page_count - 1 { + } else if cur_page == total_page_count - 1 { // Validate return true; } } - _ => (), - } - clear_screen(); - review_pages[cur_page].place(); - if cur_page > 0 { - LEFT_ARROW.display(); + _ => refresh = false, } - if cur_page < total_page_count { - RIGHT_ARROW.display(); + if refresh + { + clear_screen(); + review_pages[cur_page].place(); + if cur_page > 0 { + LEFT_ARROW.display(); + } + if cur_page < total_page_count { + RIGHT_ARROW.display(); + } + crate::screen_util::screen_update(); } - crate::screen_util::screen_update(); } _ => (), }