From 72e6be8679cc4e9cd6f1677f9b29e1ed2bbfa056 Mon Sep 17 00:00:00 2001 From: bunnie Date: Sun, 19 Jun 2022 17:33:26 +0800 Subject: [PATCH] add some initial selectable list framework --- apps/vault/locales/i18n.json | 12 ++ apps/vault/src/main.rs | 31 +++- apps/vault/src/submenu.rs | 7 + apps/vault/src/ux/framework.rs | 276 +++++++++++++++++++++++++++++---- 4 files changed, 293 insertions(+), 33 deletions(-) diff --git a/apps/vault/locales/i18n.json b/apps/vault/locales/i18n.json index 0a500463f..5ad5503eb 100644 --- a/apps/vault/locales/i18n.json +++ b/apps/vault/locales/i18n.json @@ -137,6 +137,12 @@ "zh": "FIDO 请求", "en-tts": "FIDO Request" }, + "vault.select_font": { + "en": "Select a font style", + "ja": "フォントを選択する", + "zh": "选择字体", + "en-tts": "Select a font style" + }, "vault.menu_autotype": { "en": "Autotype", "ja": "自動的に入力", @@ -155,6 +161,12 @@ "zh": "删除项目", "en-tts": "Delete item" }, + "vault.menu_change_font": { + "en": "Change font", + "ja": "フォントを変更する", + "zh": "更改字体", + "en-tts": "Change font" + }, "vault.menu_close": { "en": "Close submenu", "ja": "メニューを閉じる", diff --git a/apps/vault/src/main.rs b/apps/vault/src/main.rs index caf542450..d334d8f28 100644 --- a/apps/vault/src/main.rs +++ b/apps/vault/src/main.rs @@ -5,6 +5,7 @@ mod ux; use ux::*; use num_traits::*; use xous_ipc::Buffer; +use xous::{send_message, Message}; use usbd_human_interface_device::device::fido::*; use std::thread; @@ -16,6 +17,8 @@ mod shims; use shims::*; mod submenu; +use locales::t; + // CTAP2 testing notes: // run our branch and use this to forward the prompts on to the device: // netcat -k -u -l 6502 > /dev/ttyS0 @@ -77,6 +80,7 @@ pub(crate) enum VaultOp { MenuAutotype, MenuEdit, MenuDelete, + MenuChangeFont, /// exit the application Quit, @@ -160,10 +164,13 @@ fn main() -> ! { let _menu_mgr = submenu::create_submenu(conn, menu_sid); let xns = xous_names::XousNames::new().unwrap(); + // TODO: add a UX loop that indicates we're waiting for a PDDB mount before moving forward let mut vaultux = VaultUx::new(&xns, sid); + vaultux.gen_fake_data(0); // THIS IS FOR TESTING ONLY replace with a call that does something "more proper" let mut update_vaultux = true; let mut was_callback = false; let mut allow_redraw = false; + let modals = modals::Modals::new(&xns).unwrap(); loop { let msg = xous::receive_message(sid).unwrap(); log::debug!("got message {:?}", msg); @@ -186,16 +193,16 @@ fn main() -> ! { vaultux.raise_menu(); } "↓" => { - log::info!("down arrow"); + vaultux.nav(NavDir::Down); } "↑" => { - log::info!("up arrow"); + vaultux.nav(NavDir::Up); } "←" => { - log::info!("left arrow"); + vaultux.nav(NavDir::PageUp); } "→" => { - log::info!("right arrow"); + vaultux.nav(NavDir::PageDown); } _ => { vaultux.input(s.as_str()).expect("Vault couldn't accept input string"); @@ -203,6 +210,9 @@ fn main() -> ! { } update_vaultux = true; // set a flag, instead of calling here, so message can drop and calling server is released was_callback = false; + send_message(conn, + Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0) + ).ok(); } Some(VaultOp::Redraw) => { if allow_redraw { @@ -230,6 +240,19 @@ fn main() -> ! { Some(VaultOp::MenuEdit) => { log::info!("got edit"); } + Some(VaultOp::MenuChangeFont) => { + for item in FONT_LIST { + modals + .add_list_item(item) + .expect("couldn't build radio item list"); + } + match modals.get_radiobutton(t!("vault.select_font", xous::LANG)) { + Ok(style) => { + vaultux.set_glyph_style(name_to_style(&style).unwrap_or(DEFAULT_FONT)); + }, + _ => log::error!("get_radiobutton failed"), + } + } Some(VaultOp::Quit) => { log::error!("got Quit"); break; diff --git a/apps/vault/src/submenu.rs b/apps/vault/src/submenu.rs index 7baf85085..ffdac9463 100644 --- a/apps/vault/src/submenu.rs +++ b/apps/vault/src/submenu.rs @@ -28,6 +28,13 @@ pub fn create_submenu(vault_conn: xous::CID, menu_mgr: xous::SID) -> MenuMatic { action_payload: MenuPayload::Scalar([0, 0, 0, 0]), close_on_select: true, }); + menu_items.push(MenuItem { + name: xous_ipc::String::from_str(t!("vault.menu_change_font", xous::LANG)), + action_conn: Some(vault_conn), + action_opcode: VaultOp::MenuChangeFont.to_u32().unwrap(), + action_payload: MenuPayload::Scalar([0, 0, 0, 0]), + close_on_select: true, + }); menu_items.push(MenuItem { name: xous_ipc::String::from_str(t!("vault.menu_close", xous::LANG)), action_conn: None, diff --git a/apps/vault/src/ux/framework.rs b/apps/vault/src/ux/framework.rs index e4b33e6ac..b64080da7 100644 --- a/apps/vault/src/ux/framework.rs +++ b/apps/vault/src/ux/framework.rs @@ -3,6 +3,45 @@ use gam::{UxRegistration, GlyphStyle}; use graphics_server::{Gid, Point, Rectangle, DrawStyle, PixelColor, TextView}; use xous::MessageEnvelope; use std::fmt::Write; +use pddb::Pddb; +use std::cell::RefCell; +use std::cmp::Ordering; +use std::io::{Read, Write as FsWrite}; + +/// Display list for items. "name" is the key by which the list is sorted. +/// "extra" is more information about the item, which should not be part of the sort. +struct ListItem { + name: String, + extra: String, +} +impl ListItem { + pub fn clone(&self) -> ListItem { + ListItem { name: self.name.to_string(), extra: self.extra.to_string() } + } +} +impl Ord for ListItem { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} +impl PartialOrd for ListItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl PartialEq for ListItem { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} +impl Eq for ListItem {} + +pub(crate) enum NavDir { + Up, + Down, + PageUp, + PageDown, +} #[allow(dead_code)] pub(crate) struct VaultUx { @@ -20,16 +59,60 @@ pub(crate) struct VaultUx { screensize: Point, margin: Point, // margin to edge of canvas - // our security token for making changes to our record on the GAM + /// our security token for making changes to our record on the GAM token: [u32; 4], - // current operation mode + /// current operation mode mode: VaultMode, + + /// list of all items to be displayed + item_list: Vec::, + /// list of items displayable after filtering + filtered_list: Vec::, + /// the index into the item_list that is selected + selection_index: usize, + + /// pddb handle + pddb: RefCell::, + + /// current font style + style: GlyphStyle, + item_height: i16, + items_per_screen: i16, +} + +pub(crate) const DEFAULT_FONT: GlyphStyle = GlyphStyle::Regular; +pub(crate) const FONT_LIST: [&'static str; 6] = [ + "regular", "mono", "cjk", + "bold", "large", "small" +]; +pub(crate) fn name_to_style(name: &str) -> Option { + match name { + "regular" => Some(GlyphStyle::Regular), + "mono" => Some(GlyphStyle::Monospace), + "cjk" => Some(GlyphStyle::Cjk), + "bold" => Some(GlyphStyle::Bold), + "large" => Some(GlyphStyle::Large), + "small" => Some(GlyphStyle::Small), + _ => None + } +} +fn style_to_name(style: &GlyphStyle) -> String { + match style { + GlyphStyle::Regular => "regular".to_string(), + GlyphStyle::Monospace => "mono".to_string(), + GlyphStyle::Cjk => "cjk".to_string(), + GlyphStyle::Bold => "bold".to_string(), + GlyphStyle::Large => "large".to_string(), + GlyphStyle::Small => "small".to_string(), + _ => "regular".to_string(), + } } const TITLE_HEIGHT: i16 = 26; - -impl VaultUx{ +const VAULT_CONFIG_DICT: &'static str = "vault.config"; +const VAULT_CONFIG_KEY_FONT: &'static str = "fontstyle"; +impl VaultUx { pub(crate) fn new(xns: &xous_names::XousNames, sid: xous::SID) -> Self { let gam = gam::Gam::new(xns).expect("can't connect to GAM"); @@ -49,6 +132,46 @@ impl VaultUx{ let content = gam.request_content_canvas(token).expect("couldn't get content canvas"); let screensize = gam.get_canvas_bounds(content).expect("couldn't get dimensions of content canvas"); gam.toggle_menu_mode(token).expect("couldnt't toggle menu mode"); + let margin = Point::new(4, 4); + + let pddb = pddb::Pddb::new(); + // TODO: put some informative message asking to mount the PDDB if it's not mounted, right now you just get a blank screen. + pddb.is_mounted_blocking(); + // extract the style name from the settings, or update them if never initialized + let mut style_name_bytes = [0u8; 32]; + let style_name = match pddb.get( + VAULT_CONFIG_DICT, + VAULT_CONFIG_KEY_FONT, + None, true, true, + Some(32), None:: + ) { + Ok(mut style_key) => { + style_key.read(&mut style_name_bytes).ok(); + String::from_utf8_lossy(&style_name_bytes) + } + _ => panic!("PDDB access erorr"), + }; + let style = match name_to_style(style_name.as_ref()) { + Some(s) => s, + None => { + match pddb.get( + VAULT_CONFIG_DICT, + VAULT_CONFIG_KEY_FONT, + None, true, true, + Some(32), None:: + ) { + Ok(mut style_key) => { + style_key.write(style_to_name(&DEFAULT_FONT).as_bytes()).ok(); + } + _ => panic!("PDDB access erorr"), + }; + GlyphStyle::Regular + }, + }; + let available_height = screensize.y - TITLE_HEIGHT; + let glyph_height = gam.glyph_height_hint(style).unwrap(); + let item_height = (glyph_height * 2) as i16 + margin.y * 2 + 2; // +2 because of the border width + let items_per_screen = available_height / item_height; VaultUx { input: None, @@ -56,12 +179,76 @@ impl VaultUx{ content, gam, screensize, - margin: Point::new(4, 4), + margin, token, mode: VaultMode::Fido, + item_list: Vec::new(), + selection_index: 0, + filtered_list: Vec::new(), + pddb: RefCell::new(pddb), + style, + item_height, + items_per_screen, + } + } + pub(crate) fn set_mode(&mut self, mode: VaultMode) { + self.item_list.clear(); + match mode { + VaultMode::Fido | VaultMode::Password => self.gen_fake_data(0), + VaultMode::Totp => self.gen_fake_data(1), + } + self.item_list.sort(); + self.selection_index = 0; + self.filter(""); + self.mode = mode; + } + pub(crate) fn set_glyph_style(&mut self, style: GlyphStyle) { + self.pddb.borrow().delete_key(VAULT_CONFIG_DICT, VAULT_CONFIG_KEY_FONT, None) + .expect("couldn't delete previous setting"); + + match self.pddb.borrow().get( + VAULT_CONFIG_DICT, + VAULT_CONFIG_KEY_FONT, + None, true, true, + Some(32), None:: + ) { + Ok(mut style_key) => { + style_key.write(style_to_name(&style).as_bytes()).ok(); + } + _ => panic!("PDDB access erorr"), + }; + self.style = style; + let available_height = self.screensize.y - TITLE_HEIGHT; + let glyph_height = self.gam.glyph_height_hint(self.style).unwrap(); + self.item_height = (glyph_height * 2) as i16 + self.margin.y * 2 + 2; // +2 because of the border width + self.items_per_screen = available_height / self.item_height; + } + pub(crate) fn nav(&mut self, dir: NavDir) { + match dir { + NavDir::Up => { + if self.selection_index > 0 {self.selection_index -= 1;} + } + NavDir::Down => { + if self.selection_index < self.filtered_list.len() - 1 { + self.selection_index += 1; + } + } + NavDir::PageUp => { + if self.selection_index > self.items_per_screen as usize { + self.selection_index -= self.items_per_screen as usize; + } else { + self.selection_index = 0; + } + } + NavDir::PageDown => { + if self.selection_index < self.filtered_list.len() - 1 - self.items_per_screen as usize { + self.selection_index += self.items_per_screen as usize; + } else { + self.selection_index = self.filtered_list.len() - 1; + } + } } } - pub(crate) fn set_mode(&mut self, mode: VaultMode) { self.mode = mode; } /// accept a new input string pub(crate) fn input(&mut self, line: &str) -> Result<(), xous::Error> { self.input = Some(String::from(line)); @@ -110,49 +297,39 @@ impl VaultUx{ self.gam.post_textview(&mut title_text).expect("couldn't post title"); // ---- draw list body area ---- - let available_height = self.screensize.y - TITLE_HEIGHT; - let style = GlyphStyle::Large; - let glyph_height = self.gam.glyph_height_hint(style).unwrap(); - let box_height = (glyph_height * 2) as i16 + self.margin.y * 2 + 2; // +2 because of the border width - let line_count = available_height / box_height; - - let mut test_list = Vec::new(); - for i in 0..16 { - let test_string = format!("Test item {}\nMore info about the item.", i); - test_list.push(test_string); - } - // line up the list to justify to the bottom of the screen, based on the actual font requested - let available_height = self.screensize.y - TITLE_HEIGHT; - let fitting_items = available_height / box_height; - let items_height = fitting_items * box_height; + let items_height = self.items_per_screen * self.item_height; let mut insert_at = 1 + self.screensize.y - items_height; // +1 to get the border to overlap at the bottom - let selected = 2; - for (index, item) in test_list.iter().enumerate() { - if insert_at - 1 > self.screensize.y - box_height { // -1 because of the overlapping border + let page = self.selection_index as i16 / self.items_per_screen; + let selected = self.selection_index as i16 % self.items_per_screen; + for (index, item) in self.filtered_list[ + ((page as usize) * self.items_per_screen as usize).min(self.filtered_list.len()) .. + ((1 + page as usize) * self.items_per_screen as usize).min(self.filtered_list.len()) + ].iter().enumerate() { + if insert_at - 1 > self.screensize.y - self.item_height { // -1 because of the overlapping border break; } let mut box_text = TextView::new(self.content, graphics_server::TextBounds::BoundingBox( Rectangle::new( Point::new(0, insert_at), - Point::new(self.screensize.x, insert_at + box_height) + Point::new(self.screensize.x, insert_at + self.item_height) ) ) ); box_text.draw_border = true; box_text.rounded_border = None; box_text.clear_area = true; - box_text.style = style; + box_text.style = self.style; box_text.margin = self.margin; - if index == selected { + if index == selected as usize { box_text.border_width = 4; } - write!(box_text, "{}", item).ok(); + write!(box_text, "{}\n{}", item.name, item.extra).ok(); self.gam.post_textview(&mut box_text).expect("couldn't post list item"); - insert_at += box_height; + insert_at += self.item_height; } log::trace!("vault app redraw##"); @@ -163,4 +340,45 @@ impl VaultUx{ pub(crate) fn raise_menu(&self) { self.gam.raise_menu(gam::APP_MENU_0_VAULT).expect("couldn't raise our submenu"); } + + pub(crate) fn filter(&mut self, criteria: &str) { + self.filtered_list.clear(); + for item in self.item_list.iter() { + if item.name.starts_with(criteria) { + self.filtered_list.push(item.clone()); + } + } + if self.selection_index >= self.filtered_list.len() { + self.selection_index = self.filtered_list.len() - 1; + } + } + + // populates the display list with testing data + pub(crate) fn gen_fake_data(&mut self, set: usize) { + if set == 0 { + self.item_list.push(ListItem { name: "test.com".to_string(), extra: "Used 5 mins ago".to_string() }); + self.item_list.push(ListItem { name: "google.com".to_string(), extra: "Never used".to_string() }); + self.item_list.push(ListItem { name: "my app".to_string(), extra: "Used 2 hours ago".to_string() }); + self.item_list.push(ListItem { name: "💎🙌".to_string(), extra: "Used 2 days ago".to_string() }); + self.item_list.push(ListItem { name: "百度".to_string(), extra: "Used 1 month ago".to_string() }); + self.item_list.push(ListItem { name: "duplicate.com".to_string(), extra: "Used 1 week ago".to_string() }); + self.item_list.push(ListItem { name: "duplicate.com".to_string(), extra: "Used 8 mins ago".to_string() }); + self.item_list.push(ListItem { name: "amazon.com".to_string(), extra: "Used 3 days ago".to_string() }); + self.item_list.push(ListItem { name: "ziggyziggyziggylongdomain.com".to_string(), extra: "Never used".to_string() }); + self.item_list.push(ListItem { name: "another long domain name.com".to_string(), extra: "Used 2 months ago".to_string() }); + self.item_list.push(ListItem { name: "bunniestudios.com".to_string(), extra: "Used 30 mins ago".to_string() }); + self.item_list.push(ListItem { name: "github.com".to_string(), extra: "Used 6 hours ago".to_string() }); + } else { + self.item_list.push(ListItem { name: "gmail.com".to_string(), extra: "162 321".to_string() }); + self.item_list.push(ListItem { name: "google.com".to_string(), extra: "445 768".to_string() }); + self.item_list.push(ListItem { name: "my 图片 app".to_string(), extra: "982 111".to_string() }); + self.item_list.push(ListItem { name: "🍕🍔🍟🌭".to_string(), extra: "056 182".to_string() }); + self.item_list.push(ListItem { name: "百度".to_string(), extra: "111 111".to_string() }); + self.item_list.push(ListItem { name: "duplicate.com".to_string(), extra: "462 124".to_string() }); + self.item_list.push(ListItem { name: "duplicate.com".to_string(), extra: "462 124".to_string() }); + self.item_list.push(ListItem { name: "amazon.com".to_string(), extra: "842 012".to_string() }); + self.item_list.push(ListItem { name: "ziggyziggyziggylongdomain.com".to_string(), extra: "462 212".to_string() }); + self.item_list.push(ListItem { name: "github.com".to_string(), extra: "Used 6 hours ago".to_string() }); + } + } } \ No newline at end of file